參考資料 << Android 開發藝術探索 >> 歡迎訪問個人我的博客 傳送門html
在平常開發中,咱們天天都在和各類 View 打交道,好比TextView,Button等,咱們直接拿過來就可使用,那麼 Android 是怎麼把 View 繪製到屏幕上呢,接下來咱們結合源碼來具體分析。android
在具體結合源碼分析前,先了解一個比較重要的概念 ViewRootcanvas
先看一張圖 Android 窗口構成圖解app
ViewRoot 對應於 ViewRootImpl 類,它是鏈接 WindowManager 和 根佈局 DecorView(看上圖) 的紐帶, View 的三大流程均是經過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被建立完畢後,會將 DecorView 添加到 Window 中,同時會建立 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 創建關聯。ide
View 的繪製流程是從 ViewRoot 的 performTraversals 方法開始的,它通過 measure、layout 和 draw 三個過程才能最終將一個 View 繪製出來,其中 measure 用來測量 View 的寬和高,layout 用來肯定 View 在父容器中的放置位置,而 draw 則負責將 View 繪製在屏幕上。針對 performTraversals的大體流程以下:源碼分析
performTraversals 會依次調用 performMeasure、performLayout 和 performDraw 三個方法,這三個方法分別完成頂級 View 的 measure、layout 和 draw 這三大流程,其中在 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對全部的子元素進行 measure 過程,這個時候 measure 流程就從父容器傳遞到子元素中了,這樣就完成了一次 measure 過程。接着子元素會重複父容器的 measure 過程,如此反覆就完成了整個 View 樹的遍歷。同理,performLayout 和 performDraw 的傳遞流程和 performMeasure 是相似的,惟一不一樣的是,performDraw 的傳遞過程是在 draw 方法中經過 dispatchDraw 來實現的,不過這並無本質區別。佈局
接下來結合源碼來分析這三個過程。post
這裏分兩種狀況,View 的測量過程和 ViewGroup 的測量過程。測試
View 的 測量過程由其 measure 方法來完成,源碼以下:this
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { //省略代碼... if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }
能夠看到 measure 方法是一個 final 類型的方法,這意味着子類不能重寫此方法。
在 13 行 measure 中會調用 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; }
能夠看到要獲得測量的尺寸須要用到 MeasureSpec,MeasureSpec 是什麼鬼呢,敲黑板了,重點來了。
MeasureSpec 決定了 View 的測量過程。確切來講,MeasureSpec 在很大程度上決定了一個 View 的尺寸規格。
來看 MeasureSpec 類的實現
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** @hide */ @IntDef({UNSPECIFIED, EXACTLY, AT_MOST}) @Retention(RetentionPolicy.SOURCE) public @interface MeasureSpecMode {} public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; 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); } @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } public static String toString(int measureSpec) { //省略... } }
能夠看出 MeasureSpec 中有兩個主要的值,SpecMode 和 SpecSize, SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。
SpecMode 有三種模式:
UNSPECIFIED
不限制:父容器不對 View 有任何限制,要多大給多大,這種狀況比較少見,通常不會用到。
EXACTLY
限制固定值:父容器已經檢測出 View 所須要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。
MeasureSpec 中三個主要的方法來處理 SpecMode 和 SpecSize
不知道童鞋們以前有沒有注意到 onMeasure 有兩個參數 widthMeasureSpec 和 heightMeasureSpec,那這兩個值從哪來的呢,這兩個值都是由父視圖通過計算後傳遞給子視圖的,說明父視圖會在必定程度上決定子視圖的大小,可是最外層的根視圖 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是從哪裏獲得的呢?這就須要去分析 ViewRoot 中的源碼了,在 performTraversals 方法中調了 measureHierarchy 方法來建立 MeasureSpec 源碼以下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; int childHeightMeasureSpec; childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //省略代碼... }
裏面調用了 getRootMeasureSpec 方法生成 MeasureSpec,繼續查看 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 中的寬和高的參數來劃分。
對於 DecorView 而言, rootDimension 的值爲 lp.width 和 lp.height 也就是屏幕的寬和高,因此說 根視圖 DecorView 的大小默認老是會充滿全屏的。那麼咱們使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 產生過程又是怎麼樣的呢,在 ViewGroup 的測量過程當中會具體介紹。
先回頭看 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; }
如今理解起來是否是很簡單呢,若是 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,這也是系統默認的行爲。以後會在 onMeasure 方法中調用 setMeasuredDimension 方法來設定測量出的大小,這樣 View 的 measure 過程就結束了,接下來看 ViewGroup 的 measure 過程。
ViewGroup中定義了一個 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); } } }
從上述代碼來看,除了完成本身的 measure 過程之外,還會遍歷去全部在頁面顯示的子元素,
而後逐個調用 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); }
measureChild 的思想就是取出子元素的 LayoutParams,而後再經過 getChildMeasureSpec 來建立子元素的 MeasureSpec,接着將 MeasureSpec 直接傳遞給 View 的 measure 方法來進行測量。
那麼 ViewGroup 是如何建立來建立子元素的 MeasureSpec 呢,咱們繼續看 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); }
上面的代碼理解起來很簡單,爲了更清晰地理解 getChildMeasureSpec 的邏輯,這裏提供一個表,表中對 getChildMeasureSpec 的工做原理進行了梳理,表中的 parentSize 是指父容器中目前可以使用的大小,childSize 是子 View 的 LayoutParams 獲取的值,從 measureChild 方法中可看出
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);
表以下:
經過上表能夠看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就能夠快速地肯定出子元素的 MeasureSpec 了,有了 MeasureSpec 就能夠進一步肯定出子元素測量後的大小了。
至此,View 和 ViewGroup 的測量過程就告一段落了。來個小結。
MeasureSpec 的模式和生成規則
MeasureSpec 中 specMode 有三種模式:
UNSPECIFIED
不限制:父容器不對 View 有任何限制,要多大給多大,這種狀況比較少見,通常不會用到。
EXACTLY
限制固定值:父容器已經檢測出 View 所須要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。
生成規則:
MeasureSpec 測量過程:
measure 過程主要就是從頂層父 View 向子 View 遞歸調用 view.measure 方法,measure 中調 onMeasure 方法的過程。
說人話呢就是,視圖大小的控制是由父視圖、佈局文件、以及視圖自己共同完成的,父視圖會提供給子視圖參考的大小,而開發人員能夠在 XML 文件中指定視圖的大小,而後視圖自己會對最終的大小進行拍板。
那麼測量事後,怎麼獲取 View 的測量結果呢
通常狀況下 View 測量大小和最終大小是同樣的,咱們可使用 getMeasuredWidth 方法和 getMeasuredHeight 方法來獲取視圖測量出的寬高,可是必須在 setMeasuredDimension 以後調用,不然調用這兩個方法獲得的值都會是0。爲何要說是通常狀況下是同樣的呢,在下文介紹 Layout 中會具體介紹。
測量結束後,視圖的大小就已經測量好了,接下來就是 Layout 佈局的過程。上文說過 ViewRoot 的 performTraversals 方法會在 measure 結束後,執行 performLayout 方法,performLayout 方法則會調用 layout 方法開始佈局,代碼以下
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { mLayoutRequested = false; mScrollMayChange = true; mInLayout = true; final View host = mView; if (host == null) { return; } if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { Log.v(mTag, "Laying out " + host + " to (" + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); } try { host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); //...省略代碼 } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } mInLayout = false;
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); 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); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } }
layout 方法接收四個參數,分別表明着左、上、右、下的座標,固然這個座標是相對於當前視圖的父視圖而言的,而後會調用 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft、mRight、mTop、mBottom 這四個值,View 的四個頂點一旦肯定,那麼 View 在父容器中的位置也就肯定了,接着會調用 onLayout 方法,這個方法的用途是父容器肯定子元素的位置,和 onMeasure 方法相似
onLayout 源碼以下:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
納尼,怎麼是個空方法,沒錯,就是一個空方法,由於 onLayout 過程是爲了肯定視圖在佈局中所在的位置,而這個操做應該是由佈局來完成的,即父視圖決定子視圖的顯示位置,咱們繼續看 ViewGroup 中的 onLayout 方法
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
能夠看到,ViewGroup 中的 onLayout 方法居然是一個抽象方法,這就意味着全部 ViewGroup 的子類都必須重寫這個方法。像 LinearLayout、RelativeLayout 等佈局,都是重寫了這個方法,而後在內部按照各自的規則對子視圖進行佈局的。因此呢咱們若是要自定義 ViewGroup 那麼就要重寫 onLayout 方法。
public class TestViewGroup extends ViewGroup { public TestViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() > 0) { View childView = getChildAt(0); measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } }
xml 中使用
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.will.testdemo.customview.TestViewGroup android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/holo_blue_bright" > <TextView android:id="@+id/tv_hello" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="15sp" /> </com.will.testdemo.customview.TestViewGroup> </LinearLayout>
顯示效果以下:
不知道童鞋們發現了沒,我給自定義的 ViewGroup 設置了背景色,看效果貌似佔滿全屏了,但是我在 xml 中設置的 wrap_content 啊,這是什麼狀況,咱們回頭看看 ViewGroup 中 View 的 MeasureSpec 的建立規則
從表中可看出由於 ViewGroup 的父佈局設置的 match_parent 也就是限制固定值模式,而 ViewGroup 設置的 wrap_content,那麼最後 ViewGroup 使用的是 父佈局的大小,也就是窗口大小 parentSize,那麼若是咱們給 ViewGroup 設置固定值就會使用 咱們設置的值,來改下代碼。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.will.testdemo.customview.TestViewGroup android:layout_width="200dp" android:layout_height="100dp" android:background="@android:color/holo_blue_bright" > <TextView android:id="@+id/tv_hello" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark" android:text="@string/hello_world" android:textColor="@android:color/white" android:textSize="15sp" /> </com.will.testdemo.customview.TestViewGroup> </LinearLayout>
效果以下:
表中的其餘狀況,建議童鞋們本身寫下代碼,會理解的更好。
以前說過,通常狀況下 View 測量大小和最終大小是同樣的,爲何呢,由於最終大小在 onLayout 中肯定,咱們來改下代碼:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200); } }
顯示效果
沒錯,onLayout 就是這麼任性,因此要獲取 View 的真實大小最好在 onLayout 以後獲取。那麼如何來獲取 view 的真實大小呢,能夠經過下面的代碼來獲取
tv_hello.post(Runnable { log(" getMeasuredWidth() = ${tv_hello.measuredWidth}") log(" getWidth() = ${tv_hello.width}") }
打印以下:
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getMeasuredWidth() = 239 01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog: getWidth() = 339
能夠看到實際高度和測試的高度是不同的,由於咱們在 onLayout 中作了修改。
由於 View 的繪製過程和 Activity 的生命週期是不一樣步的,因此咱們可能在 onCreate 中獲取不到值。這裏提供幾種方法來獲取
1.Activity 的 onWindowFocusChanged 方法
override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus){ //獲取 view 的大小 } }
2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 這裏童鞋們搜索下就能夠找到使用方法,篇幅較長就不舉例子了
肯定了 View 的大小和位置後,那就要開始繪製了,Draw 過程就比較簡單,它的做用是將 View 繪製到屏幕上面。View 的繪製過程遵循以下幾步:
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 的繪製過程的傳遞是經過 dispatchDraw 實現的,dispatchdraw 會遍歷調用全部子元素的 draw 方法,如此 draw 事件就一層一層的傳遞下去。和 Layout 同樣 View 是不會幫咱們繪製內容部分的,所以須要每一個視圖根據想要展現的內容來自行繪製,重寫 onDraw 方法。具體可參考 TextView 或者 ImageView 的源碼。
View 的工做流程和原理到這就分析完了,難點主要是 MeasureSpec 測量過程,須要童鞋們認真揣摩。