第12章 CustomView封裝控件

一、自定義屬性與自定義Style

概述:

在一個自 定義控件的XML中經常會發現類似下面的代碼 :

<com.trydeclarestyle.MyTextView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    attrstest:headerHeight="300dp"
    attrstest:headerVisibleHeight="100dp"
    attrstest:age="young" />

注意到最後三個屬性,明顯不是系統自帶的,而是人爲添加上去的。怎麼添加自定義的屬性呢?利用XML中的declare-styleable標籤來實現。

declare-styleable標籤的使用方法:

1.自定義一個類MyTextView

public class MyTextView extends TextView {
    public MyTextView(Context context) {
        super(context);
    }
}

2.新建res/values/attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<declare-styleable name="MyTextView">
		<attr name="header" format="reference"/>
		<attr name="headerHeight" format="dimension"/>
		<attr name="headerVisibleHeight" format="dimension"/>
		<attr name="age">
			<flag name="child" value="10"/>
			<flag name="young" value="18"/>
			<flag name="old" value="60"/>
		</attr>
	</declare-styleable>
</resources>

(1)最重要的一點是,declare-styleable旁邊有一個name屬性,這個屬性的取值對應所定義的類名。也就是說,要爲哪個類添加自定義的屬性,那麼這個name屬性的值就是哪個類的類名。這裏爲自定義的MyTextView類添加XML屬性,所以name="MyTextView"。
(2)自定義屬性值可以組合使用。比如<attr name="border_color" format="color|reference"/>表示既可以自定義color值(比如#ff00ff),也可以利用@color/XXX來引用color.xml中已有的值。

這裏先看一下 declare-styleable 標籤中所涉及的標籤的用法。

• reference 指的是從string.xml、drawable.xml、color.xml等文件中引用過來的值。
• flag 是自己定義的,類似於android:gravity="top"。
• dimension 指的是從dimension.xml文件中引用過來的值。注意,這裏如果是dp,就會進行像素轉換。

使用方法如下 :

<com.harvic.com.trydeclarestyle.MyTextView
	android:layout_width="fill_parent"
	android:layout_height="match_parent"
	attrstest:header="@drawable/pic1"
	attrstest:headerHeight="300dp"
	attrstest:headerVisibleHeight="1OOdp"
	attrstest:age="young"/>

可以看到,header 的取值是從其他XML文件中引用過來的;dimension表示尺寸,直接輸入數字;flag相當於代碼裏的常量,比如這裏的young就表示數字18。

在XML中使用自定義的屬性:

1.添加自定義控件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:attrstest="http://schemas.android.com/apk/res/com.harvic.com.trydeclarestyle"
	android:layout_width="match parent"
	android:layout_height="match parent">
	<com.harvic.com.trydeclarestyle.MyTextView
		android:layout_width="fill_parent"
		android:layout_height="match_parent"
		attrstest:header="@drawable/picl"
		attrstest:headerHeight="300dp"
		attrstest:headerVisibleHeight="100dp"
		attrstest:age="young"/>
</RelativeLayout>

2.導入自定義的屬性集(方法一)

要讓XML識別我們自定義的屬性也非常簡單,在根佈局上添加如下代碼即可。

xmlns:attrstest="http://schemas.android.com/apk/res/com.harvic.com.trydeclarestyle"

這裏有兩點需要注意。
(1)xmlns:attrstest:這裏的attrstest是自定義的,你想定義成什麼就可以定義成什麼。但要注意的是,在訪問你定義的XML控件屬性時,就是通過這個標識符訪問的。比如,這裏定義成attrstest,那麼對應的訪問自定義控件的方式就是attrstest:headerHeight="300dp"。
(2)com.harvic.com.trydeclarestyle:它是AndroidManifest.xml中的包名,即AndroidManifest.xml中package字段對應的值。

3.導入自定義的屬性集(方法二)

另一種自動導入自定義屬性集的方式要相對簡單,只需在根佈局上添加如下代碼即可 。

xmlns:attrstest="http://schemas.android.com/apk/res-auto"

在代碼中獲取自定義屬性的值:

使用代碼獲取用戶所定義的某個屬性的值,主要使用TypedArray類,這個類提供了獲取某個屬性值的所有方法,如下所示。需要注意的是,在使用完以後必須調用 TypedArray類的recycle()函數來釋放資源。

typedArray.getInt(int index, float defValue);
typedArray.getDimension(int index, float defValue);
typedArray.getBoolean(int index, float defValue);
typedArray.getColor(int index, float defValue);
typedArray.getString(int index);
typedArray.getDrawable(int index);
typedArray.getResources();
public class MyTextView extends TextView {
    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        float headerHeight = typedArray.getDimenSion(R.styleable.MyTextView_headerHeight, -1);
        int age = typedArray.getInt(R.styleable.MyTextView_age, -1);
        typedArray.recycle();// a must!!!
        this.setText("headerHeight:" + headerHeight + " age:" + age);
    }
}

declare-styleable標籤其他屬性的用法:

1.reference:參考某一資源ID

屬性定義:
<declare-styleable name ="名稱">
	<attr name="background" format ="reference"/>
</declare-styleable>

屬性使用 :
<ImageView
	android:layout_width="42dip"
	android:layout_height="42dip"
	android:background="@drawable/圖片ID"/>

2.color:顏色值

屬性定義:
<declare-styleable name="名稱">
	<attr name="textColor" format="color"/>
</declare-styleable>

屬性使用:
<TextView
	android:layout_width="42dip"
	android:layout_height="42dip"
	android:textColor="#00FF00"/>

3.boolean:布爾值

屬性定義:
<declare-styleable name="名稱">
	<attr name="focusable" format="boolean"/>
</declare-styleable>

屬性使用:
<Button
	android:layout_width="42dip"
	android:layout_height="42dip"
	android:focusable="true"/>

4.dimension:尺寸值

屬性定義:
<declare-styleable name="名稱">
	<attr name="layout_width" format="dimension"/>
</declare-styleable>

屬性使用:
<Button
	android:layout_width="42dip"
	android:layout_height="42dip"/>

5.float:浮點值

屬性定義:
<declare-styleable name="AlphaAnimation">
	<attr name="fromAlpha" format="float"/>
	<attr name="toAlpha" format="float"/>
</declare-styleable>

屬性使用:
<alpha
	android:fromAlpha="l.0"
	android:toAlpha="0.7"/>

6.integer:整型值

屬性定義 :
<declare-styleable name="AnimatedRotateDrawable">
	<attr name="visible"/>
	<attr name="frameDuration" format="integer"/>
	<attr name="framesCount" format="integer"/>
	<attr name="pivotX"/>
	<attr name="pivotY"/>
	<attr name="drawable"/>
</declare-styleable>

屬性使用 :
<animated-rotate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:drawable="@drawable/圖片ID"
	android:pivotX="50%"
	android:pivotY="50%"
	android:framesCount ="12"
	android:frameDuration ="100"/>

7.string:字符串

屬性定義 :
<declare-styleable name="MapView">
	<attr name="apiKey" format="string"/>
</declare-styleable>

屬性使用 :
<com.google.android.maps.MapView
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:apiKey="OjOkQ80oDlJL9C6HAja99uGXCRiS2CGjKO_bc_g"/>

8.fraction:百分數

屬性定義 :
<declare-styleable name="RotateDrawable">
	<attr name="visible"/>
	<attr name="fromDegrees" format="float"/>
	<attr name="toDegrees" format="float"/>
	<attr name="pivotX" format="fraction"/>
	<attr name="pivotY" format="fraction" />
	<attr name="drawable"/>
</declare-styleable>

屬性使用 :
<rotate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:interpolator="@anim/動畫ID"
	android:fromDegrees="0"
	android:toDegrees="360"
	android:pivotX="200%"
	android:pivotY="300%"
	android:duration="5000"
	android:repeatMode="restart"
	android:repeatCount="infinite"/>

9.enum:枚舉值

屬性定義 :
<declare-styleable name="名稱">
	<attr name="orientation">
	    <enum name="horizontal" value="0"/>
	    <enum name="vertical" value="l"/>
    </attr>
</declare-styleable>

屬性使用:
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent" />

10.flag:位或運算

屬性定義:
<declare-styleable name="名稱">
	<attr name="windowSoftlnputMode">
		<flag nane="stateUnspecified" value="0"/>
		<flag name="stateUnchanged" value="l"/>
		<flag name="stateHidden" value="2"/>
		<flag name="stateAlwaysHidden" value="3"/>
		<flag name="stateVisible" value="4"/>
		<flag name="stateAlwaysVisible" value="5"/>
		<flag name="adjustUnspecified" value="0x00"/>
		<flag name="adjustResize" value="0xl0"/>
		<flag name="adjustPan" value="0x20"/>
		<flag name="adjustNothing" value="0x30"/>
	</attr>
</declare-styleable>

屬性使用 :
<activity
	android:name=".StyleAndThemeActivity"
	android:label="@string/app name "
	android:windowSoftlnputMode="stateUnspecified | stateUnchanged | stateHidden">
	<intent-filter>
		<action android:name="android.intent.action.MAIN"/>
		<category android:name="android.intent.category.LAUNCHER"/>
	</intent-filter>
</activity>

二、測量與佈局

ViewGroup繪製流程:

注意:View及ViewGroup基本相同,只是在ViewGroup中不僅要繪製自己,還要繪製其中的子控件,而View只需妥繪製自己就可以了,所以這裏就以ViewGroup爲例來講述整個繪製流程。

繪製流程分爲三步:測量、佈局、繪製,分別對應onMeasure()、onLayout()、onDraw()函數。

• onMeasure():測量當前控件的大小,爲正式佈局提供建議(注意:只是建議,至於用不用,要看onLayout()函數)。
• onLayout():使用layout()函數對所有子控件進行佈局。
• onDraw():根據佈局的位置繪圖 。

onMeasure()函數與MeasureSpec:

佈局繪畫涉及兩個過程:測量過程和佈局過程。測量過程通過measure()函數來實現,是View樹自頂向下的遍歷,每個View在循環過程中將尺寸細節往下傳遞,當測量過程完成之後,所有的View都存儲了自己的尺寸。佈局過程則通過layout()函數來實現,也是自頂向下的,在這個過程中,每個父View負責通過計算好的尺寸放置它的子View。

前面提到,onMeasure()函數是用來測量當前控件大小的,給onLayout()函數提供數值參考。需要特別注意的是,測量完成以後,要通過setMeasuredDimension(int,int)函數設置給系統。

1.onMeasure()函數

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
● 它們是指父類傳遞過來給當前View的一個建議值,即想把當前View的尺寸設置爲寬widthMeasureSpec、高heightMeasureSpec。

2.MeasureSpec的組成

雖然從表面上看起來它們是int類型的數字,但它們是由mode+size兩部分組成的。
widthMeasureSpec和heightMeasureSpec轉換爲二進制數字表示,它們都是32位的,前2位代表模式(mode),後面30位代表數值(size)。

1)模式分類

它有三種模式。

(1) UNSPECIFIED(未指定):父元素不對子元素施加任何束縛,子元素可以得到任意想要的大小。
(2) EXACTLY(完全):父元素決定子元素的確切大小,子元素將被限定在給定的邊界裏而忽略它本身的大小。
(3) AT MOST(至多):子元素至多達到指定大小的值。

它們對應的二進制值分別是:
UNSPECIFIED = 00000000000000000000000000000000
        EXACTLY = 01000000000000000000000000000000
       AT_MOST = 10000000000000000000000000000000
由於前2位代表模式,所以它們分別對應十進制的0、l、2。

2)模式提取

