初識ViewRoot和DecorView
-
ViewRoot對應於ViewRootImpl類,,它是連接WindowManager和DecorView的紐帶,view的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯。
-
View的繪製流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程最終將一個view繪製出來。
- measure過程決定來View的寬/高,Measure以後,可通過getMeasureWidth和getMeasureHeight方法來獲取測量寬高。
- layout過程決定來View四個頂點的座標和事件的View的寬高,layout完成後可通過getTop、getLeft等來獲取四個頂點座標,getWidth和getHeight來獲取最終寬高。
- draw過程則決定來View的顯示,只有draw方法完成後,View的內容才能呈現到屏幕上。
- DecorView作爲頂層View,一般情況下內部會包含一個豎直方向的LinearLayout,這個LinearLayout裏面分上下兩部分(titlebar和content)。我們在Activity中setContentView設置的佈局就是駕到來id爲content的FrameLayout中。可以通過ViewGroup content = findViewById(R.id.content)得到content,通過content.getChildAt(0)的到我們設置的View。DecorView其實是一個FrameLayout,View層的事件都先經過DecorView,然後再傳給我們的View。
DecorView被加載到Window中的過程
- 從Activity的startActivity開始,最終調用到ActivityThread的handleLaunchActivity方法來創建Activity,首先,會調用performLaunchActivity方法,內部會執行Activity的onCreate方法,從而完成DecorView和Activity的創建。然後,會調用handleResumeActivity,裏面首先會調用performResumeActivity去執行Activity的onResume()方法,執行完後會得到一個ActivityClientRecord對象,然後通過r.window.getDecorView()的方式得到DecorView,然後會通過a.getWindowManager()得到WindowManager,最終調用其addView()方法將DecorView加進去。
- WindowManager的實現類是WindowManagerImpl,它內部會將addView的邏輯委託給WindowManagerGlobal,可見這裏使用了接口隔離和委託模式將實現和抽象充分解耦。在WindowManagerGlobal的addView()方法中不僅會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView通過root.setView()把DecorView加載到Window中。這裏的ViewRootImpl是ViewRoot的實現類,是連接WindowManager和DecorView的紐帶。View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯.
理解MeasureSpec
MeasureSpec
MeasureSpec代表一個32爲int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。
public static class MeasureSpec {
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;
//打包
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
//...
}
SpecMode有三類,每一類都有不同含義
- UNSPECIFIED
不確定模式,父容器不對View做任何限制,要多大給多大,這種模式一般用於系統內部,表示一種測量狀態。
- EXACTLY模式
精確模式,父容器以及檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種。
- AT_MOST模式
最大值模式,父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼值要看不同View的具體實現。它對應於LayoutParams中的wrap_content。
MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,爲了方便操作,其提供了打包和解包的方法,打包方法爲makeMeasureSpec,解包方法爲getMode和getSize。
MeasureSpec和LayoutParams的對應關係
在View測量的時候,系統會將LayoutParams在父容器約束下轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬高。MeasureSpec是由父容器的SpecMode和本身的LayoutParams共同決定的。
對於頂級view(DecorView)和普通view來說,MeasureSpec的轉換過程略有不同。對於DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同決定;對於普通的View,它的MeasureSpec由父視圖的MeasureSpec和其自身的LayoutParams共同決定。 MeasureSpec一旦確定,onMeasure中就可以確定View的測量寬高。
DecorView的MeasureSpec確定:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看到,是根據它的LayoutParams的參數來區分的:
- LayoutParams.MATCH_PARENT:精確模式,大小是窗口的大小
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但不能超過窗口大小。
- 固定大小:精確模式,大小爲LayoutParams中指定的大小。
普通View的MeasureSpec確定
對於普通View,View的Measure過程由ViewGroup傳遞而來,先看一下ViewGroup的measureChildWithMargins方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述方法會對子元素進行measure,在調用子元素的measure方法前,會先通過getChildMeasureSpec來得到子元素的MeasureSpec。ViewGroup的getChildMeasureSpec方法如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding); //子 view剩餘最大空間
int resultSize = 0;
int resultMode = 0;
switch (specMode) { //parent的specMode
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) { //子view的LayoutParams是具體值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述方法主要作用是根據父容器的MeasureSpec同時結合View本身的LayoutParams來確定子元素的MeasureSpec。
普通View的MeasureSpec的創建規則如下:
可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,即可以快速確定出子元素的MeasureSpec了,有了MeasureSpec就可以進一步確定出子元素測量後的大小了。
View的工作流程
View的工作流程主要是指measure、layout、draw這三大流程,其中measure確定View的測量寬高,layout確定View的最終寬高和四個頂點的位置(即確定view的位置),draw將View繪製到屏幕上。
View的繪製流程之measure
measure的過程要分情況來看,如果只是一個原始的View,那麼通過measure方法就完成其測量,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,然後各個子View再遞歸這個過程。
- View的measure過程
View的measure過程由其measure方法來完成,它是一個final方法,意味子類不能重寫。在measure方法中會調用onMeasure方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//...
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法會去設置View寬高的測量值,我們只要看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;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
我們只要看.AT_MOST和EXACTLY模式,可以簡單理解,其實getDefaultSize返回的大小就是measureSpec的specSize。
對於UNSPECIFIED的情況,一般用於系統內部的測量過程。這種情況getDefaultSize第一個參數size由getSuggestedMinimumWidth()返回
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
可以看到,如果View沒有設置背景,那麼返回android:minWidth這個屬性所指定的值,這個值可以爲0;如果View設置了背景,則返回android:minWidth和背景的最小寬度這兩者中的最大值。
mBackground.getMinimumWidth()返回的就是Drawable的原始寬度,前提是這個Drawable有原始寬度,否則返回0.
從getDefaultSize方法的實現來看,View的寬高由specSize決定,所以,直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於match_parent。原因,結合代碼和MeasureSpec的確定規則知,如果View在佈局中使用wrap_content,那麼它的SpecMode是AT_MOST,在這種模式下,它的寬高等於specSize也就是parentSize,效果和match_parent是一樣的。解決辦法就是給View指定一個默認寬高,並在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 heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) { //wrap_content時的默認寬高
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
- ViewGroup的measure過程
對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷調用所有子元素的measure方法。和View不同的是,ViewGroup是一個抽象類,沒有重寫View的onMeasure方法,但提供來一個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);
}
}
}
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中的measureChildren()方法中會遍歷測量ViewGroup中所有的View,當View的可見性處於GONE狀態時,不對其進行測量。
- 然後,測量某個指定的View時,根據父容器的MeasureSpec和子View的LayoutParams等信息計算子View的MeasureSpec。
- 最後,將計算出的MeasureSpec傳入View的measure方法
這裏ViewGroup沒有定義測量的具體過程,因爲ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類(比如LinearLayout)去實現。不同的ViewGroup子類有不同的佈局特性,這導致它們的測量細節各不相同,如果需要自定義測量過程,則子類可以重寫這個方法。(setMeasureDimension方法用於設置View的測量寬高,如果View沒有重寫onMeasure方法,則會默認調用getDefaultSize來獲得View的寬高)
LinearLayout的onMeasure方法實現解析(只看Vertical模式)
系統會遍歷子元素並對每個子元素執行measureChildBeforeLayout方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,並且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每測量一個子元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的margin等。
在Activity啓動時獲取某個View的測量寬高
由於View的measure過程和Activity的生命週期方法不是同步執行的,如果View還沒有測量完畢,那麼獲得的寬/高就是0。所以在onCreate、onStart、onResume中均無法正確得到某個View的寬高信息。解決方式如下:
- Activity/View#onWindowFocusChanged:此時View已經初始化完畢,當Activity的窗口得到焦點和失去焦點時均會被調用一次,如果頻繁地進行onResume和onPause,那麼onWindowFocusChanged也會被頻繁地調用。
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
- view.post(runnable): 通過post可以將一個runnable投遞到消息隊列的尾部,始化好了然後等待Looper調用次runnable的時候,View也已經初始化好了。
protected void onStart(){
super.onStart();
view.post(new Runnable(){
@Overrie
public void run(){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
- ViewTreeObserver#addOnGlobalLayoutListener:當View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout方法將被回調。
protected void onStart(){
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
- View.measure(int widthMeasureSpec, int heightMeasureSpec):match_parent時不知道parentSize的大小,測不出;具體數值時,直接makeMeasureSpec固定值,然後調用view..measure就可以了;wrap_content時,在最大化模式下,用View理論上能支持的最大值去構造MeasureSpec是合理的。
int widthMSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMSpec, heightMSpec);
View的繪製流程之Layout
layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它在onLayout中會遍歷所有子元素並調用其layout方法,在layout方法中onLayout方法又會被調用。layout方法確定View本身的位置,而onLayout方法則會確定所有子元素的位置。
先看ViewGroup的
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b); //調用View的layout方法
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
/**
* {@inheritDoc}
*/
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b); //抽象方法,子類必須自己實現佈局
可以看到ViewGroup的layout方法實際還是調用View的layout方法,而onLayout則是一個空的抽象方法,子類自己實現。
在看View的layout方法:
@SuppressWarnings({"unchecked"})
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); //會調setFrame方法
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b); //onLayout方法
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;
}
//onLayout也是一個空方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
layout方法大致流程:首先會通過setFrame方法來設定View的四個頂點的位置,即View在父容器中的位置。然後,會執行到onLayout空方法,這個方法的用途是父容器確定子元素的位置。(即先確定自己本身位置,再遍歷確定子元素位置)子類如果是ViewGroup類型,則重寫這個方法,實現ViewGroup中所有View控件佈局流程。
LinearLayout的onLayout方法實現(Vertical方向)
其中會遍歷調用每個子View的setChildFrame方法爲子元素確定對應的位置。其中的childTop會逐漸增大,意味着後面的子元素會被放置在靠下的位置。而setChildFrame方法只是調用子view的layout方法而已。這樣父元素在layout方法中完成自己的定位,並通過onLayout方法去調用子元素的layout方法,子元素又通過自己的layout方法確定自己的位置,這樣一層一層傳遞完成整個View樹的layout。
==問題==:View的測量寬高和最終寬高有什麼區別?
在View的默認實現中,View的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成於View的measure過程,而最終寬/高形成於View的layout過程,即兩者的賦值時機不同,測量寬/高的賦值時機稍微早一些。 在一些特殊的情況下則兩者不相等:
- 比如重寫View的layout方法,使最終寬度總是比測量寬/高大100px。
- View需要多次measure才能確定自己的測量寬/高,在前幾次測量的過程中,其得出的測量寬/高有可能和最終寬/高不一致,但最終來說,測量寬/高還是和最終寬/高相同。
View的繪製流程之Draw
繪製基本上可以分爲六個步驟:
- 首先繪製View的背景(canvas);
- 如果需要的話,保持canvas的圖層,爲padding做準備;
- 然後,繪製View的內容(onDraw);
- 接着,繪製View的子View(dispatchDraw);
- 如果需要的話,繪製View的padding邊緣並恢復圖層;
- 最後,繪製View的裝飾(onDrawScrollBars例如滾動條等等)。
public void draw(Canvas canvas) {
if (mClipBounds != null) {
canvas.clipRect(mClipBounds);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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)
*/
//...
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas); //繪製子View
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
//...
protected void dispatchDraw(Canvas canvas) {
//View中是空的,由子 View實現,比如ViewGroup中由自己的實現
}
View繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層層傳遞來下去。
View中setWillNotDraw的作用
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
如果一個View不需要繪製任何內容,那麼設置WILL_NOT_DRAW這個標記位爲true以後,系統會進行相應的優化。
- 默認情況下,View沒有啓用這個優化標記位,但是ViewGroup會默認啓用這個優化標記位。
- 當我們的自定義控件繼承於ViewGroup並且本身不具備繪製功能時,就可以開啓這個標記位從而便於系統進行後續的優化。
- 當明確知道一個ViewGroup需要通過onDraw來繪製內容時,我們需要顯示地關閉WILL_NOT_DRAW這個標記位。
自定義View
自定義View分類
- 繼承View重寫onDraw
主要用於實現一些不規則的效果。採用這種方式需要自己在onMeasure支持wrap_content,並且padding也需要在onDraw自己處理。
- 繼承ViewGroup派生特殊layout
用於實現自己定義的新佈局。需要合適地處理ViewGroup的測量、佈局兩個過程,同時處理子元素的測量和佈局過程。
- 繼承特定的View(比如TextView)
用於擴展某種已有的View的功能。這種方法可以不需要自己支持wrap_content和padding。
- 繼承特定的ViewGroup(比如LinearLayout)
用於擴展某種佈局。不需要自己處理ViewGroup的測量和佈局這兩個過程。
自定義View須知
- 讓View支持wrap_content
直接繼承View或ViewGroup的控件,如果沒在onMeasure中對wrap_content做處理,那麼wrap_content會無效。
- 如果有必要,支持padding
直接繼承View的控件,如果不再onDraw中處理padding,那麼padding屬性無法起作用。直接繼承自ViewGroup的控件需要在onMeasure和onLayout中考慮自身padding和子元素margin對其造成的影響,不如會無效。
- 儘量不要在View中使用Handler,沒必要
View內部本身提供來post系列方法,完成可以替代Handler
- View中如果又線程或動畫,要及時停止,參考View#onDetachedFromWindow
當包含此view的Activity退出或當前View被remove時,View的onDetachedFromWindow方法會被調用,在這個方法停止線程或動畫是很好的時機。
- View帶嵌套,需處理好滑動衝突
View有嵌套情形,需要自己處理好滑動衝突(外部攔截法或內部攔截法)
自定義View示例
1. 繼承View重寫onMeasure和onDraw方法
這種方式需要自己在onMeasure處理wrap_content和在onDraw處理padding。
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//處理自定義屬性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); //CircleView_circle_color是屬性集_屬性使用_連接起來的固定格式
a.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
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);
//支持wrap_content,默認值200
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//處理padding
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 radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
radius, mPaint);
}
}
添加自定義屬性步驟:
- 在values目錄下創建自定義屬性XML文件attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView"> //屬性集合
<attr name="circle_color" format="color" /> //具體屬性
</declare-styleable>
</resources>
- 在View的構造方法解析自定義屬性並做處理
- 在佈局文件中使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" //使用自定義屬性必須聲明,app是自定義的名稱,可改
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<com.ryg.chapter_4.ui.CircleView
android:id="@+id/circleView1"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/light_green" /> //“app”和上面定義的一致即可
</LinearLayout>
2. 繼承ViewGroup派生layout
需要合適處理ViewGroup的測量、佈局以及子元素的測量佈局過程,這裏只看onMeasure和onLayout方法,完整可參看github
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
這裏兩點不規範,一是沒有子元素時不應該把寬高設爲0,而應該根據LayoutParams來處理;第二點在測量時沒有考慮它的padding和子元素的margin。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
//遍歷子View,調用子View的layout方法
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
同樣沒有考慮它的padding和子元素的margin佔用
Requestlayout,onlayout,onDraw,DrawChild區別與聯繫?
- requestLayout()方法 :會導致調用 measure()過程 和layout()過程,將會根據標誌位判斷是否需要ondraw。
- onLayout()方法:如果該View是ViewGroup對象,需要實現該方法,對每個子視圖進行佈局。
- onDraw()方法:繪製視圖本身 (每個View都需要重載該方法,ViewGroup一般不需要實現該方法)。
- drawChild():去重新回調每個子視圖的draw()方法。
invalidate() 和 postInvalidate()的區別 ?
- invalidate()與postInvalidate()都用於刷新View,主要區別是invalidate()在主線程中調用,若在子線程中使用需要配合handler;而postInvalidate()可在子線程中直接調用。
參考書籍《Android開發藝術探究》第4章