天天看點

View的工作原理小結

View的工作原理

一、初識ViewRoot和DecorView

ViewRoot對應于ViewRootImpl類,為連接配接WindowManager和DecorView的紐帶。首先看看ViewRootImpl是什麼時候被調用的。

1、啟動Acitivity 最終會調用 ActivityThread#handleResumeActivity(),這裡面會執行窗體添加 DecorView 的操作

( ViewManager wm = a.getWindowManager();
    wm.addView(decor, l);//這是窗體添加 DecorView)
           

2、接下來可以看第一點裡面的addView方法

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
           

可見在addView方法中又調用了mGlobal的addView方法,mGlobal為WindowManagerGlobal 的執行個體。

3、接下來可看mGolbal的addView方法

root = new ViewRootImpl(view.getContext,display);
try {
            root.setView(view, wparams, panelParentView);//由它去設定view
        } catch (RuntimeException e) {}
通過
           

上述方法我們可得知,addView最後還是調用ViewRootImpl來實作将DecorView 添加到Window上。

DecorView:作為頂級View,一般情況下它内部會包含一個豎直方向的LinearLayout,上面是标題欄,下面是内容欄。在Activity中我們通過setContentView所設定的布局檔案其實就是被加到内容欄之中的ViewGroup

content = findViewById(R.android.id.content)//擷取内容欄
content.getChildAt(0);//擷取内容欄裡面的子元素
           
View的工作原理小結

|

二、初識MeasureSpec:

MeasureSpec很大程度上決定了一個View的尺寸規格,其本身為一個32為的int值,高2位為SpecMode(測量模式),低30為SpecSize(某種測量模式下的規格大小)。

其中SpecMode主要有三種:

一、UNSPECIFIED:父容器不對view有任何限制,表一種測量狀态,應用于系統内部。

二、EXACTLY:精确大小,對應LayoutParamas中的match_parent和具體數值兩種形式。

三、AT_MOST:父容器指定一個可用大小,對應于LayoutParams的wrap_content。

MeasureSpec的産生:系統将LayoutParams在父容器的限制下轉換成對應的MeasureSpec。

(1)普通view的MeasureSpec:由父容器的MeasureSpec和自身的LayoutParams來共同決定,此外還和View的margin及padding有關。

普通view的MeasureSpec的建立規則如下表

View的工作原理小結

(2)DecorView的MeasureSpec:由視窗的尺寸和其自身的LayoutParams來共同決定,其MeasureSpec在ViewRootImpl中的measureHierarchy方法中決定。

三、View的工作流程(measure、layout、draw)

1、measure:

(1)view的measure:

view的measure方法是final類型的方法,子類不能去重寫,在measure中調用了onMeasure方法,是以隻需要關注onMeasure的實作就行。

流程: view —》measure() —》onMeasure —》setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasure),getDefaultSize(getSuggestedMinimumHeight(),HeightMeasure)){設定view寬高的測量值)}—》 getDefaultSize()

以下代碼為getDefaultSize方法的實作:

public static int getDefaultSize(int size,int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
    result = size;//這個size為getSuggestedMinimumWidth()傳入的值,下面有這個方法
                   //具體實作的代碼
    break;
    case MeasureSpec.AT_MOST://AT_MOST和.EXACTLY這兩種情況。簡單地了解,其實
    case MeasureSpec.EXACTLY://getDefaultSize傳回的大小就是measureSpec中的
    result = specSize;       //的specSize,而這個specSize就是View測量後的大小
    break;
    }
    return result
           

以下代碼為getSuggestedMinimumWidth()方法的實作(這個方法主要針對UNSPECIFIED模式):

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight,mBackground.getMinimumHeight
}
           

根據上述方法我們可得知View沒有設定背景,那麼View的寬度為mMinWidth,而mMinWidth對android:minWidth這個屬性所指定的值。

注意:通過getDefaultSize這個方法的實作可得知,如果直接繼承View的自定義控件沒有設定wrap_content的自身大小,那麼在布局中使用wrap_content和使用match_parent并無差別

View的工作原理小結

解決方法:其實說到底就是當為AT_MOST,也就是wrap_content,需要給其指定一個預設的寬高(mWidth、mHeight)。

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth,mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth,heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,mHeight);
}
}
           

(2)ViewGroup的measure過程:沒有重寫View的onMeasure方法,提供了一個measureChildren方法。

measureChildren方法的具體實作:

protected void measureChildren(int widthMeasureSpec,int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
        }
    }
}
           

可見最後又調用measureChild方法。

measureChild的具體實作:

protected void measureChild(View child,int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
    mPaddingLeft + mPaddingRight,lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    mPaddingTop + mPaddingBottom,lp.height);
    child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
           

ViewGroup是抽象類,其不對onMeasure方法進行統一實作的原因是,不同的ViewGroup子類有不同的布局特性,比如(LinearLayout和RelativeLayout)。

上述已經總結完View的大緻測量流程。

接下來記錄下一些注意點:

tip:什麼時候去擷取一個view的寬高(因為view的measure和Activity的生命周期方法不是同步執行的)?

解決辦法:

1、在onWindowFocusChanged中去擷取,這個時候可保證view已經繪制完畢,Activity視窗得到焦點和失去焦點都會被調用一次。

2、view.post(runnable),将runnable放到消息隊列的尾部,Looper調用此runnable,view也初始化了。

3、viewTreeObserver,其中許多回調可以擷取view的寬高,當View樹的狀态以及可見性改變時會回調onGlobalLayout方法。

2、layout(即有測量、必有布局):

(1)View的layout:

首先看下view的layout方法(僞代碼):

public void layout(int l,int t,int r,int b){
    //初始化mLeft、mright、mTop、mBottom四個值
    //頂點确定,位置也就确定了
    //setFrame()
}
           

(2)ViewGroup的layout:

View和ViewGroup均沒有實作onLayout方法,因為其的具體實作也是和布局特性有關的。

流程:父元素的layout(完成自己的定位) --》 父元素的onLayout --》setChildFrame(為子元素指定對應的位置,其實就是調用子元素的layout方法而已) --》 子元素的layout --》 setFrame;

3、Draw:

繪制過程:繪制背景 – 》繪制本身 --》繪制children(dispatchDraw)–》繪制裝飾;

四、自定義View

1、自定義view的四種類型:

(1)直接繼承View注意點:

需要重寫onDraw方法,自己支援wrap_content、padding也需要自己處理。

(2)直接繼承viewgroup注意點:

處理好measure和layout方法,同時需要考慮padding和margin的影響,不然本身padding和子元素的margin會失效

(3)繼承特定的view(例如button)

(4) 繼承特定的LinearLayout(例如LinearLayout)

2、自定義屬性

步驟:

(1)values檔案夾下建立attrs.xml<? xml version="1.0" encoding="utf-8"?>

<resource>
    <declare-styleble name="CircleView">
        <attr name="circle_color" format="color" />
     </decalare-styleable>
<resource>        
           

(2) 代碼中擷取此屬性//view的構造方法裡面

TypeArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
mColor = a.getColor(styleable.CircleView_circle_color,Color.RED);
a.recyle();
           

(3)使用自定義屬性,需要在布局檔案中聲明:xmlns:

app=http://schemas.android.com/apk/res-auto

自定義view是一個比較複雜的事,這裡隻是粗略的記錄下一些注意點.