如果我們需要自己來提取widthMeasureSpc和heightMeasureSpec中的模式和數值該怎麼辦呢?

首先想到的肯定是通過 MASK 和與運算去掉不需要的部分,從而得到對應的模式或數值。
 

// 對應 11000000000000000000000000000000;共32位,前2位是1
int MODE_MASK = 0xc0000000;
// 提取模式
public static int getMode(int measureSpec) {
	return (measureSpec & MODE_MASK);
}
// 提取數值
public static int getSize(int measureSpec) {
	return (measureSpec & ~MODE_MASK);
}

從這裏可以看出,模式和數值的提取主要用到了 MASK 的與、非運算。
3)MeasureSpec

Android 己經爲我們提供了 MeasureSpec 類來輔助實現這個功能。

MeasureSpec.getMode(int spec) // 獲取模式
MeasureSpec.getSize(int spec) // 獲取數值

模式的取值爲:MeasureSpec.AT_MOST、MeasureSpec.EXACTLY、MeasureSpec.UNSPECIFIED

通過下面的代碼就可以分別獲取widthMeasureSpec和heightMeasureSpec的模式和數值了。

int measureWidth = MeasureSpec.getSize(widthMeasureSpec) ;
int measureHeight = MeasureSpec.getSize(heightMeasureSpec) ;
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec) ;
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec) ;

4)模式的用處

需要注意的是,widthMasureSpec和heightMeasureSpec各自都有對應的模式,而模式分別來自XML定義。
簡單來說,XML佈局和模式有如下的對應關係:
• wrap_content → MeasureSpec.AT_MOST
• match_parent → MeasureSpec.EXACTLY
• 具體值 → MeasureSpec.EXACTLY

<com.example.harvic.myapplication.FlowLayout
	android:layout width="match_parent"
	android:layout_height="wrap_content">
</com.example.harvic.myapplication.FlowLayout>

在上述代碼中,FlowLayout在onMeasure()函數中傳值時,widthMeasureSpec的模式是MeasureSpec.EXACTLY,即父窗口寬度值;heightMeasureSpec的模式是MeasureSpec.ATMOST,即值不確定。

一定要注意的是,當模式是MeasureSpec.EXACTLY時,就不必設定我們計算的數值了,因爲這個大小是用戶指定的,我們不應更改。但當模式是MeasureSpec.AT_MOST時,也就是說用戶將佈局設置成了wrap_content,就需要將大小設定爲我們計算的數值,因爲用戶根本沒有設置具體值是多少,需要我們自己計算。

也就是說,假如width和height是我們經過計算的控件所佔的寬度和高度,那麼在onMeasure()函數中使用setMeasuredDimension()函數進行設置時,代碼應該是這樣的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
	int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
	int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
	int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
	// 經過計算,控件所佔的寬和高分別對應 width 和 height
	// 計算過程暫時省略
	...
	setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY)? 
                         measureWidth:width, 
                         (measureHeightMode ==MeasureSpec.EXACTLY)?measureHeight:height);
}

onLayout()函數:

1.概述:

onLayout()是實現所有子控件佈局 的函數。注意,是所有子控件!

ViewGroup的onLayout()函數的默認行爲是什麼?在 ViewGroup.java中的源碼如下:

@Override
protected abstract void onLayout(boolean changed, int l, int t , int r , int b);

這是一個抽象函數,說明凡是派生自ViewGroup的類都必須自己去實現這個函數。像LinearLayout、RelativeLayout等佈局都重寫了這個函數,然後在內部按照各自的規則對子視圖進行佈局。

2.示例

(1)三個TextView豎直排列;(2)背景的Layout寬度是match_parent,高度是wrap_content。

1)XML佈局

<com.harvic.simplelayout.MyLinLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:background="#ff00ff"
	tools:context=".MainActivity">
	<TextView 
		android:text="第一個VIEW"
		android:layout_width="wrap_content"
		android:layout_height ="wrap_content"
		android:background="#ff0000"/>
	<TextView 
		android:text="第二個VIEW"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:background="#00ff00"/>
	<TextView 
		android:text="第三個VIEW"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:background="#0000ff"/>
</com.harvic.simplelayout.MyLinLayout>

2)MyLinLayout實現:重寫onMeasure()函數

我們提到過,onMeasure()函數的作用就是根據container內部的子控件計算自己的寬和高,然後通過setMeasuredDimension(int width,int height)函數設置進去。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    int height = 0;
    int width = 0;
    int count = getChildCount();
    for (int i=0; i<count; i++) {
        // 測量子控件
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        // 獲得子控件的高度和寬度
        int childHeight = child.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();
        // 得到最大寬度,並且累加高度
        height += childHeight;
        width = Math.max(childWidth, width);
    }
    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY)? measureWidth:width, 
                         (measureHeightMode ==MeasureSpec.EXACTLY)? measureHeight:height);
}

這裏的measureWidthMode應該是MeasureSpec.EXACTLY,measureHeightMode應該是MeasureSpec.AT_MOST。在最後利用setMeasuredDimension(width,height)函數來進行設置時,width使用的是從父類傳過來的measureWidth,而高度則是我們自己計算的height。即實際的運算結果是這樣的:setMeasureDimension(measureWidth, height);

總體來講,onMeasure()函數中計算出來的width和height就是當XML佈局設置爲layout_width="wrap_content",layout_height="wrap_content"時所佔的寬和高,即整個container所佔的最小矩形。

3)MyLinLayout實現:重寫onLayout()函數

在這一部分就是根據自己的意願把 container 內部的各個控件排列起來,在這裏要實現的是將所有的控件垂直排列。

protected void onLayout(boolean changed, int 1 , int t, int r, int b) {
    int top = 0;
    int count = getChildCount();
    for (int i=0; i<count; i++) {
        View child = getChildAt(i);
        int childHeight = child.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();
        child.layout(0, top, childWidth, top + childHeight);
        top += childHeight;
    }
}

4)getMeasuredWidth()與getWidth()函數

通過這個例子,講解一個很容易出錯的問題:getMeasuredWidth()與getWidth()函數的區別。它們的值大部分時候是相同的,但含義卻是根本不一樣的,下面來簡單分析一下。

二者的區別主要體現在下面兩點:
● getMeasureWidth()函數在measure()過程結束後就可以獲取到寬度值;而getWidth()函數要在layout()過程結束後才能獲取到寬度值。
● getMeasureWidth()函數中的值是通過setMeasuredDimension()函數來進行設置的;而getWidth()函數中的值則是通過layout(left,top,right,bottom)函數來進行設置的。

前面講過,setMeasuredDimension()函數提供的測量結果只是爲佈局提供建議的,最終的取用與否要看layout()函數。所以看這裏重寫的MyLinLayout,是不是我們自己使用child.layout(left,top,right,bottom)函數來定義了各個子控件所在的位置?

int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
child.layout(0, top, childWidth, top + childHeight);

從代碼中可以看到,我們使用child.layout(0,top,childWidth,top+childHeight);來佈局控件的位置,其中getWidth()函數的取值就是這裏的右座標減去左座標的寬度。因爲我們這裏的寬度直接使用的是child.getMeasuredWidth()函數的返回值,當然會導致getMeasuredWidth()與getWidth()函數的返回值是一樣的。如果我們在調用layout()函數的時候傳入的寬度值不與getMeasuredWidth()函數的返回值相同,那麼getMeasuredWidth()與getWidth()函數的返回值就不再一樣了。

3.疑問:container自己什麼時候被佈局

前面我們說了,在派生自ViewGroup的container中,比如MyLinLayout,在onLayout()函數中佈局它所有的子控件。那它自己什麼時候被佈局呢?

它當然也有父控件,它的佈局也是在父控件中由它的父控件完成的,就這樣一層一層地向上由各自的父控件完成對自己的佈局,直到所有控件的頂層節點。在所有控件的頂部有一個ViewRoot,它纔是所有控件的祖先節點。讓我們來看看它是怎麼做的吧。

在它的佈局裏,會調用自己的一個layout()函數(不能被重載,代碼位於View.Java)。

/* final標識符,不能被重載,參數爲每個視圖位於父視圖的座標軸
* @param 1 Left position,relative to parent
* @param t Top position,relative to parent
* @param r Right position,relative to parent
* @param b Bottom position,relative to parent
*/
public final void layout(int 1, int t, int r, int b) {
	boolean changed = setFrame(l, t, r, b);// 設置每個視圖位於父視圖的座標軸
	if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
		if (ViewDebug.TRACE_HIERARCHY) {
			ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
		}
		onLayout(changed, l, t, r, b);// 回調onLayout()函數,設置每個子視圖的佈局
		mPrivateFlags &= ~LAYOUT_REQUIRED;
	}
	mPrivateFlags &= ~FORCE_LAYOUT;
}

在setFrame(l,t,r,b)函數中設置的是自己的位置,設置結束以後纔會調用onLayout(changed,1,t,r,b)函數來設置內部所有子控件的位置。
到這裏,有關onMeasure()和onLayout()函數的內容就結束了。

獲取子控件margin值的方法:

1.獲取方法及示例

如果要自定義ViewGroup支持子控件的layout_margin參數,則自定義的ViewGroup類必須重寫generateLayoutParams()函數,並且在該函數中返回一個ViewGroup.MarginLayoutParams派生類對象。我們在上面MyLinLayout例子的基礎上,添加layout_margin參數。

1)在XML中添加layout_margin參數

<com.harvic.simplelayout.MyLinLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:background="#ff00ff"
	tools:context=".MainActivity">
	<TextView 
		android:text="第一個VIEW"
		android:layout_width="wrap_content"
		android:layout_height ="wrap_content"
		android:layout_marginTop="10dp"
		android:background="#ff0000"/>
	<TextView 
		android:text="第二個VIEW"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:layout_marginTop="20dp"
		android:background="#00ff00"/>
	<TextView 
		android:text="第三個VIEW"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:layout_marginTop="30dp"
		android:background="#0000ff"/>
</com.harvic.simplelayout.MyLinLayout>

運行後效果同上例的是一樣的,設置的margin根本沒起作用!這是爲什麼呢?因爲測量和佈局都是我們自己實現的,我們在onLayout()函數中沒有根據margin來佈局,當然不會出現有關margin的效果。需要特別注意的是,如果我們在onLayout()函數中根據margin來佈局,那麼在onMeasure()函數中計算container的大小時,也要加上layout_margin參數,否則會導致container太小而控件顯示不全的問題。

2)重寫generateLayoutParams()和generateDefaultLayoutParams()函數

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
    return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

3)重寫onMeasure()函數

protected void onMeasure(int widthMeasureSpec , int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    int height = 0;
    int width = 0;
    int count = getChildCount();
    for (int i=0; i<count; i++) {
        // 測量子控件
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        
        // 獲得子控件的高度和寬度 + margin值
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;

        // 得到最大寬度,並且累加高度
        height += childHeight;
        width = Math.max(childWidth, width);
    }
    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY)? measureWidth:width, 
                         (measureHeightMode ==MeasureSpec.EXACTLY)? measureHeight:height);
}

4)重寫onLayout()函數

protected void onLayout(boolean changed, int 1 , int t, int r, int b) {
    int top = 0;
    int count = getChildCount();
    for (int i=0; i<count; i++) {
        View child = getChildAt(i);

        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;

        child.layout(0, top, childWidth, top + childHeight);
        top += childHeight;
    }
}

2.原理

只有重寫generateLayoutParams()函數才能獲取到控件的margin值。那爲什麼要重寫呢?下面這句又爲什麼非要強轉呢?

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

