歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Android開發實踐:自定義ViewGroup的onLayout()分析

Android開發實踐:自定義ViewGroup的onLayout()分析

日期:2017/3/1 9:36:36   编辑:Linux編程

前一篇文章主要講了自定義View為什麼要重載onMeasure()方法(見 http://www.linuxidc.com/Linux/2014-12/110164.htm),那麼,自定義ViewGroup又都有哪些方法需要重載或者實現呢 ?

Android開發中,對於自定義View,分為兩種,一種是自定義控件(繼承View類),另一種是自定義布局容器(繼承ViewGroup)。如果是自定義控件,則一般需要重載兩個方法,一個是onMeasure(),用來測量控件尺寸,另一個是onDraw(),用來繪制控件的UI。而自定義布局容器,則一般需要實現/重載三個方法,一個是onMeasure(),也是用來測量尺寸;一個是onLayout(),用來布局子控件;還有一個是dispatchDraw(),用來繪制UI。

本文主要分析自定義ViewGroup的onLayout()方法的實現。

ViewGroup類的onLayout()函數是abstract型,繼承者必須實現,由於ViewGroup的定位就是一個容器,用來盛放子控件的,所以就必須定義要以什麼的方式來盛放,比如LinearLayout就是以橫向或者縱向順序存放,而RelativeLayout則以相對位置來擺放子控件,同樣,我們的自定義ViewGroup也必須給出我們期望的布局方式,而這個定義就通過onLayout()函數來實現。

我們通過實現一個水平優先布局的視圖容器來更加深入地了解onLayout()的實現吧,效果如圖所示(黑色方塊為子控件,白色部分為自定義布局容器)。該容器的布局方式是,首先水平方向上擺放子控件,水平方向放不下了,則另起一行繼續水平擺放。

1. 自定義ViewGroup的派生類

第一步,則是自定ViewGroup的派生類,繼承默認的構造函數。

public class CustomViewGroup extends ViewGroup {

public CustomViewGroup(Context context) {
super(context);
}

public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}

public CustomViewGroup(Context context, AttributeSet attrs, intdefStyle) {
super(context, attrs, defStyle);
}
}

2. 重載onMeasure()方法

為什麼要重載onMeasure()方法這裡就不贅述了,上一篇文章已經講過,這裡需要注意的是,自定義ViewGroup的onMeasure()方法中,除了計算自身的尺寸外,還需要調用measureChildren()函數來計算子控件的尺寸。

onMeasure()的定義不是本文的討論重點,因此這裡我直接使用默認的onMeasure()定義,當然measureChildren()是必須得加的。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

3. 實現onLayout()方法
onLayout()函數的原型如下:

//@param changed 該參數指出當前ViewGroup的尺寸或者位置是否發生了改變
//@param left top right bottom 當前ViewGroup相對於其父控件的坐標位置
protected void onLayout(boolean changed,int left, int top, int right, int bottom);

由於我們希望優先橫向布局子控件,那麼,首先,我們知道總寬度是多少,這個值可以通過getMeasuredWidth()來得到,當然子控件的寬度也可以通過子控件對象的getMeasuredWidth()來得到。

這樣,就不復雜了,具體的實現代碼如下所示:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

int mViewGroupWidth = getMeasuredWidth(); //當前ViewGroup的總寬度

int mPainterPosX = left; //當前繪圖光標橫坐標位置
int mPainterPosY = top; //當前繪圖光標縱坐標位置

int childCount = getChildCount();
for ( int i = 0; i < childCount; i++ ) {

View childView = getChildAt(i);

int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();

//如果剩余的空間不夠,則移到下一行開始位置
if( mPainterPosX + width > mViewGroupWidth ) {
mPainterPosX = left;
mPainterPosY += height;
}

//執行ChildView的繪制
childView.layout(mPainterPosX,mPainterPosY,mPainterPosX+width, mPainterPosY+height);

//記錄當前已經繪制到的橫坐標位置
mPainterPosX += width;
}
}

4. 布局文件測試

下面我們就嘗試寫一個簡單的xml文件,來測試一下我們的自定義ViewGroup,我們把子View的背景顏色都設置為黑色,方便我們辨識。

<com.titcktick.customview.CustomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/black"/>

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/black"/>

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/black"/>

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/black"/>

</com.titcktick.customview.CustomViewGroup>

5. 添加layout_margin

為了讓核心邏輯更加清晰,上面的onLayout()實現我隱去了margin的計算,這樣就會導致子控件的layout_margin不起效果,所以上述效果是子控件一個個緊挨著排列,中間沒有空隙。那麼,下面我們來研究下如何添加margin效果。

其實,如果要自定義ViewGroup支持子控件的layout_margin參數,則自定義的ViewGroup類必須重載generateLayoutParams()函數,並且在該函數中返回一個ViewGroup.MarginLayoutParams派生類對象,這樣才能使用margin參數。

ViewGroup.MarginLayoutParams的定義關鍵部分如下,它記錄了子控件的layout_margin值:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
}

你可以跟蹤源碼看看,其實XML文件中View的layout_xxx參數都是被傳遞到了各種自定義ViewGroup.LayoutParams派生類對象中。例如LinearLayout的LayoutParams定義的關鍵部分如下:

public class LinearLayout extends ViewGroup {

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

public float weight;
public int gravity = -1;

public LayoutParams(Context c, AttributeSet attrs) {

super(c, attrs);

TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

a.recycle();
}
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LinearLayout.LayoutParams(getContext(), attrs);
}
}

這樣你大概就可以理解為什麼LinearLayout的子控件支持weight和gravity的設置了吧,當然我們也可以這樣自定義一些屬於我們ViewGroup特有的params,這裡就不詳細討論了,我們只繼承MarginLayoutParams來獲取子控件的margin值。

public class CustomViewGroup extends ViewGroup {

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomViewGroup.LayoutParams(getContext(), attrs);
}

}

這樣修改之後,我們就可以在onLayout()函數中獲取子控件的layout_margin值了,添加了layout_margin的onLayout()函數實現如下所示:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

int mViewGroupWidth = getMeasuredWidth(); //當前ViewGroup的總寬度
int mViewGroupHeight = getMeasuredHeight(); //當前ViewGroup的總高度

int mPainterPosX = left; //當前繪圖光標橫坐標位置
int mPainterPosY = top; //當前繪圖光標縱坐標位置

int childCount = getChildCount();
for ( int i = 0; i < childCount; i++ ) {

View childView = getChildAt(i);

int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();

CustomViewGroup.LayoutParams margins = (CustomViewGroup.LayoutParams)(childView.getLayoutParams());

//ChildView占用的width = width+leftMargin+rightMargin
//ChildView占用的height = height+topMargin+bottomMargin
//如果剩余的空間不夠,則移到下一行開始位置
if( mPainterPosX + width + margins.leftMargin + margins.rightMargin > mViewGroupWidth ) {
mPainterPosX = left;
mPainterPosY += height + margins.topMargin + margins.bottomMargin;
}

//執行ChildView的繪制
childView.layout(mPainterPosX+margins.leftMargin, mPainterPosY+margins.topMargin,mPainterPosX+margins.leftMargin+width, mPainterPosY+margins.topMargin+height);

mPainterPosX += width + margins.leftMargin + margins.rightMargin;
}
}

6. 總結

費了好大勁,終於算是把自定義ViewGroup的onLayout()相關知識點講清楚了,如果有任何疑問歡迎留言或者來信[email protected]交流,如果喜歡本文歡迎轉載,但希望能尊重我的勞動成果,給出本文文鏈接,謝謝。

最簡單的Ubuntu Touch & Android 雙系統安裝方式 http://www.linuxidc.com/Linux/2014-01/94881.htm

在Nexus上實現Ubuntu和Android 4.4.2 雙啟動 http://www.linuxidc.com/Linux/2014-05/101849.htm

Ubuntu 14.04 配置 Android SDK 開發環境 http://www.linuxidc.com/Linux/2014-05/101039.htm

64位Ubuntu 11.10下Android開發環境的搭建(JDK+Eclipse+ADT+Android SDK詳細) http://www.linuxidc.com/Linux/2013-06/85303.htm

Ubuntu 14.04 x64配置Android 4.4 kitkat編譯環境的方法 http://www.linuxidc.com/Linux/2014-04/101148.htm

Ubuntu 12.10 x64 安裝 Android SDK http://www.linuxidc.com/Linux/2013-03/82005.htm

更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11

Copyright © Linux教程網 All Rights Reserved