<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                [TOC] View 是 Android 應用開發中相當重要的角色,重要程度絲毫不亞于四大組件,甚至遠高于 Broadcast 和 ContentProvider。本文為學習《Android 開發藝術探索》中 View 的工作原理后,自己對于 View 的三大流程的源碼分析。 # ViewRoot Activity在添加Window時,會先創建一個ViewrRootImpl,并通過ViewRootImpl來完成界面更新和Window的添加操作。View 的繪制流程從 ViewRootImpl 的 performTraversals 方法開始,在其內部調用View的measure、layout 和 draw 方法將一個 View 繪制出來。 這個過程如下圖所示: ![](https://img.kancloud.cn/15/3d/153d56e6b3f961b1f4c22bf36c230dc8_773x573.jpg) 每一個視圖的繪制過程都必須經歷三個最主要的階段,即onMeasure()、onLayout()和onDraw()。從父布局開始往子布局依次繪制。 # 測量measure View的measure方法在ViewGroup中被調用,ViewGroup會傳遞寬高兩個方向的測量說明書MeasureSpec過來,View根據MeasureSpec設置自身的測量寬高。 ```java public final void measure(int widthMeasureSpec, int heightMeasureSpec) {} ``` ## LayoutParams的概念 ```java // View.java protected ViewGroup.LayoutParams mLayoutParams; ``` 每個View都有一個ViewGroup.LayoutParams屬性,該屬性是給父布局ViewGroup在測量、布局、繪制子View時使用的,告訴父容器自己想要被布局成什么樣。 ViewGroup.LayoutParams是ViewGroup的一個靜態內部類,描述了當前ViewGroup的子View可以配置哪些屬性。ViewGroup的LayoutParams僅僅描述了View的寬、高,ViewGroup的子類可以在LayoutParams中添加一些其他的屬性,比如LinearLayout的LayoutParams增加了weight屬性和gravity屬性。 在LinearLayout測量、布局以及繪制子View時,都會從子View的LayoutParams中取出相關屬性的值進行使用。 ## MeasureSpec的概念 MeasureSpec 代表一個32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode 記錄規格,SpecSize 記錄大小。 SpecMode 有三種模式: * EXACTLY:父視圖希望 View 的大小由 specSize 決定 * AT_MOST:View 最大只能是 specSize 中指定的大小 * UNSPECIFIED:View 的大小沒有限制 每一個 View 都會有自己的 MeasureSpec,可以理解為測量說明書。每個View 的 MeasureSpec 都由父視圖提供給它,所以 View 的測量大小會受父視圖的影響。 View在拿到父視圖傳遞過來的兩個MeasureSpec后,怎么設定自己的寬高呢,一起來看看: ## View 的 measure 過程 View的measure方法會調用自身的onMeasure方法,在onMeasure方法中設置自身的測量寬高。想要改變View的測量寬高時,可以重寫onMeasure方法,onMeasure方法源碼如下: ```java /** * 兩個參數為寬高兩個方向的 MeasureSpec(測量說明書) * 由 measure 方法傳遞過來,View 的 measure 方法則是在 ViewGroup 的 measurechildren 方法中調用 */ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 設置測量出來的寬/高 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } ``` 先調用getDefaultSize方法取到自己的測量寬高,再調用setMeasuredDimension方法進行設置,依次來看: 1、getDefaultSize 方法是關鍵的方法,用來從父布局傳來的MeasureSpec中獲取自己的測量寬/高,源碼如下: ```java /** * @param size:View 的默認大小 * @param measureSpec:子 View 的 measureSpec(父視圖給過來的) * @return 此 View 的測量寬/高 */ 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; } ``` 可以看到: * 測量模式 為 UNSPECIFIED 模式時,此 View 的測量值為默認值,默認值通過getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()方法獲得 * 測量模式 為 AT_MOST 模式和 EXACTLY 模式時,此 View 的測量值為父視圖所給 measureSpec 中的 specSize getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法類似,選擇一個看源碼: ```java protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } ``` 可以看出: * View 未指定背景時,View 的寬度為 mMinWidth,即 android:minWidth 屬性所指定的值(默認為 0) * View 指定背景時,View 的寬度為 mMinWidth 和背景最小寬度 這兩者中的較大值 2、開始設置測量寬高,setMeasuredDimension方法如下: ```java protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { //... setMeasuredDimensionRaw(measuredWidth, measuredHeight); } // 設置測量寬、測量高的值 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { //測量出來的寬高設置給 View 的兩個成員變量:mMeasuredWidth 和 mMeasuredHeight mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; //... } ``` 通過上面代碼看到,View從父布局傳遞來的MeasureSpec中,獲取SpecMode和SpecSize來確定自身的大小。那么父布局又是如何給子View生成MeasureSpec的呢?來看看ViewGroup的測量過程。 ## ViewGroup 的 measure 過程 ViewGroup 繼承自View類,為一個抽象類,因此沒有重寫 View 的 onMeasure 方法,都留給子類去實現。但是提供了測量子元素的方法 measureChildren。 ViewGroup 的子類可以重寫 onMeasure 方法,調用 measureChildren 方法,來測量所有的子View: ViewGroup在onMeasure方法中設置自身的測量寬高,同時通過父布局給自己的MeasureSpec,來為自己的子View設置MeasureSpec。 ```java // 這兩個參數為ViewGroup從它的父布局那里獲取來的MeasureSpec, // 本來是用來設置ViewGroup自己的測量寬、高的,現在需要用到這兩個參數來為子View生成MeasureSpec 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方法如下: ```java protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { // 獲取子View的LayoutParams final LayoutParams lp = child.getLayoutParams(); // 獲取子元素的 MeasureSpec final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 調用子元素View的 measure 方法 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } ``` 可以看到,先是得到子元素的 LayoutParams,然后根據 MeasureSpec、自身的padding以及子元素的 LayoutParams 來計算出子元素的 MeasureSpec。getChildMeasureSpec方法源碼如下: ```java /** * @param spec ViewGroup的 MeasureSpec * @param padding ViewGroup的padding 值 * @param childDimension 子元素自身設定的寬、高值 */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 得到ViewGroup的測量模式和測量值 int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); // ViewGroup的剩余空間 int size = Math.max(0, specSize - padding); // 子元素的測量值和測量模式 int resultSize = 0; int resultMode = 0; switch (specMode) { // ViewGroup為精準模式 case MeasureSpec.EXACTLY: if (childDimension >= 0) { // 子元素設置了固定的寬高值時 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子元素想和ViewGroup一樣大,就讓它一樣大 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子元素想自己確定它的大小,但是不能比ViewGroup更大 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // ViewGroup為最大值模式時 case MeasureSpec.AT_MOST: if (childDimension >= 0) { // 子元素想設置成一個具體的值,讓它設 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子元素想和ViewGroup一樣大,但是ViewGroup大小還不確定呢 // 要約束子元素不能比ViewGroup還大 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子元素想決定它自己的大小,可以,但是不能比ViewGroup還大 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // ViewGroup問我們我們想多大(ViewGroup為未指定模式時) case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 子元素想指定一個具體的值,讓它去吧 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子元素想和ViewGroup一樣大,來確定下 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 子元素想自己確定它的大小,幫它確定下 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //返回子元素的 MeasureSpec return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } ``` 可以看到,首先拿到ViewGroup的 SpecMode 和 SpecSize,再根據ViewGroup的 SpecMode 的不同,給子 View 設置不同的 MeasureSpec。 當子 View 的寬高設置不同的值時,它的 MeasureSpec 也不同: * View 采用固定寬高時,不管ViewGroup的 MeasureSpec 是什么,View 的 MeasureSpec 都是 Exactly 模式并且大小等于其 LayoutParams 中的設置大小 * View 寬高是 match_parent 時:ViewGroup是精準模式,View 也是精準模式,大小是ViewGroup的剩余空間;ViewGroup是最大化模式時,View 也是最大化模式,且大小也是ViewGroup的剩余空間 * View 寬高是 wrap_content 時,不管ViewGroup是精準模式還是最大化模式,View 的模式總是最大化模式且大小均為ViewGroup的剩余空間 ## 問題拓展 1、在上面的分析中可以看到,當 View 的 SpecMode 為 AT_MOST 和 EXACTLY 時,測量值均為 specSize。因此就有一個問題: 當View配置wrap\_content時,View的SpecMode總是為AT_MOST,SpecSize總是為ViewGroup的剩余空間。View在根據MeasureSpec設置測量寬高時,就會設置成ViewGroup的剩余空間,與期望的wrap\_content不符 解決方案為自定義View時重寫onMeasure方法,在View的SpecMode為AT_MOST時,為View指定一個寬高。ImageView、TextView等都是如此操作,可參考其源碼。 以 TextView 的源碼為例: ```java if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desired, heightSize); } ``` 2、另一個問題,根布局沒有父視圖,MeasureSpec由哪里來呢?一起來看看。 在 ViewRootImpl 的 performTraversals 方法中通過如下方式獲取MeasureSpec: ```java // lp.width和lp.height在創建ViewGroup實例的時候已被賦值為MATCH_PARENT childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); ``` getRootMeasureSpec方法源碼如下: ```java 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; } ``` 可以看到根布局的 MeasureSpec 由 windowSize 和 ViewGroup.LayoutParams 決定。由于第二個參數為MATCH_PARENT,所以根布局的SpecMode為EXACTLY模式,SpecSize為windowsSize,也就是根布局會充滿全屏。 ## 整體總結【重點關注】 1、不管是View還是ViewGroup,每個人都會有一個父布局給過來的MeasureSpec,用來設置自己的寬高 * 測量模式為AT_MOST或EXACTLY時,測量寬高值為MeasureSpec中的specSize * 測量模式為UNSPECIFIED時,測量寬高值為默認值 2、View在onMeasure方法中,根據從父布局ViewGroup那里得到的MeasureSpec,來設置自身的寬高。 3、同樣,ViewGroup也是在onMeasure方法中設置自己的寬高。 4、ViewGroup在onMeasure方法中設置自己寬高之前會多做一步,循環為所有子View設置MeasureSpec,并調用每個子View的measure方法,讓子View測量自己的寬高。 5、ViewGroup根據自己的MeasureSpec、padding以及子View的LayoutParams,為子View設置MeasureSpec。 * 子View設置為固定寬高時,子View的specMode為:Exactly,specSize為:LayoutParams中設置的值,子View的測量寬高為LayoutParams中設置的值。 * 子View設置為match_parent時,子View的specMode為:等同ViewGroup的specMode,specSize為:ViewGroup的剩余空間,子View的測量寬高為ViewGroup的剩余空間。 * 子View設置為wrap_content時,子View的specMode為:AT_MOST,specSize為:ViewGroup的剩余空間,子View的測量寬高為ViewGroup的剩余空間。 6、自定義ViewGroup通常會重寫onMeasure方法,在onMeasure方法中為所有子View生成MeasureSpec并測量子View寬高。 # layout ## View 的 layout 方法 View的layout方法會為View自己及所有的childView指定位置,View 的 layout 方法源碼如下: ```java /** * l:View 左側坐標 * t:View 上方坐標 * r:View 右側坐標 * b:View 下方坐標 * 以上均指相對于父視圖 */ public void layout(int l, int t, int r, int b) { // 設置當前View的位置 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // 設置每個childView的位置 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); //... } } ``` 兩個關鍵方法,一個是setFrame方法設置當前View的位置,一個是onLayout方法設置childView的位置,分別來看看: 1、setFrame方法源碼如下: ```java protected boolean setFrame(int left, int top, int right, int bottom) { //... if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; int newWidth = right - left; int newHeight = bottom - top; // 設置當前View的位置(相對于父布局的) mLeft = left; mTop = top; mRight = right; mBottom = bottom; // 當前View大小改變,我們可以重寫onSizeChanged方法做相應處理 if (sizeChanged) { sizeChange(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { //請求重新繪制View invalidate(sizeChanged); invalidateParentCaches(); } } return changed; } ``` 2、在ViewGroup代碼中,onLayout方法是一個抽象方法,子類需要實現此方法,在實現中計算出每個childView的位置。 ```java protected abstract void onLayout(boolean changed, int l, int t, int r, int b); ``` 以LinearLayout為例: ```java // LinearLayout的onLayout方法 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); } } // 以縱向為例 void layoutVertical(int left, int top, int right, int bottom) { int childTop; int childLeft; int width = right - left; int childRight = width - mPaddingRight; int childSpace = width - paddingLeft - mPaddingRight; for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child.getVisibility() != GONE) { // 這里使用了childView的測量寬、高的值 final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); //...中間省略部分設置childLeft、childTop的代碼 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); //... } } } // 調用childView的layout方法進行位置設定 private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); } ``` ## 整體總結【重點關注】 1、View的layout方法中,調用setFrame方法為View自己指定位置,調用onLayout方法為所有的childView指定位置 2、setFrame方法為mLeft、mTop、mRight、mBottom幾個屬性賦值,來確定View自己相對于父布局的位置 3、ViewGroup的onLayout方法是一個抽象方法,子類需要實現此方法,并在實現中根據ViewGroup的布局邏輯計算出每個childView的位置,然后調用childView的layout方法進行子View布局 # draw 1、View的draw方法 View的draw方法有兩個實現,一個參數的draw方法是被三個參數的方法調用的,三個參數的方法是被父布局ViewGroup的drawChild方法調用的。 ```java boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {} public void draw(Canvas canvas) {} ``` ```java // 由ViewGroup的drawChild方法調用 boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { //... if (!drawingWithDrawingCache) { if (drawingWithRenderNode) { //... } else { //... draw(canvas); } } } ``` 一個參數的draw方法源碼如下: ```java public void draw(Canvas canvas) { // 繪制背景 if (!dirtyOpaque) { drawBackground(canvas); } if (!verticalEdges && !horizontalEdges) { // 繪制View內容 if (!dirtyOpaque) onDraw(canvas); // 繪制childView dispatchDraw(canvas); //... // 繪制裝飾(前景內容) onDrawForeground(canvas); // 繪制默認焦點高亮顯示 drawDefaultFocusHighlight(canvas); return; } // 我們通過重寫onDraw方法,拿到Canvas來繪制我們想要繪制的內容。 protected void onDraw(Canvas canvas) {} ``` View的繪制過程有如下幾步: * 繪制背景 * 繪制View內容 * 繪制childView * 繪制前景 * 繪制默認焦點高亮顯示 View中的dispatchDraw方法是一個空方法,ViewGroup中有相應的實現。 2、ViewGroup繪制childView ```java // 這個方法用于繪制childView protected void dispatchDraw(Canvas canvas) { for (int i = 0; i < childrenCount; i++) { while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) { //... more |= drawChild(canvas, transientChild, drawingTime); } if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } ``` 在dispatchDraw方法中循環調用了drawChild方法,也就是調用了每個childView的draw方法,將childView繪制出來: ```java protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); } ``` ## 整體總結【重點關注】 1、View的draw方法按流程分別繪制背景、content、childView、前景、默認焦點等。其中onDraw方法繪制View的內容。 2、如果是ViewGroup,會調用dispatchDraw方法繪制childView。 3、ViewGroup的dispatchDraw方法,會調用所有的childView的draw方法進行繪制子View。 4、可通過重寫View的onDraw方法,拿到Canvas來繪制想要繪制的內容。 # 參考文檔 [Android 視圖繪制流程完全解析,帶你一步步深入了解 View 系列文章](http://blog.csdn.net/guolin_blog/article/details/16330267) [Android 開發藝術探索](https://book.douban.com/subject/26599538/)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看