深入淺出Android系列之從ViewToBitmap延伸到View的繪製全過程
前言:
最近遇到的一個 Bug , 當我們應用將 View 生成圖片後 , 佈局會出現佈局錯亂的現象, 檢查程式碼發現 , 之前的工程師的 ui和程式碼邏輯是沒什麼問題的 , 那問題應該就是出在生成圖片的操作上面,先來看看程式碼
view.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) view.layout(0, 0, view.measuredWidth, view.measuredHeight) val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) view.draw(canvas)
(生成圖片的程式碼)
整個程式碼的邏輯很簡單,執行了一遍 view 的 measure() 、 layout () 、 draw() , 然後在 draw 中放入我們自己的畫布 , 最後生成 bitmap 。
經過測試 , 問題就出現在measure() 、 layout() 上 , 先後執行這兩個方法 , 導致佈局的寬度和高度出現問題 , 簡單來說 , 原來一個 TextView 的寬度是 match_conten , 當我們呼叫完這兩個方法後就變成了 TextView 文字的寬度 , 而這個時候如果有其他佈局依賴於這個 TextView 的位置就會導致佈局出現問題
這個時候就在想 , 這三個方法其實就是 View 的繪製過程啊 , 但是在我們執行之前 , View 生成時應該是已經執行過一次的 , 所以我把 measure() 、 layout () 註釋掉 , 單純執行 draw () , 發現程式碼是可以正常執行的 , 同時佈局沒有出現問題 。
我又試了註釋一個 measure() , 執行另外兩個方法 , 發現也是正常執行 , 註釋 layout () , 執行另外兩個方法 , 發現執行也是正常的 , 那麼為什麼先後執行 measure() 、 layout () 佈局就會有問題呢 ?
我開始思考View的繪製過程
從DecorView 被載入到 Window 開始 :
我們都知道, PhoneWindow 是 Android 系統中最基本的視窗系統,每個 Activity 會建立一個。同時, PhoneWindow 也是 Activity 和 View 系統互動的介面。 DecorView 本質上是一個 FrameLayout ,是 Activity 中所有 View 的祖先。
從 Activity 的 startActivity 開始,最終呼叫到 ActivityThread 的 handleLaunchActivity 方法來建立 Activity ,相關核心程式碼如下:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) { .... // 建立Activity,會呼叫Activity的onCreate方法 // 從而完成DecorView的建立 Activity a = performLaunchActivity(r, customIntent); if (a != null) { r.createdConfig = new Configuration(mConfiguration); Bundle oldState = r.state; handleResumeActivity(r.tolen, false, r.isForward, !r.activity..mFinished && !r.startsNotResumed); } } final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); mSomeActivitiesChanged = true; // 呼叫Activity的onResume方法 ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; ... if (r.window == null &&& !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); // 得到DecorView View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); // 得到了WindowManager,WindowManager是一個介面 // 並且繼承了介面ViewManager ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; // WindowManager的實現類是WindowManagerImpl, // 所以實際呼叫的是WindowManagerImpl的addView方法 wm.addView(decor, l); } } } }
在這裡面出現了兩個概念 ViewRoot 和 DecorView, 應該簡單的提兩句 , 因為 View 的三大流程均是透過 ViewRoot 來完成的 。ViewRoot 對應於 ViewRootImpl 類,它是連線 WindowManager 和 DecorView 的紐帶。 當Activity 物件被建立完畢後,會將 DecorView 新增到 Window 中,同時會建立 ViewRootImpl 物件,並將 ViewRootImpl 物件和 DecorView 建立關聯。
View 繪製的流程 :
在 Android 中 , View 的繪製主要是 3 大流程 , 分別是 measure , layout , draw , 分別對應著測量 , 位置 , 和繪製 。
同時,在Android 中,主要有兩種檢視: View 和 ViewGroup , View 就是一個獨立的檢視, ViewGroup 是 一個容器元件,該容器可容納多個子檢視,即ViewGroup 可容納多個 View 或 ViewGroup ,且支援巢狀。 雖然ViewGroup 繼承於View ,但是在 View 繪製三大流程中,View 和ViewGroup 它們之間的操作並不完全相同,比如:
-
View 和 ViewGroup 都需要進行 measure ,確定各自的測量寬 / 高。 View 只需直接測量自身即可,而 ViewGroup 通常都必須先測量所有子 View ,最後才能測量自己
-
通常ViewGroup 先定位自己的位置( layout ),然後再定位其子 View 位置( onLayout )
-
View 需要進行 draw 過程,而 ViewGroup 通常不需要(當然也可以進行繪製),因為 ViewGroup 更多作為容器存在,起儲存放置功能
measure:
對於 View 的測量主要分為兩個步驟
-
求取 View 的測量規格 MeasureSpec 。
-
依據上一步求得的MeasureSpec ,對 View 進行測量,求取得到 View 的最終測量寬 / 高。
那什麼是 MeasureSpec呢?MeasureSpec 表示的是一個32 位的整形值,它的高2 位表示測量模式SpecMode ,低30 位表示某種測量模式下的規格大小SpecSize
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0X3 << MODE_SHIFT; // 不指定測量模式, 父檢視沒有限制子檢視的大小,子檢視可以是想要 // 的任何尺寸,通常用於系統內部,應用開發中很少用到。 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 精確測量模式,檢視寬高指定為match_parent或具體數值時生效, // 表示父檢視已經決定了子檢視的精確大小,這種模式下View的測量 // 值就是SpecSize的值。 public static final int EXACTLY = 1 << MODE_SHIFT; // 最大值測量模式,當檢視的寬高指定為wrap_content時生效,此時 // 子檢視的尺寸可以是不超過父檢視允許的最大尺寸的任何尺寸。 public static final int AT_MOST = 2 << MODE_SHIFT; // 根據指定的大小和模式建立一個MeasureSpec public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } // 微調某個MeasureSpec的大小 static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return make MeasureSpec(0, UNSPECIFIED); } int size = getSize(measureSpec) + delta; if (size < 0) { size = 0; } return makeMeasureSpec(size, mode); } }
( MeasureSpec核心程式碼)
一個MeasureSpec 表達的是:該 View 在該種測量模式( SpecMode )下對應的測量尺寸( SpecSize )。其中, SpecMode 有三種型別:
-
UNSPECIFIED :表示父容器對子 View 未施加任何限制,子 View 尺寸想多大就多大。
-
EXACTLY :如果子 View 的模式為 EXACTLY ,則表示子 View 已設定了確切的測量尺寸,或者父容器已檢測出子 View 所需要的確切大小。 這種模式對應於LayoutParams.MATCH_PARENT 和子View 設定具體數值兩種情況。
-
AT_MOST :表示自適應內容,在該種模式下, View 的最大尺寸不能超過父容器的 SpecSize ,因此也稱這種模式為 最大值模式。 這種模式對應於LayoutParams.WRAP_CONTENT 。
Measure 的基本流程 :
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // ViewGroup沒有定義測量的具體過程,因為ViewGroup是一個 // 抽象類,其測量過程的onMeasure方法需要各個子類去實現 onMeasure(widthMeasureSpec, heightMeasureSpec); ... } // 不同的ViewGroup子類有不同的佈局特性,這導致它們的測量細節各不相同,如果需要自定義測量過程,則子類可以重寫這個方法 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // setMeasureDimension方法用於設定View的測量寬高 setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } // 如果View沒有重寫onMeasure方法,則會預設呼叫getDefaultSize來獲得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 = sepcSize; break; } return result; }
(View . measure()原始碼 )
View.measure(int, int) 中引數 widthMeasureSpec 和 heightMeasureSpec 是由父容器傳遞進來的,具體的測量過程請參考後文內容。
需要注意的是,View.measure(int, int) 是一個 final 方法,因此其不可被覆寫,實際真正測量 View 自身使用的是 View.onMeasure(int, int) 方法 .
在 onMeasure 中主要做了三件事
第一件事 : 透過getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 方法獲取得到 View 的推薦最小測量寬 / 高
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); } protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }
(View.java)
可以很清晰看出,當 View 沒有設定背景時,它的寬度就為 mMinWidth , mMinWidth 就是 android:minWidth 這個屬性對應設定的值(未設定 android:minWidth 時,其值預設為 0 ),當 View 設定了背景時,它的寬度就是 mMinWidth 和 mBackground.getMinimumWidth() 之中的較大值
getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 其實就是用於獲取 View 的最小測量寬 / 高,其具體邏輯為:當 View 沒有設定背景時,其最小寬 / 高為 android:minWidth/android:mMinHeight 所指定的值,當 View 設定了背景時,其最小測量寬 / 高為 android:minWidth/android:minHeight 與其背景圖片寬 / 高的較大值。
簡而言之,View 的最小測量寬 / 高為 android:minWidth/android:minHeight 和其背景寬 / 高之間的較大值。
第二件事 : 透過getDefaultSize() 獲取到 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; }
(getDefaultSize()原始碼)
這裡的size 是透過 getSuggestedMinimumWidth()/getSuggestedMinimumHeight() 方法獲取得到系統建議 View 的最小測量寬 / 高。
引數measureSpec 是經由 View.measure()==>View.onMeasure()==>View.getDefaultSize() 呼叫鏈傳遞進來的,表示的是當前 View 的 MeasureSpec 。
getDefaultSize() 內部首先會獲取 View 的測量模式和測量大小,然後當 View 的測量模式為 UNSPECIFIED 時,也即未限制 View 的大小,因此此時 View 的大小就是其原生大小(也即 android:minWidth 或背景圖片大小),當 View 的測量模式為 AT_MOST 或 EXACTLY 時,此時不對這兩種模式進行區分,一律將 View 的大小設定為測量大小(即 SpecSize ) 。
第三件事 : 獲取到 View 的測量寬 / 高後,透過 setMeasuredDimension() 記錄 View 的測量寬 / 高:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { ... setMeasuredDimensionRaw(measuredWidth, measuredHeight); } // 記錄測量寬/高 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }
(setMeasuredDimension()原始碼)
無論是對View 的測量還是 ViewGroup 的測量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法負責,然後真正執行 View 測量的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。
具體來說,View 直接在 onMeasure() 中測量並設定自己的最終測量寬 / 高。在預設測量情況下, View 的測量寬 / 高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同決定,當 View 自身的測量模式為 LayoutParams.UNSPECIFIED 時,其測量寬 / 高為 android:minWidth/android:minHeight 和其背景寬 / 高之間的較大值,其餘情況皆為自身 MeasureSpec 指定的測量尺寸。
而對於ViewGroup 來說,由於佈局特性的豐富性,只能自己手動覆寫 onMeasure() 方法,實現自定義測量過程,但是總的思想都是先測量 子 View 大小,最終才能確定自己的測量大小。
layout:
當確定了 View 的測量大小後,接下來就可以來確定 View 的佈局位置了,也即將 View 放置到螢幕具體哪個位置。 這也是 layout 乾的事情 。
View.layout() 主要就做了兩件事:
-
setFrame() :首先透過 View.setFrame() 來確定自己的佈局位置
-
onLayout() : setFrame() 是用於確定 View 自身的佈局位置,而 onLayout() 主要用於確定 子 View 的佈局位置
protected boolean setFrame(int left, int top, int right, int bottom) { ... // Invalidate our old position invalidate(sizeChanged); mLeft = left; mTop = top; mRight = right; mBottom = bottom; }
(setFrame()原始碼 )
draw :
當 View 的測量大小,佈局位置都確定後,就可以最終將該 View 繪製到螢幕上了。 這也就是 draw 乾的事情 。
public void draw(Canvas canvas) { ... /* * 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 drawBackground(canvas); // skip step 2 & 5 if possible (common case) ... // Step 2, save the canvas' layers if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } ... // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers ... if (drawTop) { ... canvas.drawRect(left, top, right, top + length, p); } if (drawBottom) { ... canvas.drawRect(left, bottom - length, right, bottom, p); } if (drawLeft) { ... canvas.drawRect(left, top, left + length, bottom, p); } if (drawRight) { ... canvas.drawRect(right - length, top, right, bottom, p); } ... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
(View.draw()原始碼)
View.draw() 主要做了以下 6 件事:
-
繪製背景:drawBackground()
-
如果有必要的話,儲存畫布圖層:Canvas.saveLayer()
-
繪製自己:onDraw()
-
繪製子View : dispatchDraw()
-
如果有必要的話,繪製淡化效果並恢復圖層:Canvas.drawRect()
-
繪製裝飾:onDrawForeground()
總結:
View 的繪製主要 三大流程 :
measure :測量流程,主要負責對 View 進行測量,其核心邏輯位於 View.measure() ,真正的測量處理由 View.onMeasure() 負責。預設的測量規則為:如果 View 的佈局引數為 LayoutParams.WRAP_CONTENT 或 LayoutParams.MATCH_PARENT ,那麼其測量大小為 SpecSize ;如果其佈局引數為 LayoutParams.UNSPECIFIED ,那麼其測量大小為 android:minWidth/android:minHeight 和其背景之間的較大值。
自定義View 通常覆寫 onMeasure() 方法,在其內一般會對 WRAP_CONTENT 預設一個預設值,區分 WARP_CONTENT 和 MATCH_PARENT 效果,最終完成自己的測量寬 / 高。而 ViewGroup 在 onMeasure() 方法中,通常都是先測量子 View ,收集到相應資料後,才能最終測量自己。
layout :佈局流程,主要完成對 View 的位置放置,其核心邏輯位於 View.layout() ,該方法內部主要透過 View#setFrame() 記錄自己的四個頂點座標(記錄與對應成員變數中即可),完成自己的位置放置,最後會回撥 View.onLayout() 方法,在其內完成對 子 View 的佈局放置。
( 不同於 measure 流程首先對 子 View 進行測量,最後才測量自己, layout 流程首先是先定位自己的佈局位置,然後才處理放置 子 View 的佈局位置。 )
draw :繪製流程,就是將 View 繪製到螢幕上,其核心邏輯位於 View#draw() ,主要就是對 背景、自身內容( onDraw() )、子 View ( dispatchDraw() )、裝飾(捲軸、前景等) 進行繪製。
( 通常自定義View 覆寫 onDraw() 方法,完成自己的繪製即可, ViewGroup 一般充當容器使用,因此通常無需覆寫 onDraw() )
Activity 的根檢視(即 DecorView )最終是繫結到 ViewRootImpl ,具體是由 ViewRootImpl#setView(...) 進行繫結關聯的,後續 View 繪製的三大流程都是均有 ViewRootImpl 負責執行的。
對 View 的測量流程中,最關鍵的一步是求取 View 的 MeasureSpec , View 的 MeasureSpec 是在其父容器 MeasureSpec 的約束下,結合自己的 LayoutParams 共同測量得到的,具體的測量邏輯由 ViewGroup.getChildMeasureSpec() 負責。
DecorView 的 MeasureSpec 取決於自己的 LayoutParams 和螢幕尺寸,具體的測量邏輯位於 ViewRootImpl.getRootMeasureSpec() 。
用邏輯過一遍流程:
1 、 首先,當 Activity 啟動時,會觸發呼叫到 ActivityThread#handleResumeActivity() ,其內部會經歷一系列過程,生成 DecorView 和 ViewRootImpl 等例項,最後透過 ViewRootImpl#setView(decor,MATCH_PARENT) 設定 Activity 根 View 。
( ViewRootImpl.setView() 內容透過將其成員屬性 ViewRootImpl.mView 指向 DecorView ,完成兩者之間的關聯。)
2 、 ViewRootImpl 成功關聯 DecorView 後,其內部會設定同步屏障併傳送一個 CALLBACK_TRAVERSAL 非同步渲染訊息,在下一次 VSYNC 訊號到來時, CALLBACK_TRAVERSAL 就會得到響應,從而最終觸發執行 ViewRootImpl.performTraversals() ,真正開始執行 View 繪製流程。
3 、 ViewRootImpl.performTraversals() 內部會依次呼叫 ViewRootImpl#performMeasure() 、 ViewRootImpl#performLayout() 和 ViewRootImpl#performDraw() 三大繪製流程,其中:
4 、 performMeasure() :內部主要就是對 DecorView 執行測量流程: DecorView#measure() 。 DecorView 是一個 FrameLayout ,其佈局特性是層疊佈局,所佔的空間就是其 子 View 佔比最大的寬 / 高,因此其測量邏輯( onMeasure() )是先對所有 子 View 進行測量,具體是透過 ViewGroup.measureChildWithMargins(...) 方法對 子 View 進行測量,子 View 測量完成後,記錄最大的寬 / 高,設定為自己的測量大小(透過 View#setMeasuredDimension() ),如此便完成了 DecorView 的測量流程。
5 、 performLayout() :內部其實就是呼叫 DecorView.layout() ,如此便完成了 DecorView 的佈局位置,最後會回撥 DecorView.onLayout() ,負責 子 View 的佈局放置,核心邏輯就是計算出各個 子 View 的座標位置,最後透過 child.layout() 完成 子 View 佈局。
6、 performDraw() :內部最終呼叫到的是 DecorView.draw() ,該方法內部並未對繪製流程做任何修改,因此最終執行的是 View.draw() ,所以主要就是依次完成對 DecorView 的 背景、子 View ( dispatchDraw() ) 和 檢視裝飾(捲軸、前景等) 的繪製
回到開頭 Bug , 其原因就是在重新呼叫 measure 後對佈局的位置進行重新測量 ,因為子View已經有了真實的寬度,所以子View的寬度就沿用了真實寬度,又 呼叫了 layout 重新進行佈局導致後續有依賴到這個子View的佈局都出現了問題
解決方案有 2 個
1、 只呼叫 draw () 方法將當前佈局繪製到畫布上即可
2、 使用 view .drawToBitmap() 方法也可以直接生成 view 的圖片
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69917874/viewspace-2997821/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android View的繪製過程AndroidView
- 從繪製時機深入淺出View.postView
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- View 的繪製過程View
- Android啟動過程剖析-深入淺出Android
- Android View繪製原理:繪製流程排程、測算等AndroidView
- Android原始碼分析之View繪製流程Android原始碼View
- Android高階進階之路【一】Android中View繪製流程淺析AndroidView
- View的繪製二:View的繪製流程View
- Android自定義View之Paint繪製文字和線AndroidViewAI
- Python Matplotlib繪製條形圖的全過程Python
- View繪製01-Android渲染系統中的ViewViewAndroid
- Android進階(五)View繪製流程AndroidView
- Android View繪製原始碼分析 MeasureAndroidView原始碼
- 探究Android View 繪製流程,Canvas 的由來AndroidViewCanvas
- Android自定義View之(一)View繪製流程詳解——向原始碼要答案AndroidView原始碼
- 探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕AndroidView
- 深入淺出Tomcat系列Tomcat
- 理解 Android 程式啟動之全過程Android
- Android原始碼完全解析——View的Measure過程Android原始碼View
- 淺談移動端 View 的顯示過程View
- 基於原始碼分析 Android View 繪製機制原始碼AndroidView
- Android View繪製流程看這篇就夠了AndroidView
- 深入淺出MyBatis:MyBatis外掛及開發過程MyBatis
- Android自定義view-自繪ViewAndroidView
- View繪製——畫多大?View
- View 繪製流程分析View
- View繪製——畫在哪?View
- 【Android自定義View】繪圖之文字篇(三)AndroidView繪圖
- 【Android自定義View】繪圖之Path篇(二)AndroidView繪圖
- Android中View的測量和佈局過程AndroidView
- 深入淺出Android BufferQueue-上Android
- Android系統原始碼分析--View繪製流程之-inflateAndroid原始碼View
- Android系統原始碼分析–View繪製流程之-setContentViewAndroid原始碼View
- Android系統原始碼分析--View繪製流程之-setContentViewAndroid原始碼View
- 《Apache RocketMQ 深入淺出》系列文章ApacheMQ
- 【原創】【深入淺出系列】之程式碼可讀性
- View的繪製三:UI繪製的三大步驟ViewUI