View 是如何显示到屏幕上的
View 是如何顯示到屏幕上的
- 基于 android 29
在上一篇 View 繪制流程解析中我們知道了在 Activity 進行 onResume 后 View 顯示到屏幕上前需要經過的流程,接下來這篇我們重點來看看 View 在顯示前 在 ViewRootImpl 的 performTraversals 方法中調用的三大方法。
文章目錄
- View 是如何顯示到屏幕上的
- 測量
- 測量總結
- 布局
- 布局總結
- 繪制
- 繪制總結
測量
首先在 View 繪制前到屏幕前的第一步,也是最復雜的一步,測量。
這里承接上一篇從 ViewRootImpl#performTraversals 中調用 performMeasure 方法開始講起。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {if (mView == null) {return;}Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");try {mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);} }首先可以知道這里的 mView 就是 DecorView,那 childWidthMeasureSpec 和 childHeightMeasureSpec 又是什么呢?想了解它,我們就必須先來看看 View 中的一個重要的內部類 MeasureSpec 了。
MeasureSpec 中有幾個非常重要的常量,這里我用 2 進制先寫在開頭,以便于后面的分析。
-
MODE_MASK:11000000000000000000000000000000
-
UNSPECIFIED:00000000000000000000000000000000
-
EXACTLY:01000000000000000000000000000000
-
AT_MOST:10000000000000000000000000000000
首先我們通過如下方法看 MeasureSpec 的數值是如何構造出來的
public static int makeMeasureSpec(int size, int mode) {// sUseBrokenMakeMeasureSpec 其實是判斷安卓版本如果是小于等于 17 時才為 true,否則為 falseif (sUseBrokenMakeMeasureSpec) {return size + mode;} else {// 在安卓 17 以上時這樣寫其實出于安全考慮,作用和加法一致return (size & ~MODE_MASK) | (mode & MODE_MASK);} }通過個方法其實就能知道 MeasureSpec 其實通過巧妙的位運算同事包含了大小和測量模式。在一個 int 值的中的前 2 位其實就是測量模式后 30 位是大小。
下面舉個例子,若傳入 size = 3,mode = EXACTLY:
(size)00000000000000000000000000000111 & (~MODE_MASK)00111111111111111111111111111111
= 00000000000000000000000000000111
(mode)01000000000000000000000000000000 & (MODE_MASK)00111111111111111111111111111111
= 01000000000000000000000000000000
(size & ~MODE_MASK) | (mode & MODE_MASK)
00000000000000000000000000000111 | 01000000000000000000000000000000
= 01000000000000000000000000000111
在理解了 makeMeasureSpec 的原理后后面的兩個方法也很容易理解啦。
獲取測量模式:
public static int getMode(int measureSpec) {//noinspection ResourceTypereturn (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); }在了解了 MeasureSpec 的原理后我們回到 mView.measure 方法,來看看這里的 childWidthMeasureSpec 和 childHeightMeasureSpec 是怎么得來的,因此我們回到 ViewRootImpl#performTraversals 方法中看到了這樣一句。
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);這里的 mWidth 和 mHeight 分別是屏幕的寬高,lp.width 和 lp.height 分別是 DecorView 的寬高的 LayoutParams 的屬性,接下來我們進入 getRootMeasureSpec 方法具體看下做了什么。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec; }從這里我可以知道當布局屬性為 MATCH_PARENT 時,測量模式為 EXACTLY,大小為窗口大小,當布局屬性為 WRAP_CONTENT 時,測量模式為 AT_MOST,大小為窗口大小,當布局屬性為精確值時,測量模式為 EXACTLY,大小為 DecorView 的大小
在知道了 childWidthMeasureSpec 和 childHeightMeasureSpec 是怎么得來的后我們繼續跟蹤進入 View 的 measure。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {boolean optical = isLayoutModeOptical(this);if (optical != isLayoutModeOptical(mParent)) {Insets insets = getOpticalInsets();int oWidth = insets.left + insets.right;int oHeight = insets.top + insets.bottom;widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);}// 這里將 widthMeasureSpec 和 heightMeasureSpec 拼接成功了一個 long 作為測量緩存的 keylong key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);// 下面這段判斷了是否需要布局final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec|| heightMeasureSpec != mOldHeightMeasureSpec;final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);final boolean needsLayout = specChanged&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);if (forceLayout || needsLayout) {mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;resolveRtlPropertiesIfNeeded();// 判斷之前緩存的測量值有沒有,或是否強制布局int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);if (cacheIndex < 0 || sIgnoreMeasureCache) {onMeasure(widthMeasureSpec, heightMeasureSpec);mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;} else {// 有的話直接取long value = mMeasureCache.valueAt(cacheIndex);setMeasuredDimensionRaw((int) (value >> 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}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;// 存入寬高的 MeasureSpecmMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL); }這里有個小細節 getMeasuredHeight 和 getMeasuredWidth。
public final int getMeasuredWidth() {return mMeasuredWidth & MEASURED_SIZE_MASK; } public final int getMeasuredHeight() {return mMeasuredHeight & MEASURED_SIZE_MASK; }由于 MEASURED_SIZE_MASK = 0x00ffffff 這兩個方法返回的寬和高只取了一個 int 的后 24 位。
在了解 View 的 measure 做了什么后,我們繼續跟蹤進入 onMeasure 方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }我們發現 setMeasuredDimension 方法就是設置 View 的寬高的大小,因此我們重點看 getDefaultSize 和 getSuggestedMinimumWidth,getSuggestedMinimumHeight 這幾個方法。
protected int getSuggestedMinimumWidth() {return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() {return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); } 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; }可以看到 getSuggestedMinimumWidth,getSuggestedMinimumHeight 這兩個方法就是取背景的大小和最小寬高的最大值,而 getDefaultSize 會判斷若測量模式是 UNSPECIFIED 時返回的就是 getSuggestedMinimumWidth,getSuggestedMinimumHeight 的大小,而 AT_MOST 和 EXACTLY 時,返回的是實際測量的大小。
由于我們這里的 View 是 DecorView ,而 DecorView 繼承自 FrameLayout,因此我們還需要進入 FrameLayout 的 onMeasure 中查看。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int count = getChildCount();// 寬和高任意一個不是 EXACTLY 測量模式就為 truefinal boolean measureMatchParentChildren =MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;// 先清空 MATCH_PARENT 的子組件集合mMatchParentChildren.clear();int maxHeight = 0;int maxWidth = 0;int childState = 0;// 遍歷和測量子組件的并獲取最大寬高for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (mMeasureAllChildren || child.getVisibility() != GONE) {// 測量子 View 的邊界,也就是子 View 的大小// 會先調用 getChildMeasureSpec,再調用子 View 的 measure 方法measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);final LayoutParams lp = (LayoutParams) child.getLayoutParams();maxWidth = Math.max(maxWidth,child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);maxHeight = Math.max(maxHeight,child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);childState = combineMeasuredStates(childState, child.getMeasuredState());if (measureMatchParentChildren) {// 這里判斷寬和高有一個是 MATCH_PARENT 的就添加到 mMatchParentChildren 集合中if (lp.width == LayoutParams.MATCH_PARENT ||lp.height == LayoutParams.MATCH_PARENT) {mMatchParentChildren.add(child);}}}}// 將 padding 加到最大寬高上maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();// 背景的最小寬高和當前最大寬高,取大的maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());// 前景圖的最小寬高和當前最大寬高,取大的final Drawable drawable = getForeground();if (drawable != null) {maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());}// 先保存自己的寬高和測量值setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),resolveSizeAndState(maxHeight, heightMeasureSpec,childState << MEASURED_HEIGHT_STATE_SHIFT));count = mMatchParentChildren.size();if (count > 1) {// 接下來這里會再次測量 MATCH_PARENT 的子 Viewfor (int i = 0; i < count; i++) {final View child = mMatchParentChildren.get(i);final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();final int childWidthMeasureSpec;if (lp.width == LayoutParams.MATCH_PARENT) {final int width = Math.max(0, getMeasuredWidth()- getPaddingLeftWithForeground() - getPaddingRightWithForeground()- lp.leftMargin - lp.rightMargin);childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);} else {childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeftWithForeground() + getPaddingRightWithForeground() +lp.leftMargin + lp.rightMargin,lp.width);}final int childHeightMeasureSpec;if (lp.height == LayoutParams.MATCH_PARENT) {final int height = Math.max(0, getMeasuredHeight()- getPaddingTopWithForeground() - getPaddingBottomWithForeground()- lp.topMargin - lp.bottomMargin);childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);} else {childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,getPaddingTopWithForeground() + getPaddingBottomWithForeground() +lp.topMargin + lp.bottomMargin,lp.height);}child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}} }通過這個方法我們知道了在 ViewGroup 的 onMeasure 中,會先遍歷子 View 進行測量,以此來確定自身的大小。而測量子 View 的核心就是取出子 View 的 LayoutParams 和 ViewGroup 自身的 MeasureSpec,然后傳入 getChildMeasureSpec 方法中來創建出子 View 的 MeasureSpec。
至此 View 的測量就結束了。
測量總結
-
MeasureSpec 前兩位是測量模式,后 30 位是大小
-
getMeasuredWidth 和 getMeasuredHeight 方法返回的寬和高只取了一個 int 的后 24 位
-
FrameLayout 的寬和高任意一個不是 EXACTLY 測量模式,且子 View 中 MATCH_PARENT 屬性有兩個時,MATCH_PARENT 的子 View 會測量 2 次
-
View 的 MeasureSpec 在 ViewGroup#getChildMeasureSpec 中完成的測量,其是根據父容器的 MeasureSpec 和自己的 LayoutParams 決定的
\父容器的測量模式 EXACTILY父容器的測量模式AT_MOST父容器的測量模式UNSPECIFIED 子 view 的 LayoutParams:直接輸入值 mode:EXACTILY
size:childSizemode:EXACTILY
size:childSizemode:EXACTILY
size:childSize子 view 的 LayoutParams:match_parent mode:EXACTILY
size:parentSizemdoe:AT_MOST
size:parentSizemdoe:UNSPECIFIED
size:0子 view 的 LayoutParams:wrap_content mode:AT_MOST
size:parentSizemdoe:AT_MOST
size:parentSizemdoe:UNSPECIFIED
size:0
布局
這是 View 顯示到屏幕上的第二步,布局。
這里承接上一篇從 ViewRootImpl#performTraversals 中調用 performLayout 方法開始講起。
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,int desiredWindowHeight) {// ... ...final View host = mView;if (host == null) {return;}// ... ...try {host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());// ... ...} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}mInLayout = false; }這里我們只看重點的方法 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()),因此我們跟蹤進入 layout 方法中,在 layout 方法中有個 setFrame 方法該方法需要傳入 4 個值來確定 View 的位置,也就是 left,top,right,bottom。而該方法調用完后會繼續調用 onLayout 方法,若該 View 是 ViewGroup 時則需要重寫該方法。這里我們來看 FrameLayout 的 onLayout 的實現。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {layoutChildren(left, top, right, bottom, false /* no force left gravity */); } void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {final int count = getChildCount();// 獲取自己 paddingfinal int parentLeft = getPaddingLeftWithForeground();final int parentRight = right - left - getPaddingRightWithForeground();final int parentTop = getPaddingTopWithForeground();final int parentBottom = bottom - top - getPaddingBottomWithForeground();// 遍歷子 View for (int i = 0; i < count; i++) {final View child = getChildAt(i);// 只對可見性不為 GONE 的 View 布局if (child.getVisibility() != GONE) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();// 獲取子 View 的寬高final int width = child.getMeasuredWidth();final int height = child.getMeasuredHeight();int childLeft;int childTop;int gravity = lp.gravity;if (gravity == -1) {gravity = DEFAULT_CHILD_GRAVITY;}final int layoutDirection = getLayoutDirection();final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;// 下面這段是通過自己的 padding 和子 View 的 Gravity 和 margin 來確定子 View 的位置switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {case Gravity.CENTER_HORIZONTAL:childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +lp.leftMargin - lp.rightMargin;break;case Gravity.RIGHT:if (!forceLeftGravity) {childLeft = parentRight - width - lp.rightMargin;break;}case Gravity.LEFT:default:childLeft = parentLeft + lp.leftMargin;}switch (verticalGravity) {case Gravity.TOP:childTop = parentTop + lp.topMargin;break;case Gravity.CENTER_VERTICAL:childTop = parentTop + (parentBottom - parentTop - height) / 2 +lp.topMargin - lp.bottomMargin;break;case Gravity.BOTTOM:childTop = parentBottom - height - lp.bottomMargin;break;default:childTop = parentTop + lp.topMargin;}// 確定完位置后通知子 View 布局child.layout(childLeft, childTop, childLeft + width, childTop + height);}} }FrameLayout 會先獲取自己的 padding,然后遍歷取出子 View,判斷子 View 的可見性不為 GONE 時,會通過過自己的 padding 和子 View 的 Gravity 和 margin 來確定子 View 的位置。
至此 View 的布局過程就結束了。
布局總結
- View 的布局會調用 layout 方法,而 layout 中會通過 setFrame 先確定自身的位置然后再調用 onLayout 方法
- ViewGroup 需要重寫 onLayout 方法來為子 View 確定位置
繪制
這是 View 顯示到屏幕上的最后一步,繪制。
這里承接上一篇從 ViewRootImpl#performTraversals 中調用 performDraw 在該方法中有會先調用 draw 方法,在 draw 方法中又會調用 drawSoftware,在 drawSoftware 中我們便能看到熟悉的 mView.draw(canvas) 啦!
public void draw(Canvas canvas) {final int privateFlags = mPrivateFlags;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 neededint saveCount;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 contentonDraw(canvas);// Step 4, draw the childrendispatchDraw(canvas);drawAutofilledHighlight(canvas);// Overlay is part of the content and draws beneath Foregroundif (mOverlay != null && !mOverlay.isEmpty()) {mOverlay.getOverlayView().dispatchDraw(canvas);}// Step 6, draw decorations (foreground, scrollbars)onDrawForeground(canvas);// Step 7, draw the default focus highlightdrawDefaultFocusHighlight(canvas);if (debugDraw()) {debugDrawFocus(canvas);}// we're done...return;}// ... ... }繪制總結
繪制有 4 步
總結
以上是生活随笔為你收集整理的View 是如何显示到屏幕上的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 神出鬼没USO:幽灵潜艇
- 下一篇: 乔任梁之死:希望更多人看到并了解!