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);//擷取内容欄裡面的子元素

|
二、初識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的建立規則如下表
(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并無差別
解決方法:其實說到底就是當為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是一個比較複雜的事,這裡隻是粗略的記錄下一些注意點.