View的工作原理
文章目錄
1.1 ViewRoot和DecorView簡介
1.1.1 ViewRoot相關
View的三大流程:1.View的測量流程;2.佈局流程;3.繪製流程。
ViewRoot對應ViewRootImpl類,是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot完成的。
在ActivityThread中,當Activity對象創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯:
root = new ViewRootImpl(view.getContext(),display); root.setView(view,wparams,panelParentView);
View的繪製流程是從ViewRoot的performTraversals方法開始的,經過measure、layout和draw三個流程纔將View繪製完成。measure用來測量View的寬和高;layout用來確認View在父容器中的放置位置;draw用來將View繪製在屏幕上。
performTraversals會依次調用 performMeasure、performLayout、performDraw三個方法,這三個方法分別完成頂級View的measure、layout、draw三大流程。其中在performMeasure中會調用measure方法,measure方法又會調用onMeasure方法,在onMeasure方法中會對所有子元素進行measure過程,這樣measure流程就從父容器流轉到子元素中了。子元素重複父容器過程,反覆直到整個View樹的遍歷。performLayout和performDraw的傳遞流程和performMeasure類似,不同的是:performDraw的傳遞過程是在draw方法中通過dispatchDraw實現的,本質上無差異。
measure過程決定了View的寬/高。measure完成後,可以通過getMeasuredWidth和getMeasuredHeight獲取View測量後的寬/高。正常情況下measure過程的View的寬/高就是實際的View的寬/高。
layout過程決定了View的四個頂點的座標和實際的View的寬/高。layout完成後,可以通過getTop、getBottom、getLeft、getRight獲取View的四個頂點的位置,且可以通過getWidth、getHeight獲取View的最終寬/高。
draw過程決定了View的顯示。draw完成後,View的內容呈現在屏幕上。
1.1.2 DecorView相關
DecorView作爲頂級View,一般情況下內部包含一個豎直方向的LinearLayout,LinearLayout中有上下兩部分,標題欄和內容欄(具體與Android版本主題有關)。
在Activity中通過setContentView設置的佈局文件就是被加到內容欄中,而內容欄的id是content,所有Activity指定佈局的方法叫做setContentView而非setView。
獲取content的方法:
ViewGroup content = findViewById(R.android.id.content)
。獲取View的方法:
content.getChildAt(0)
。DecorView實際上是一個FrameLayout,View層的事件都是經過DecorView後傳遞給View的。
1.2 理解MeasureSpec
- MeasureSpec在很大程度上決定一個View的尺寸規格。
- 一個View的實際尺寸規格,還會受父容器影響。即父容器會影響View的MeasureSpec的創建過程。
- 系統會將View的LayoutParams根據父容器施加的規則轉換成對應的MeasureSpec,再根據這個MeasureSpec來測量出View的寬/高。
1.2.1 MeasureSpec
MeasureSpec代表一個32位的int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode指測量模式;SpecSize指某種測量模式下的規格大小。
MeasureSpec內部的一些常量定義:
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(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } ...
MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,同時提供了打包解包的方法。一組SpecMode和SpecSize可以打包成一個MeasureSpec,一個MeasureSpec也可以拆成原始的SpecMode和SpecSize。(MeasureSpec這裏指MeasureSpec代表的int,而非對象本身)
SpecMode三種類型:
- 1.UNSPECIFIED:父容器不對View有任何限制,要多大給多大,該情況一般用於系統內部,表示一種測量狀態。
- 2.EXACTLY:父容器已經檢測出View所需要的精確大小,這時View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種模式。
- 3.AT_MOST:父容器指定一個可用大小即SpecSize,View的大小不能大於這個值,具體的值還要看View的具體實現。它對應於LayoutParams中的wrap_content。
1.2.2 MeasureSpec和LayoutParams的對應關係
系統內部是通過MeasureSpec來進行View的測量。正常情況下我們使用View指定的MeasureSpec,但我們也可以給View設置LayoutParams。
在View測量的時候,系統會將LayoutParams在父容器的約束下轉換成MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬/高。
注意:MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而決定View的寬/高。另外,對於頂級View(即DecorView)和普通View而言,MeasureSpec的轉換過程是不同的:
- DecorView:其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定。
- 普通View:其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定。
MeasureSpec一旦確定,onMeasure中就可以確定View的測量寬/高。
ViewRootImpl中的measureHierarchy方法中的一段代碼,展示了DecorView的MeasureSpec的創建過程:
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中desiredWindowHeight和childWidthMeasureSpec是屏幕尺寸。
再看getRootMeasureSpec方法的實現:
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; }
上述代碼可見DecorView的MeasureSpec的產生過程。其遵守如下規則(根據LayoutParams中的寬/高的參數來劃分):
- LayoutParams.MATCH_PARENT:精確模式,大小就是窗口大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小。
- 固定大小(比如100dp):精確模式,大小爲LayoutParams中指定的大小。
對於普通佈局中的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,可見子元素的MeasureSpec的創建與父容器的MeasureSpec和子元素自身的LayoutParams有關,此外還和View的margin及paddig有關。
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); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { 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 = 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; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
該方法是根據父容器的MeasureSpec和View自身的LayoutParams確定子元素的MeasureSpec,參數中的padding是指父容器中已佔用的空間大小,因此子元素可以的大小爲父容器的尺寸減去padding:
int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding);
普通View的MeasureSpec的創建規則:
EXACTLY AT_MOST UNSPECIFIED dp/px EXACTLY(childSize) EXACTLY(childSize) EXACTLY(childSize) match_parent EXACTLY(parentSize) AT_MOST(parentSize) UNSPECIFIED(0) wrap_parent AT_MOST(parentSize) AT_MOST(parentSize) UNSPECIFIED(0) 注:1.橫排爲childparentSpecmode,縱排爲LayoutParams;2.parentSize指父容器中目前可使用的大小。
只要提供了父容器的MeasureSpec和子元素的LayoutParams,就可以快速確定出子元素的MeasureSpec。有了MeasureSpec就可以進一步確定出子元素測量後的大小。
1.3 View的工作流程
1.3.1 measure過程
measure過程要分兩種情況:
- 原始的View:通過measure方法完成測量過程。
- ViewGroup:完成自己的測量過程外,遍歷去調用所有子元素的measure方法。
View的measure過程:
- View的measure過程由其measure方法完成。measure方法是fianl類型,在其方法內會調用View的onMeasure方法。onMeasure的方法實現:
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; }
可見getDefaultSize返回的大小是measureSpec中的specSize,這個specSize就是View測量後的大小。對於UNSPECIFIED情況,View的大小爲getDefaultSize第一個參數size,即寬/高爲getSuggestedMinimumWidth和getSuggestedMinimumHeight方法的返回值:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
從getSuggestedMinimumWidth代碼可見,如果View沒有設置背景,View的寬度爲mMinWidth,mMinWidth對應
android:minWidth
屬性值,如果該屬性值不指定,mMinWidth默認0;如果View設置了背景,View的寬度爲max(mMinWidth, mBackground.getMinimumWidth())。再看Drawable的getMinimumWidth方法:public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }
getMinimumWidth返回的就是Drawable的原始寬度(前提是這個Drawable有原始寬度),否則就返回0。ShapeDrawable無原始寬/高,而BitmapDrawable有原始寬/高(圖片的尺寸)。
從getDefaultSize方法的實現看,在非UNSPECIFIED情況下,View的寬/高由SpecSize決定。
直接繼承View的自定義控件需要重寫onMeasure方法,且要設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。這是因爲View在佈局中使用wrap_content,那它的SpecMode是AT_MOST模式,它的寬/高等於SpecSize,根據之前的普通View的MeasureSpec的創建規則可知,這種情況下View的寬/高就是父容器當前剩餘的空間大小,與match_parent表現一致。解決方法:
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); } }
只需要給View指定一個默認的內部寬/高(mWidth和mHeight),並且在wrap_content時設置此寬/高即可。
- 對於非wrap_content情況,我們沿用系統的測量值即可。
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); } } }
再看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); }
measureChild方法做的就是抽出子元素的LayoutParams,然後通過getChildMeasureSpec創建子元素的MeasureSpec,接着將MeasureSpec傳遞給View的measure方法進行測量。
ViewGroup是抽象類,其測量過程的onMeasure交由各子類去實現,比如LinearLayout、RelativeLayout等。之所以ViewGroup不像View一樣統一onMeasure方法,是因爲ViewGroup的子類的佈局特性差異過大。
注意:在極端情況下,系統可能需要多次measure才能確認最終的測量寬/高,在onMeasure中獲取測量寬/高可能不準確。因此,在onLayout方法中取獲取View的測量寬/高或者最終寬/高。
如果我們想在Activity啓動時獲取某個View的寬/高,實際上在onCreat、onStart、onResume中均無法正確獲取(未完成測量時,獲取爲0)。這是因爲View的measure過程和Activity的生命週期方法不是同步執行的。解決方法有四個:
1.onWindowFocusChanged:View已經初始化完成,寬/高已經準備完成。注意該方法會被多次調用,當Activity的窗口得到焦點和失去焦點時均會被調用。頻繁的進行onResume和onPause,也會被調用。
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus){ int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }
2.view.post(runnable):通過post將一個runnable投遞到消息隊列尾部,然後等待Looper調用此runnable的時候,View已經初始化好了。
protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
3.ViewTreeObserver:ViewTreeObserver的衆多回調可以完成這個功能,比如OnGlobalLayoutListener接口,當View樹的狀態放生改變或者View樹內部的View的可見性發生改變時,OnGlobalLayoutListener方法會被調用。
protected void onStart() { super.onStart(); ViewTreeObserver observer = view.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnDrawListener(this); int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); }
4.view.measure(int widthMeasureSpec,int heightMeasureSpec):通過手動對View進行measure來得到View的寬高。比較複雜。
手動對View進行measure來得到View的寬高,要根據View的LayoutParams區分:
match_parent:無法測量。構造此種MeasureSpec需要知道parentSize,無法獲取。
具體數值(dp/px):寬高都是100px情況舉例:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec)
wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec)
注意:
(1<<30)-1
,View尺寸使用30位二進制表示,最大是30個1,2^30-1,即(1<<30)-1
。
1.3.2 layout過程
當ViewGroup的位置被確定後,它在onLayout中會遍歷所有的子元素並調用其layout方法,在layout方法中,onLayout方法又會被調用。layout方法確定View本身的位置,onLayout確定所有子元素的位置:
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); if (shouldDrawRoundScrollbar()) { if(mRoundScrollbarRenderer == null) { mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); } } else { mRoundScrollbarRenderer = null; } 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); } } } final boolean wasLayoutValid = isLayoutValid(); mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if (!wasLayoutValid && isFocused()) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; if (canTakeFocus()) { // We have a robust focus, so parents should no longer be wanting focus. clearParentsWantFocus(); } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) { // This is a weird case. Most-likely the user, rather than ViewRootImpl, called // layout. In this case, there's no guarantee that parent layouts will be evaluated // and thus the safest action is to clear focus here. clearFocusInternal(null, /* propagate */ true, /* refocus */ false); clearParentsWantFocus(); } else if (!hasParentWantsFocus()) { // original requestFocus was likely on this view directly, so just clear focus clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } // otherwise, we let parents handle re-assigning focus during their layout passes. } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { mPrivateFlags &= ~PFLAG_WANTS_FOCUS; View focused = findFocus(); if (focused != null) { // Try to restore focus as close as possible to our starting focus. if (!restoreDefaultFocus() && !hasParentWantsFocus()) { // Give up and clear focus once we've reached the top-most parent which wants // focus. focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false); } } } if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值。View 的四個頂點確定後,View在父容器中的位置也就確定了;接着會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,onLayout 的具體實現同樣和具體的佈局有關,所以View和ViewGroup均沒有真正實現onLayout方法。
View的getMeasureWidth(getMeasureHeight)和getWidth(getHeight)的區別:
public final int getWidth() { return mRight - mLeft; } public final int getHeight() { return mBottom - mTop; }
在View的默認實現中, View的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成於View的measure過程,而最終寬/高形成於View的layout過程。
特殊情況1:(重寫layout)
public void layout(int l,int t,int ,int b){ super.layout(l,t,r + 100,b + 100); }
上述代碼會導致在View的最終寬/高總是比測量寬/高大100px,但沒有實際意義。
特殊情況2:(多次measure)
過程中可能View的最終寬/高和測量寬/高不一致,但是最終會一致。
1.3.3 draw過程
draw的作用是將View繪製到屏幕上。
過程:
- 1.繪製背景:background.draw(canvas)
- 2.繪製自己:onDraw
- 3.繪製children :dispatchDraw
- 4.繪製裝飾:onDrawScrollBars
View繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw 會遍歷調用所有子元素的draw方法,重複直至繪製完成。
public void draw(Canvas canvas) { 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) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // 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); drawAutofilledHighlight(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // Step 7, draw the default focus highlight drawDefaultFocusHighlight(canvas); if (debugDraw()) { debugDrawFocus(canvas); } // we're done... return; } ...
View 有一個特殊的方法setWillNotDraw:
public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK); }
如果一個View不需要繪製任何內容,那麼設置這個標記位爲true以後,系統會進行相應的優化。默認View 沒有啓用這個優化標記位,但是ViewGroup會默認啓用這個優化標記位。注意:VicwGroup需要通過onDraw來繪製內容時,我們需要顯式地關閉WILL_NOT_DRAW這個標記位。
1.4 自定義View
1.4.1 自定義View的分類
1.繼承View重寫onDraw方法:
主要用於實現一些不規則的效果。預期效果不方便通過佈局的組合方式來達到,往往需要靜態或者動態地顯示一些不規則的圖形。採用這種方式需要自己支持wrap_content,padding也需要自己處理。
2.繼承ViewGroup派生特殊的Layout:
主要用於實現自定義佈局。需要合適地處理ViewGroup的測量、佈局這兩個過程;同時處理子元素的測量和佈局過程。
3.繼承特定的View(比如TextView):
一般用於擴展某種已有的Viewdev功能。不需要自己支持wrap_content和padding 等。
4.繼承特定的ViewGroup:
不需要自己處理ViewGroup的測量和佈局這兩個過程。與2的區別:方法2更接近View的底層。
1.4.2 自定義View須知
讓View支持wrap_content:
直接繼承View或者ViewGroup的控件,如果不在onMeasure中對wrap_content做特殊處理,那麼當外界佈局中使用wrap_content時就無法達到預期效果。
讓View支持padding:
直接繼承View的控件,如果不在draw方法中處理padding,那麼padding屬性是無法起作用的。直接繼承ViewGroup的控件要在onMeasure和onLayout中考慮padding和子元素的margin對其造成的影響,不然將導致padding和子元素的margin失效。
不要在View中使用Handler:
View內部本身就提供了post系列的方法,可以替代handler的作用。
View中如果有線程或者動畫,需要及時停止:
onDetachedFromWindow是停止線程或者動畫的好時機。當包含此View的Activity退出或者當前View被remove時,View 的onDetachedFromWindow方法會被調用;當包含View的Activity啓動時,View 的onAttachedToWindow方法會被調用。View變得不可見時我們也需要停止線程和動畫,如果不及時處理這種問題,有可能會造成內存泄漏。
View帶有滑動嵌套情形時,需要處理好滑動衝突:
如果有滑動衝突,要合適地處理滑動衝突,否則將會嚴重影響View的效果。
1.4.3 添加自定義屬性
1.在values目錄下面創建自定義屬性的XML,比如atrs.xml。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CircleView"> <attr name="circle_color" format="color"/> </declare-styleable> </resources>
除了color,還可以指定reference(資源id)、dimension(尺寸)、string、interger、boolean等。
2.在View的構造函數中解析自定義屬性的值並處理。
public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); //預加載自定義屬性集合CircleView TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.CircleView); //解析CircleView屬性集合中的circle_color屬性,並設置默認顏色值 mColor=typedArray.getColor(R.styleable.CircleView_circle_color,Color.RED); //實現資源 typedArray.recycle(); init(); }
3.在佈局文件中使用自定義屬性。
<com.virtual.testview.CircleView android:id="@+id/circleView1" android:layout_width="wrap_content" android:layout_height="100dp" app:circle_color="#F00A0A" android:background="#000000"/>
注意:使用自定義屬性,必須在佈局文件中添加schemas聲明:
xmlns:app="http://schemas.android.com/apk/res-auto"
。app是自定義前綴,可以更換其他名字
1.5 參考資料
- Android開發藝術探索