自定義view流程(結合原始碼分析)
一、View的繪製流程
主要是:測量(measure)、佈局(layout)、繪製(draw)三大流程。
- 對於一個普通View(不是容器)
主要是關心測量和繪製兩個過程,測量可以確定自身的寬、高、大小,繪製可以顯示出view的具體內容(呈現在螢幕上的)。 - 對於ViewGroup(容器控制元件)主要是關心測量和佈局兩個過程,測量不僅僅要測量自身還要測量所有的子view,佈局主要是指定所有子view在自身上的位置。
- 具體實現是重寫onMeasure、onLayout、onDraw方法,在這些方法中進行編碼處理。
二、View的測量
-
View的測量主要是由自身的MeasureSpec決定的,而自身的MeasureSpec又由父容器的MeasureSpec和自身的LayoutParams決定的。
-
MeasureSpec包含SpecMode和SpecSize兩部分,SpecMode是測量規則,SpecSize是在一定規則下的測量大小。
-
SpecMode分為三種模式
a、UNAPECIFIED:父容器對view沒有任何限制,要多大給多大,該模式多為系統自己使用,自定義一般不考慮該模式。
b、EXACTLY:父容器已經知道view所需要的精確大小(SpecSize)。
c、AT_MOST:父容器給了一個最大值(SpecSize)。當父容器的SpecMode為EXACTLY時:
如果view的LayoutParams為match_parent和具體的值,父容器會為其指定為EXACTLY模式。
如果view的LayoutParams為wrap_content,父容器會為其指定為AT_MOST模式。
當父容器的SpecMode為AT_MOST時:
如果view的LayoutParams為具體的數值,父容器會為其指定為EXACTLY模式。
如果view的LayoutParams為match_parent,父容器會為其指定為AT_MOST模式。
如果view的LayoutParams為wrap_content,父容器會為其指定為AT_MOST模式。父view在測量子view之前會先呼叫getChildMeasureSpec方法確定子view的MeasureSpec,下面看getChildMeasureSpec的原始碼:
/** * 確定子view的MeasureSpec的具體方法 */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //父view自己的模式和大小 int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); //父view的大小減去padding值,就是現在子view可用的空間大小 int size = Math.max(0, specSize - padding); //對於viewGroup來說,resultSize和resultMode除了用於確定自身大小外,還要傳給下級子view //這個變數存的是最終子view測量大小 int resultSize = 0; //這個變數存的是最終子view的測量模式,如果這個子view是viewGroup的話,那麼這個測量模式是要給子view的下級子view使用的,就這樣一層一層的遞迴 int resultMode = 0; //根據specMode確定子view的MeasureSpec switch (specMode) { case MeasureSpec.EXACTLY://父容器的模式是EXACTLY,說明父容器的大小是精確值(父容器的大小已經確定了) if (childDimension >= 0) { //子view的LayoutParams的值是具體的值,比如100dp,那麼子view的大小就用這個100dp,子view的模式也是精確值模式 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子view的LayoutParams的值是MATCH_PARENT,父view是精確值,所以子view的大小就是父view的可用大小(也是精確值),子view的模式也是精確值模式 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子view的LayoutParams的值是WRAP_CONTENT,父view是精確值,但是子view自己的大小是不確定的(最大為父view的可用size),所以子view的模式是最大模 式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: //父容器的模式是AT_MOST,說明父容器的大小是不確定的(父容器的大小是一個最大值,這個最大值是父容器的上層父容器給的) if (childDimension >= 0) { //子view的LayoutParams的值是具體的值,比如100dp,那麼子view的大小就用這個100dp,子view的模式也是精確值模式 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子view的LayoutParams的值是MATCH_PARENT,那麼子view的大小就是父view的可用大小,而父view的模式是AT_MOST,說明父view的大小是不確定的,所以子view的大小也是不確定的,子view的模式是AT_MOST模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子view的LayoutParams的值是WRAP_CONTENT,父view的大小不確定,子view自身的大小也不確定,所以子view的模式是AT_MOST模式 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.UNSPECIFIED: //這種模式一般是系統自己用的,自定義控制元件一般不考慮這種情況 if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //最終根據resultSize和resultMode生成子view的MeasureSpec return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
綜上所述:如果View的SpecMode為EXACTLY時,其大小是確定的,不需要做特殊處理。如果View的SpecMode為AT_MOST時,其大小是不確定的,所以在測量時需要視情況設定一個預設值,否則wrap_content是無效的、不顯示內容的。
- 示例程式碼實現如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
//獲取父容器為其指定的測量模式和測量尺寸
int widthSpecMode =MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize =MeasureSpec.getSize(widthMeasureSpec);
int hightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hightSpecSize =MeasureSpec.getSize(heightMeasureSpec);
//寬或者高的SpecMode是AT_MOST時就設定一個預設值,如果不是就用SpecSize
if (widthSpecMode ==MeasureSpec.AT_MOST && hightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(500,500);
} else if (widthSpecMode ==MeasureSpec.AT_MOST) {
setMeasuredDimension(500,hightSpecSize);
} else {
setMeasuredDimension(widthSpecSize,500);
}
}
三、View的繪製
- View的繪製是通過重寫onDraw方法實現的,可以在onDraw裡使用canvas、paint繪製圖形來實現自定義效果。
- 需要注意的是在繪製的時候需要考慮padding的影響,如果不做處理padding會無效,因為padding是跟view本身有關的。不用關心margin,因為margin是跟父容器相關的,跟view自身無關。
- 為了可以方便的在xml中改變效果,還需要對外提供自定義屬性。
四、ViewGroup的測量和佈局
- ViewGroup的onMeasure方法中,既要測量自身的大小,又要測量子View的大小,測量子View的大小可以使用measureChildren測量所有子View,也可以自己寫for迴圈遍歷測量子View,呼叫的方法是measureChild和measureChildWithMargins(這兩個方法是測量單個子View的)。下面對這些方法的原始碼進行分析:
measureChildren的原始碼:
//測量所有子view
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//迴圈測量子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//執行測量子view的方法
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild的原始碼:
//具體測量一個子view的方法
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
//獲取到子view的引數LayoutParams,後面會用
final LayoutParams lp = child.getLayoutParams();
//根據LayoutParams裡面設定的寬的match_parent或者wrap_content(即lp.width),在結合父view的MeasureSpec來確定子view的寬的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//根據LayoutParams裡面設定的高的match_parent或者wrap_content(即lp.height),在結合父view的MeasureSpec來確定子view的高的MeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//上面的幾行程式碼是確定子view的MeasureSpec的,這行程式碼是真正進行子view測量的,將上面確定下來的子view的MeasureSpec傳給子view。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
- onMeasure示例程式碼實現如下:
/**
* 模擬水平方向可滑動的LinearLayout的測量過程,這裡不考慮padding和margin的影響
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取父容器為其指定的測量模式和測量尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int hightMode = MeasureSpec.getMode(heightMeasureSpec);
int hightSize = MeasureSpec.getSize(heightMeasureSpec);
//測量所有子view的寬和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
//測量自身的寬和高
int measureWidth = 0;
int measureHight = 0;
int childCount = getChildCount();
if (childCount != 0) {
//計算出由子view決定的寬度
for (int i = 0; i < childCount;i++) {
measureWidth +=getChildAt(i).getMeasuredWidth();
}
//計算出由子view決定的高度(選取子view中高度最大值為其測量高度)
for (int j = 0; j < childCount;j++) {
if(getChildAt(j).getMeasuredHeight() > measureHight) {
measureHight =getChildAt(j).getMeasuredHeight();
}
}
}
if (widthMode == MeasureSpec.AT_MOST && hightMode ==MeasureSpec.AT_MOST) {
setMeasuredDimension(measureWidth,measureHight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(measureWidth,hightSize);
} else if (hightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize,measureHight);
}
}
- 容器控制元件的佈局,主要是指定每一個子view在自身上的位置,重寫onLayout方法,程式碼實現如下:
/**
* 對子view進行佈局,這裡不考慮padding和margin的影響
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right,int bottom) {
//每個子view的左起點
int childLeft = 0;
//子view的個數
int childCount = getChildCount();
//為每個子view指定位置
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
childView.layout(childLeft, 0,childLeft + childView.getMeasuredWidth(), childView.getMeasuredHeight());
childLeft +=childView.getMeasuredWidth();
}
}
五、在現有控制元件的基礎上進行自定義
上面所說的自定義控制元件,都是直接繼承View或者ViewGroup的,實際開發中有很多需求是不需要重頭自己定義一個控制元件的,可以繼承一個現有控制元件,去重寫其特定的某一個方法來擴充套件功能。具體用哪種方式去實現,要具體情況具體分析了,如何選取一種最適合的自定義方式,是值得思考的,也是一個難點。