View的工作原理
ViewRoot和DecorView
ViewRoot對應于ViewRootImpl,連接WindowManager和DecorView的紐帶。
View的繪制流程從ViewRoot的performTraversals方法開始,經過以下三個過程:
- measure
- layout
- draw
理解MeasureSpec
MeasureSpec是一個會影響到View測量過程的參數。在測量View的寬高的過程中,系統會將View的LayoutParams根據父View的規則轉換成對應的MeasureSpec,在進行寬高測量。
MeasureSpec
MeasureSpec是一個32位的int值,高兩位代表SpecMode,低30位代表SpecSize。即模式+尺寸。Android里將這兩個參數打包成了一個int值來避免過都的內存分配,可以通過get方法解包得到mode和size的分別值。源碼里主要是一些“位操作”,類似:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
如源碼里給出的,SpecMode有三類:
- UNSPECIFIED:父容器不對View有任何限制,一般系統內部使用。
- EXACTLY:父容器已經檢測出View的精確大小,由SpecSize指定。
- AT_MOST:父容器指定一個可用大小SpecSize,子View的大小不能超過這個值。
子View Mode會被父View的specMode所影響,在getChildMeasureSpec方法中,給出了這種影響的具體過程,其流程圖如下:
子View會根據父View的Spec不同模式,得到不同的結果。
從流程圖和表格可以總結出:
- View的MesureSpec由父View的MesureSpec和自身的LayoutParams共同決定;
- 若View指定了大小,則不管父View的MeasureSpec如何,其Spec將總是ECACTLY,而大小為其指定的大小;
- 子View的LayoutParams為Wrap_content時,無論父類為何種模式,子View總是AT_MOST。因此,對于自定義控件來說,當指定view為wrap_content時,需要指定自身的大小,否則子View會在AT_MOST的模式下,最大程度的利用父View的空間。
- getMeasureSpec方法返回的是一個打包后的MesureSpec,子View的Mode將由其前2位確定,而后30位事實上代表了父View的可用大小,子View將參考這一值,但并不是最終子View的大小(事實上,View的最終大小是在layout階段被確定的,但是一般情況下,View的測量大小和最終大小相等)。
View的工作流程
View的工作流程主要有:measure、layout、draw,即測量,布局和繪制。
View的measure過程
對于View來說,measure過程就是測量自身尺寸的過程;對于ViewGroup來說,measure過程除了測量自身尺寸外,還要遞歸的去測量所有children的尺寸。
View的measure過程比較簡單:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
基本上,所有的onMeasure都要干一件事:計算好自己的寬高,然后調用setMeasuredDimension方法保存。對于自定義的View,我們要自己計算width和height數值。這里就不貼getDefaultSize的代碼了,也比較簡單,就是根據SpecMode的值,來判斷應該使用什么樣的size。
ViewGroup的measure過程
ViewGroup的measure過程除了繪制自身外,還要繪制其children。ViewGroup本身是個抽象類,并沒有去實現View的onMeasure方法,其通過一個measureChildren的方法對所有的Children進行測量。
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對每個child進行測量:
測量過程本質上和View是一致的,外部傳入了需要測量的child視圖和父View的MeasureSpec,在調用View中getChildMeasureSpec方法創建MeasureSpec,而測量結果傳遞到View的measure方法中進行測量。接下去就是一個遞歸遍歷的過程。
由于ViewGroup本身是抽象類,沒有實現onMeasure方法,因此需要其具體的實現類,來完成這個方法。典型如LinearLayout、RelativeLayout等。事實上,每個ViewGroup的onMeasure方法考慮的東西很多,Android里LinearLayout源碼還比較長,值得一看,可以了解下具體的測量過程。
View 的Measure過程和Activity的生命周期方法并不同步,往往在onCreate方法中去獲取View的尺寸,得到的值并不是最終View的尺寸大小,為了在Activity啟動時獲取一個View的尺寸,有四種方法。
(1) Activity/View#onWindowsFocusChanged
當Activity窗口獲得焦點時會被調用,并且這個方法表示View已經初始化完畢。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2) view.post(Runnable)
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
}
});
}
(3) ViewTreeObserver
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@SuppressLint("NewApi")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int widt = view.getMeasuredHeight();
}
});
}
layout過程
layout是在Measure結束后的步驟,將用來確定子View的位置。對于ViewGroup來說,layout方法確定本身的位置,然后調用onlayout方法確定所有子view的位置。對于View而言,其layout過程如下:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
layout方法會先使用setFrame來設定View本身的四個頂點位置,在調用onLayout方法去測量子View的位置,而onlayout是一個抽象方法,對于一個view而言,將不會有什么作用,對于一個ViewGroup而言,將會去確定其中所有子view的位置;同樣的,在子view內,也會再調用layout方法確定自身和onlayout方法確定子子view,因此通過一層一層的傳遞,完成整個view樹的layout過程。
ViewGroup的一個實現類是LinearLayout,在LinearLayout中,會重寫onlayout方法,來完成自身和子View的布局位置確定:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
LinearLayout布局可以選擇橫向排列或者縱向排列內部的子View,兩者實現邏輯類似,看看layoutVertical(l,t,r,b)的一些代碼:
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
......
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
在layoutVertical中,通過Gravity的屬性,來判斷child的left、right、top等參數如何計算,這里省略貼代碼了。在對一個子view計算好四個坐標后,通過setChildFrame函數記錄。注意到在setChildFrame后,childTop會加上這個chil自身的高度,這就意味著下一個child的視圖位置一定會在當前child下面,實現垂直排列的效果。而在setChildFrame中,實際上也是調用了view的layout方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
draw過程
在Measure和layout之后,意味著每一個view在屏幕上的最終大小和位置都被確定了,這時候就通過draw過程將其繪制到屏幕上,其步驟:
- 繪制背景background.draw(canvas)
- 繪制自己(onDraw)
- 繪制Children(dispatchDraw)
- 繪制裝飾(onDrawScrollBars)
/* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */
View有一個特殊的方法setWillNotDraw,它表示如果一個View不需要繪制本身,可以把這個標志位設為true,以便于系統對其進行優化。顯然,一個普通的view一般不會去設置這個標志位,但是在某些ViewGroup中,可能本身并不需要經行繪制,那么可以通過這個方法設置從而優化性能。
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
自定義View
按照Android開發藝術探索里的分類,有如下四種情況:
- 繼承View重寫onDraw 方法
- 繼承ViewGroup派生
- 繼承已有的實體View,如TextView
- 繼承已有的實體ViewGroup,如LinearLayout
總結一下就是:自定義View,毫無疑問都需要直接或者間接繼承于View,然后根據具體需要實現的功能,來決定是利用現有的View來擴展,還是從底層開始重寫。繼承的層次越少,自定義空間就越大,同時難度也越高。因此,自定義View時,需要我們找到一種cost最小的方法去實現我們需要的功能。
自定義View時,一些注意事項:
- View需要去支持wrap_content $$ wrap_content對應的MeasureSpec是AT_MOST,如果View不對wrap_content進行處理,會最大限度的利用父view的空間
- View需要去處理padding 和margin $$ 從之前的三大過程來看,padding和margin是參與到了view的繪制計算中的,如果不處理,則這些屬性會無效
- View中盡量不使用Handler $$ 因為View本身提供了post方法來發送消息
- View中如果有線程或者動畫,需要及時停止 $$ 一般在onDetachedFromWindow中處理,否則可能造成內存泄露
- View如果帶有滑動嵌套,需要處理滑動沖突 $$ 有外部攔截發和內部攔截法
重寫onDraw方法
public class CircleView extends View {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth()-paddingLeft-paddingRight;
int height = getHeight()-paddingTop-paddingBottom;
int radus = Math.min(width,height)/2;
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radus,mPaint);
}
}
通過自定義了一個CircleView,重寫其onDraw方法,來實現畫圓。可以看到,onDraw方法中,我對padding屬性經行了處理,使得自定義View才能夠對xml文件里的padding屬性經行支持;另外,在onMeasure方法里,也對wrap_content的默認屬性進行了設置。為了使一個自定義View支持我們需要的自定義屬性,需要在values目錄下創建一個自定義屬性是xml文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
之后,在CircleView的構造函數里對屬性進行解析:
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
array.recycle();
init();
}
最后,在xml布局文件中,正常使用即可。需要注意的是,要對命名空間進行聲明,類似:
xmlns:app="http://schemas.android.com/apk/res-auto"