首先,在container中初始化子控件時,會調用LayoutParams generateLayoutParams(LayoutParams p)函數來爲子控件生成對應的佈局屬性,但默認只生成layout_width和layout_height所對應的佈局參數,即在正常情況下調用generateLayoutParams()函數生成的LayoutParams實例是不能獲取到margin值的。即:

/**
* 從指定的XML中獲取對應的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
/*
* 如果要使用默認的構造函數,就生成 layout_width="wrap content"、layout_height="wrap content"對應的參數
*/
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

所以,如果我們還需要與 margin 相關的參數,就只能重寫 generateLayoutParams()函數。

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

由於generateLayoutParams()函數的返回值是LayoutParams實例,而MarginLayoutParams是派生自LayoutParams的,所以,根據類的多態特性,可以直接將此時的LayoutParams實例強轉成MarginLayoutParams實例。
所以下面這句在這裏是不會報錯的:

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

爲了安全起見,也可以利用 instanceof 來進行判斷。
 

MarginLayoutParams lp = null;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
    lp = (MarginLayoutParams) child.getLayoutParams();
}

3.generateLayoutParams()與MarginLayoutParams()函數的實現

1)generateLayoutParams()函數的實現

先來看generateLayoutPararms()函數是如何得到佈局值的。(位於ViewGroup.java中)

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a, R.styleable.ViewGroup_Layout_layout_width, R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}

可以看出,generateLayoutParams()函數調用LayoutParams()函數產生布局信息,而LayoutParams()函數最終調用setBaseAttributes()函數來獲得對應的寬、高屬性。

上述代碼是通過TypedArray對自定義的XML進行值提取的過程。從中也可以看到,調用generateLayoutParams()函數所生成的LayoutParams屬性只有layout_width和layout_height屬性值。

2)MarginLayoutParams()函數的實現

public MarginLayoutParams(Context c, AttributeSet attrs) {
    super();
    TypedArray a= c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
    int margin = a.getDimensionPixelSize(com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
    if (margin >= 0) {
        leftMargin = margin;
        topMargin = margin;
        rightMargin= margin;
        bottomMargin = margin;
    } else {
        leftMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN);
        rightMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN);
        topMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED);
        startMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE);
        endMargin = a.getDimensionPixelSize(R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE);
    }
    a.recycle();
}

這段代碼分爲兩部分:第一部分是if語句部分,主要作用是提取layout_margin的值並進行設置;第二部分是else語句部分,如果用戶沒有設置layout_margin,而是單個設置的,就一個個提取。這段代碼就是對layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值逐個提取的過程。

從這裏大家也可以看到爲什麼非要重寫generateLayoutParams()函數了,就是因爲默認的generateLayoutParams()函數只會提取layout_width和layout_height的值,只有MarginLayoutParams()函數才具有提取margin值的功能。

三、實現FlowLayout容器

XML佈局:

先定義一個style標籤,這是爲FlowLayout中的TextView定義的。

<style name="text_flag_01">
	<item name="android:layout_width">wrap_content</item>
	<item name="android:layout_height">wrap_content</item>
	<item name="android:layout_margin">4dp</item>
	<item name="android:background">@drawable/flag_01</item>
	<item name="android:textColor">#ffffff</item>
</style>

activity_main.xml佈局文件:

<LinearLayout 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"
    tools:context=".MainActivity">
    <com.example.harvic.myapplication.FlowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="Welcome"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_01"
            android:background="@drawable/flag_03"
            android:text="IT 工程師"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="我真是可以的"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable /flag_03"
            android:text="你覺得呢"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="不要只知道掙錢"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="努力 ing"
            android:textColor="@android:color/white" />
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="I thick i can"
            android:textColor="@android:color/white"/>
    </com.example.harvic.myapplication.FlowLayout>
</LinearLayout>

提取margin值與重寫onMeasure()函數:

1.提取margin值

我們講過,要提取margin值,就一定要重寫 generateLayoutParams()函數。代碼如下:

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
    return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

2.重寫onMeasure()函數——計算當前FlowLayout所佔的區域大小

