Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義ViewGroup之WaterfallLayout(二)

Android自定義ViewGroup之WaterfallLayout(二)

編輯:關於Android編程

上一篇我們學習了自定義ViewGroup的基本步驟,並做了一個CustomGridLayout的實例,這篇我們繼續來說說自定義ViewGroup。
Android中當有大量照片需要展示的時候,我們可以用GridView作為照片牆,但是GridView太整齊了,有時候不規則也是一種美,瀑布流模型就是這樣一個不規則的展示牆,接下來我們嘗試用自定義ViewGroup來實現瀑布流。
實現瀑布流的方式也有很多,下面我們一一道來:

一、繼承ViewGroup
其實這種實現方式我們只需要在上篇博客的基礎上稍作修改即可,主要修改這幾個地方:
 •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寬的最大值,否則動態計算
 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會執行兩次,為了以後執行得到正確的結果
 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的寬高就發生了改變,這個時候又會重新調用一次onMeasure和onLayout方法(當view發生改變的時候,這兩個方法會被調用),這時候你通過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動態添加ImageView,而且代碼更美觀。
實現瀑布流圖片的點擊事件回調函數:

((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的源碼,你會發現有一個地方,同時可以理解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實現每個childView的layout方法,那麼childView是怎麼布局的呢?其實childView的布局是通過LinearLayout來實現的,也就是說在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>

實現瀑布流圖片的點擊事件回調函數:

((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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved