標籤:
在我的上篇博文Android深入遷出自訂控制項(一)中介紹了如何自訂View控制項,本篇博文主要介紹如何自訂ViewGroup
什麼是ViewGroup?在Android的樹狀結構圖中,ViewGroup類衍生出我們所熟悉的LinearLayout、RelativeLayout等布局:
簡單來說,ViewGroup其實就相當於所有布局的父親,所以我們可以通過自訂ViewGroup類實現千變萬化的布局。
自訂ViewGroup主要分為以下幾個步驟:1.建立自訂ViewGroup類,繼承於ViewGroup類,重寫ViewGroup的三個構造方法
2.重寫onMeasure方法,設定好ViewGroup及其子View在介面上所顯示的大小
3.添加參數
4.重寫onLayout方法,設定好子View的布局,這個方法也是最為關鍵的,它決定了你所自訂的布局的特性
1.建立自訂ViewGroup類,繼承於ViewGroup類,重寫ViewGroup的三個構造方法
public class FlowLayoutextends ViewGroup{public FlowLayout(Context context) {// TODO Auto-generated constructor stubthis(context,null);}public FlowLayout(Context context, AttributeSet attrs) {// TODO Auto-generated constructor stubthis(context,attrs,0);}public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);// TODO Auto-generated constructor stub}}
2.重寫onMeasure方法,設定好ViewGroup及其子View在介面上所顯示的大小
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// TODO Auto-generated method stubmeasureChildren(widthMeasureSpec, heightMeasureSpec);super.onMeasure(widthMeasureSpec, heightMeasureSpec);}
對這個方法的使用見下文,此處記得添加measureChildren(widthMeasureSpec, heightMeasureSpec);表示由系統自己測量每個子View的大小
3.添加參數,比如我們想要讓子View之間能有間距,則需要手動建立一個內部參數類
public static class FlowLayoutParams extends ViewGroup.MarginLayoutParams{public FlowLayoutParams(Context context, AttributeSet attrs) {super(context, attrs);// TODO Auto-generated constructor stub}}
4.重寫onLayout方法,設定好子View的布局,這個方法也是最為關鍵的,它決定了你所自訂的布局的特性
@Overrideprotected void onLayout(boolean arg0, int left, int top, int right, int bottom) {// TODO Auto-generated method stub//獲得FlowLayout所測量出來的寬度int mViewGroupWidth = getMeasuredWidth();/*** paintX:繪製每個View時的游標起點的橫座標* paintY:繪製每個View時的游標起點的縱座標*/int paintX = left;int paintY = top;//用於記錄上一行的最大高度int maxlineHeight = 0;int childCount = getChildCount();for(int i=0; i<childCount; i++){View childView = getChildAt(i);//獲得每個子View的margin參數FlowLayout.FlowLayoutParams params = (FlowLayout.FlowLayoutParams)childView.getLayoutParams();//獲得每個子View所測量出來的寬度和高度int childViewWidth = childView.getMeasuredWidth();int childViewHeight = childView.getMeasuredHeight();//如果繪製的起點橫座標+左間距+子View的寬度+右間距比FlowLayout的寬度還大,就需要進行換行if(paintX + childViewWidth + params.leftMargin + params.rightMargin> mViewGroupWidth){//繪製的起點的橫座標重新移回FlowLayout的橫座標paintX = left;//繪製的起點的縱座標要向下移動一行的高度paintY = paintY + maxlineHeight + params.topMargin + params.bottomMargin;maxlineHeight = 0;}maxlineHeight = Math.max(childViewHeight, maxlineHeight);childView.layout(paintX+params.leftMargin, paintY+params.topMargin, paintX+childViewWidth+params.leftMargin, paintY+childViewHeight+params.topMargin);//每繪製一次,起點游標就要向右移動一次paintX = paintX + childViewWidth + params.leftMargin + params.rightMargin;}}
解析:在onLayout方法中,我們對每個子View進行遍曆並設定好它們應該在的位置,比如是LinearLayout的話,在onLayout中系統會規定它的子View只能按著橫向或者豎向排列下去,也就是說,onLayout裡子View的排布是按著我們自己的想法來決定,到底這個自訂布局會有怎樣的特性?
此處我們demo是自訂FlowLayout,也就是每一行都向右排列下去,直到這一行不夠容納,則子View自動換行,進入下一行,依此類推,從而實現流式布局,為了實現這樣的效果,最關鍵的應該是在換行的時候,需要實現讓我們的子View能夠判斷到底換不換行,代碼思路如下:
1.首先需要記錄FlowLayout的寬度, 作為每一行的寬度上限:
int mViewGroupWidth = getMeasuredWidth();
2.每次繪製一個子View,是通過View.layout()方法來進行,而layout方法需要提供4個參數,即(繪製的起點橫座標,繪製的起點縱座標,子View的寬度,子View的高度),而每一個子View的繪製起點肯定不一樣,所以需要定義兩個變數來記錄:paintX,paintY:
/** * paintX:繪製每個View時的游標起點的橫座標 * paintY:繪製每個View時的游標起點的縱座標 */int paintX = left;int paintY = top;
3.通過for迴圈,遍曆得到每個子View,由於要讓子View之間能夠有間距,所以還需要定義一個margin參數提供給子View:
FlowLayout.FlowLayoutParams params = (FlowLayout.FlowLayoutParams)childView.getLayoutParams();
以及獲得每個子View的寬高:
int childViewWidth = childView.getMeasuredWidth();int childViewHeight = childView.getMeasuredHeight();
4.判斷如果再添加一個子View,需不需要換行,所以需要將這個View的寬度和當前行的寬度相加,與FlowLayout的寬度(即上限)進行對比,如果超過上限,就進行換行操作:
//繪製的起點的橫座標重新移回FlowLayout的橫座標paintX = left;//繪製的起點的縱座標要向下移動一行的高度paintY = paintY + maxlineHeight + params.topMargin + params.bottomMargin;//由於換行,所以當前行變成了下一行,最大高度自然也就置為當前這個新起行的子View的高度(因為它是下一行的第一個View)maxlineHeight = childViewHeight;
5.繪製當前子View,游標移動至下一個起點:
//每次都要計算當前行的最大高度maxlineHeight = Math.max(childViewHeight, maxlineHeight);childView.layout(paintX+params.leftMargin, paintY+params.topMargin, paintX+childViewWidth+params.leftMargin, paintY+childViewHeight+params.topMargin);//每繪製一次,起點游標就要向右移動一次paintX = paintX + childViewWidth + params.leftMargin + params.rightMargin;
至此,一個基本的FlowLayout布局定義完成,接下來就只需要在布局檔案中來使用它:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="fill_parent" > <com.example.view.FlowLayout android:layout_width="fill_parent" android:layout_height="fill_parent" > <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="全部" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="體育" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="連續劇" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="綜藝" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="電影" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="搞笑" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="兒童教育" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="技術教程" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="音樂頻道" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="賽事直播" /> </com.example.view.FlowLayout></RelativeLayout>
運行:
可以看到達到了我們所要的效果,但這裡我們設定的FlowLayout的大小都是fill_parent,如果改為wrap_content會怎樣?
將FlowLayout的layout_height設定為wrap_content,雖然螢幕上仍然呈現的是一樣的效果,可是在預覽視窗中點擊FlowLayout,可以看到其實FlowLayout依舊是fill_parent的大小,而不是貼著子View的邊緣,
這不是我們想要的效果,正確的做法應該是:設定為wrap_content時,FlowLayout的邊界是緊緊貼著子View的邊緣的,所以我們應該修改onMeasure方法:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// TODO Auto-generated method stub//measureChildren(widthMeasureSpec, heightMeasureSpec);super.onMeasure(widthMeasureSpec, heightMeasureSpec);//得到FlowLayout的模式和寬高int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);//記錄wrap_content下的最終寬度和高度int width = 0;int height = 0;//記錄每一行的最大寬度和最大高度int lineWidth = 0;int lineHeight = 0;int childCount = getChildCount();for(int i=0;i<childCount;i++){View childView = getChildAt(i);measureChild(childView, widthMeasureSpec, heightMeasureSpec);FlowLayout.FlowLayoutParams params = (FlowLayout.FlowLayoutParams)childView.getLayoutParams();int childWidth = params.leftMargin + childView.getMeasuredWidth() + params.rightMargin;int childHeight = params.topMargin + childView.getMeasuredHeight() + params.bottomMargin;//如果是換行,則比較寬度取當前行的最大寬度和下一個子View的寬度,將最大者暫時作為FlowLayout的寬度if(lineWidth + childWidth > widthSize){width = Math.max(lineWidth, childWidth); height = height + lineHeight;lineWidth = childWidth;lineHeight = childHeight;}else // 否則累加值lineWidth,lineHeight取最大高度 { lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); } //如果是最後一個子Viewif (i == childCount - 1){ width = Math.max(width, lineWidth); height += lineHeight; } }//根據模式來決定FlowLayout的大小,如果不是EXACTLY,則說明布局檔案中設定的模式應該是wrap_content/*** 如果是wrap_content,則將FlowLayout寬度和高度設定為我們計算出來的最終寬高* 如果是fill_parent或者具體數值,則將FlowLayout寬度和高度設定為一開始getMode和getSize得到的那個寬高*/setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height); }
再次查看預覽視窗:
關於重寫系統控制項會在以後的博文中整合,希望本文能夠讓大家對自訂ViewGroup有所協助。
Android深入淺出自訂控制項(二)