一、自定義屬性與自定義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;
}
}