1)何時換行
FlowLayout的佈局是一行行的,如果當前行己經放不下下一個控件了,就把這個控件移到下一行顯示。所以需要一個變量來計算當前行己經佔據的寬度,以判斷剩下的空間是否還能容得下下一個控件。
2)如何得到FlowLayout的寬度
FlowLayout的寬度是所有行寬度的最大值,所以我們要記錄每一行所佔據的寬度值,進而找到所有值中的最大值。
3)如何得到FlowLayout的高度
很顯然,FlowLayout的高度是每一行高度的總和,而每一行的高度則取該行中所有控件高度的最大值。

(1)利用MeasureSpec獲取系統建議的數值和模式。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    ...    
}

(2)計算FlowLayout所佔用的區域大小。

// 先申請幾個變量
int lineWidth = 0;// 記錄每一行的寬度
int lineHeight = 0;// 記錄每一行的高度
int height = 0;// 記錄整個FlowLayout所佔高度
int width = 0;// 記錄整個FlowLayout所佔寬度

int count= getChildCount();
for (int i=0; i<count; i++) {
    View child = getChildAt(i);
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    int childWidth = child.getMeasuredWidth () + lp.leftMargin + lp.rightMargin;
    int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    if (lineWidth + childWidth > measureWidth) {// 需要換行
        width= Math.max(lineWidth, childWidth);
        height += lineHeight;
        // 因爲當前行放不下當前控件,而將此控件調到下一行,所以將此控件的高度和寬度初始化給lineHeight、lineWidth
        lineHeight = childHeight;
        lineWidth = childWidth;
    } else {// 否則累加值lineWidth;lineHeight取最大高度
        lineHeight = Math.max(lineHeight, childHeight);
        lineWidth += childWidth;
    }
    if (i == count -1) {// 因爲最後一行是不會超出width範圍的,所以需要單獨處理
        height += lineHeight;
        width = Math.max(width,lineWidth);
    }
}

這裏一定要注意的是,在i用用child.getMeasuredWidth()、child.getMeasuredHeight()函數之前,一定要先調用measureChiId(chiId,widthMeasureSpec,heightMeasureSpec); 我們講過,在調用onMeasure()函數之後才能調用getMeasuredWidth()函數獲取值;同樣,只有在調用onLayout()函數後,getWidth()函數才能獲取值。

下面就要判斷當前控件是否換行及計算出最大高度和寬度了。

(3)通過setMeasuredDimension()函數設置到系統中。

setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY)? measureWidth:width, 
                      (measureHeightMode == MeasureSpec.EXACTLY)? measureHeight:height);

3.重寫onLayout函數——佈局所有子控件

在onLayout()函數中需要一個個佈局子控件。由於控件要後移和換行,所以我們要標記當前控件的top座標和left座標。

protected void onLayout(boolean changed, int 1, int t, int r, int b) {
    // 先申請下面幾個變量
    int count = getChildCount();
    int lineWidth = 0;// 累加當前行的行寬
    int lineHeight = 0;// 當前行的行高
    int top = 0, left = 0;// 當前控件的top座標和left座標

    for (int i=0; i<count; i++) {
        View child = getChildAt(i);
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        int childHeight = child.getMeasuredHeight() +lp.topMargin + lp .bottomMargin;
        if (childWidth + lineWidth > getMeasuredWidth()) {// 如果換行
            top += lineHeight;
            left = 0;
            lineHeight = childHeight;
            lineWidth = childWidth;
        } else {
            lineHeight = Math .max(lineHeight , childHeight};
            lineWidth += childWidth;
        }
        // 計算 childView 的 left , top , right , bottom
        int le = left + lp.leftMargin;
        int tc = top + lp.topMargin;
        int re = le + child.getMeasuredWidth();
        int be = tc + child.getMeasuredHeight();
        child.layout(le, te, re, be);
        // 將left置爲下一個子控件的起始點
        left += childWidth;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章