Android自定義ViewGroup之WaterfallLayout(二)
上一篇我們學習了自定義ViewGroup的基本步驟,并做了一個CustomGridLayout的實例,這篇我們繼續(xù)來說說自定義ViewGroup。
Android中當有大量照片需要展示的時候,我們可以用GridView作為照片墻,但是GridView太整齊了,有時候不規(guī)則也是一種美,瀑布流模型就是這樣一個不規(guī)則的展示墻,接下來我們嘗試用自定義ViewGroup來實現(xiàn)瀑布流。
實現(xiàn)瀑布流的方式也有很多,下面我們一一道來:
一、繼承ViewGroup
其實這種實現(xiàn)方式我們只需要在上篇博客的基礎上稍作修改即可,主要修改這幾個地方:
•LayoutParams
因為瀑布流中每張圖片寬度設為相同,高度則會不同,不能通過top加上固定高度得到bottom,所以這里我干脆把四個參數都定義上
public static class LayoutParams extends ViewGroup.LayoutParams {
public int left = 0;
public int top = 0;
public int right = 0;
public int bottom = 0;
public LayoutParams(Context arg0, AttributeSet arg1) {
super(arg0, arg1);
}
public LayoutParams(int arg0, int arg1) {
super(arg0, arg1);
}
public LayoutParams(android.view.ViewGroup.LayoutParams arg0) {
super(arg0);
}
}
•onMeasure
這里每個圖片寬相同,高等比縮放,所以會導致WaterfallLayout的layout_height沒有用。同時用一個數組top[colums]來記錄每列當前高度,以便下次添加圖片的時候添加到高度最小的那一列。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int childCount = this.getChildCount();
//寬布局為wrap_content時,childWidth取childView寬的最大值,否則動態(tài)計算
if (widthMode == MeasureSpec.AT_MOST) {
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
childWidth = Math.max(childWidth, child.getMeasuredWidth());
}
} else if (widthMode == MeasureSpec.EXACTLY) {
childWidth = (sizeWidth - (colums - 1) * hSpace) / colums;
}
//自定義View的onMeasure、onLayout會執(zhí)行兩次,為了以后執(zhí)行得到正確的結果
clearTop();
//遍歷每個子view,將它們坐標保存在它們的LayoutParams中,為后面onLayout服務
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
childHeight = child.getMeasuredHeight() * childWidth / child.getMeasuredWidth();
LayoutParams lParams = (LayoutParams) child.getLayoutParams();
int minColum = getMinHeightColum();
lParams.left = minColum * (childWidth + hSpace);
lParams.top = top[minColum];
lParams.right = lParams.left + childWidth;
lParams.bottom = lParams.top + childHeight;
top[minColum] += vSpace + childHeight;
}
//當寬為wrap_content時,計算出的viewGroup寬高
int wrapWidth;
int wrapHeight;
if (childCount < colums) {
wrapWidth = childCount * childWidth + (childCount - 1) * hSpace;
} else {
wrapWidth = colums * childWidth + (colums - 1) * hSpace;
}
wrapHeight = getMaxHeight();
setMeasuredDimension(widthMode == MeasureSpec.AT_MOST? wrapWidth:sizeWidth, wrapHeight);
}
•onLayout
因為LayoutParams定義了View的四個參數,所以直接設置即可
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = this.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = this.getChildAt(i);
LayoutParams lParams = (LayoutParams) child.getLayoutParams();
child.layout(lParams.left, lParams.top, lParams.right, lParams.bottom);
}
}
這里有個地方需要注意一下,每次設置子View的LayoutParams前需要將top[]數組清零,因為onMeasure和onLayout會調用兩次,這樣就確保了下一次設置參數正確。
延伸:為什么自定義viewGroup中的onMeasure和onLayout方法會調用兩次?
因為當我們new ViewGroup()的時候,通過getWidth()和getHeight(),得到的值首先是0,0,然后通過調用onMeasure()和onLayout()方法,會對這個view測量大小,這個時候view的寬高就發(fā)生了改變,這個時候又會重新調用一次onMeasure和onLayout方法(當view發(fā)生改變的時候,這兩個方法會被調用),這時候你通過getWidth和getHeight方法就可以看到被測量之后的寬高了。這就是會調用兩次的原因。
•點擊事件回調
//點擊事件的回調接口
public interface OnItemClickListener {
void onItemClick(View v, int index);
}
public void setOnItemClickListener(final OnItemClickListener listener) {
for (int i = 0; i < getChildCount(); i++) {
final int index = i;
View view = getChildAt(i);
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(v, index);
}
});
}
}
使用WaterfallLayout來添加圖片:
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/com.hx.waterfalllayout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#303030" android:orientation="vertical" > <com.hx.waterfalllayout.WaterfallLayout android:id="@+id/gridview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#1e1d1d" app:hSpace="10" app:numColumns="3" app:vSpace="10" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_1" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_2" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_1" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_2" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_1" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_2" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_1" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:src="@drawable/crazy_2" /> </com.hx.waterfalllayout.WaterfallLayout> </ScrollView>
這里最外層我們用的ScrollView,因為照片墻可以無限添加照片,為了讓照片數量在超出頻幕范圍后可以滾動。還有這里ImageView都是在xml中寫的,當然我們也可以在Java中向這個ViewGroup動態(tài)添加ImageView,而且代碼更美觀。
實現(xiàn)瀑布流圖片的點擊事件回調函數:
((WaterfallLayout) findViewById(R.id.waterfallLayout)).setOnItemClickListener(new com.hx.waterfalllayout.WaterfallLayout.OnItemClickListener() {
@Override
public void onItemClick(View v, int index) {
Toast.makeText(MainActivity.this, "item="+index, Toast.LENGTH_SHORT).show();
}
});
來看看運行效果:

延伸:
一般我們自定義的控件,嵌套在scrollview中會顯示不全,這個問題很糾結,不過當你打開scrollview的源碼,你會發(fā)現(xiàn)有一個地方,同時可以理解scrollview中嵌套viewpager,gridview,listview時候會顯示不全的問題了。

這里有個小技巧可以讓嵌套的viewpager,gridview,listview顯示完全,譬如我們可以定義自己的OtherGridView繼承Gridview,并重寫onMeasure方法即可,其他ViewGroup同理:
public class OtherGridView extends GridView {
public OtherGridView(Context paramContext, AttributeSet paramAttributeSet) {
super(paramContext, paramAttributeSet);
}
/** 在ScrollView內,所以要進行計算高度 */
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
二、繼承ScrollView
繼承ScrollView的瀑布流模型當圖片過多需要滑動式不必在外面再嵌套一個ScrollView。
這時不需要重寫onMesure,只需要重寫onLayout
•onLayout
/**
* 進行一些關鍵性的初始化操作,獲取ScrollWaterfallLayout的高度,以及得到第一列的寬度值。并在這里開始加載圖片
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && !loadOnce) {
firstColumn = (LinearLayout) findViewById(R.id.first_column);
secondColumn = (LinearLayout) findViewById(R.id.second_column);
thirdColumn = (LinearLayout) findViewById(R.id.third_column);
columnWidth = firstColumn.getWidth();
loadOnce = true;
loadImages();
}
}
•加載圖片
/**
* 開始加載圖片
*/
public void loadImages() {
for (int i = 0; i < imageRes.length; i++) {
Bitmap bitmap = resource2Bitmap(imageRes[i]);
if (bitmap != null) {
double ratio = bitmap.getWidth() / (columnWidth * 1.0);
int scaledHeight = (int) (bitmap.getHeight() / ratio);
addImage(i, bitmap, columnWidth, scaledHeight);
}
}
}
/**
* 向ImageView中添加一張圖片
*
* @param bitmap
* 待添加的圖片
* @param imageWidth
* 圖片的寬度
* @param imageHeight
* 圖片的高度
*/
private void addImage(int index, Bitmap bitmap, int imageWidth, int imageHeight) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(imageWidth, imageHeight);
ImageView imageView = new ImageView(getContext());
imageView.setLayoutParams(params);
imageView.setImageBitmap(bitmap);
imageView.setScaleType(ScaleType.FIT_XY);
imageView.setPadding(5, 5, 5, 5);
findColumnToAdd(imageView, imageHeight).addView(imageView);
//給圖片添加點擊事件的回調
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(v, index);
}
}
});
}
/**
* 找到此時應該添加圖片的一列。原則就是對三列的高度進行判斷,當前高度最小的一列就是應該添加的一列。
*
* @param imageView
* @param imageHeight
* @return 應該添加圖片的一列
*/
private LinearLayout findColumnToAdd(ImageView imageView,int imageHeight) {
if (firstColumnHeight <= secondColumnHeight) {
if (firstColumnHeight <= thirdColumnHeight) {
firstColumnHeight += imageHeight;
return firstColumn;
}
thirdColumnHeight += imageHeight;
return thirdColumn;
} else {
if (secondColumnHeight <= thirdColumnHeight) {
secondColumnHeight += imageHeight;
return secondColumn;
}
thirdColumnHeight += imageHeight;
return thirdColumn;
}
}
到這里就可以顯示瀑布流照片墻了,是不是很方便呢?但是這種方式也有局限性,譬如這里列寬被寫死成3列了,沒有很好的擴展性。
代碼里我們并沒有看到自定義ViewGroup實現(xiàn)每個childView的layout方法,那么childView是怎么布局的呢?其實childView的布局是通過LinearLayout來實現(xiàn)的,也就是說在LinearLayout內部調用了每個childView的layout方法,這是不是和之前我們講自定義View時的組合控件很像呢?
findColumnToAdd(imageView, imageHeight).addView(imageView);
•定義圖片點擊回調接口
//點擊事件的回調接口
public OnItemClickListener onItemClickListener;
public interface OnItemClickListener {
void onItemClick(View v, int index);
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener){
this.onItemClickListener = onItemClickListener;
}
•使用ScrollWaterfallLayout
因為代碼里指定了只有三列,所以xml需要三個水平擺放的LinearLayout
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <com.hx.waterfalllayout.ScrollWaterfallLayout android:id="@+id/scrollWaterfallLayout" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <LinearLayout android:id="@+id/first_column" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" > </LinearLayout> <LinearLayout android:id="@+id/second_column" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" > </LinearLayout> <LinearLayout android:id="@+id/third_column" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" > </LinearLayout> </LinearLayout> </com.hx.waterfalllayout.ScrollWaterfallLayout> </LinearLayout>
實現(xiàn)瀑布流圖片的點擊事件回調函數:
((ScrollWaterfallLayout)findViewById(R.id.scrollWaterfallLayout)).setOnItemClickListener(new com.hx.waterfalllayout.ScrollWaterfallLayout.OnItemClickListener() {
@Override
public void onItemClick(View v, int index) {
Toast.makeText(MainActivity.this, "item="+index, Toast.LENGTH_SHORT).show();
}
});
運行效果:

源碼下載:http://xiazai.jb51.net/201609/yuanma/Android-WaterfallLayout(jb51.net).rar
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Android自定義ViewGroup之實現(xiàn)FlowLayout流式布局
- Android簡單實現(xiàn)自定義流式布局的方法
- Android實現(xiàn)熱門標簽的流式布局
- 解析在Android中為TextView增加自定義HTML標簽的實現(xiàn)方法
- android配合viewpager實現(xiàn)可滑動的標簽欄示例分享
- Android中使用include標簽和merge標簽重復使用布局
- Android開發(fā)常用標簽小結
- android多行標簽熱點示例
- Android自定義ViewGroup之CustomGridLayout(一)
- Android自定義ViewGroup之FlowLayout(三)
相關文章
Android開發(fā)實現(xiàn)Gallery畫廊效果的方法
這篇文章主要介紹了Android開發(fā)實現(xiàn)Gallery畫廊效果的方法,結合具體實例形式分析了Android使用Gallery實現(xiàn)畫廊功能的具體操作技巧與相關注意事項,需要的朋友可以參考下2017-06-06
Android ListView與getView調用卡頓問題解決辦法
這篇文章主要介紹了Android ListView與getView調用卡頓問題解決辦法的相關資料,這里提供實例及解決辦法幫助大家解決這種問題,需要的朋友可以參考下2017-08-08
Android?使用maven?publish插件發(fā)布產物(aar)流程實踐
這篇文章主要介紹了Android?使用maven?publish插件發(fā)布產物(aar)流程實踐,Android?Gradle插件根據項目gradle中應用不同的插件類型在編譯組裝后會生成不同的產物,具體相關介紹,需要的小伙伴可以參考一下2022-09-09
Android實戰(zhàn)教程第四十篇之Chronometer實現(xiàn)倒計時
這篇文章主要介紹了Android實戰(zhàn)教程第四十篇之Chronometer實現(xiàn)倒計時,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
Android編程之界面跳動提示動畫效果實現(xiàn)方法
這篇文章主要介紹了Android編程之界面跳動提示動畫效果實現(xiàn)方法,實例分析了Android動畫效果的布局及功能相關實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11

