本篇文章承接上一篇《深入理解Android:卷三》深入理解控件系統讀書筆記(上),從繪制原理,以及動畫原理方面,繼續深入了解Android的控件系統。
6.4 深入理解控件樹的繪制
6.4.1 理解Canvas
Canvas
的繪圖指令可以分為兩部分:
- 繪制指令。這些最常用的指令由一系列名為
drawXXX()
的方法提供。用來實現實際的繪制行為,例如繪制點、線、圓以及方塊等 - 輔助指令。用于提供輔助功能,會影響后續繪制指令的效果,如設置變換、裁剪區等。同時還提供了
save()
和restore()
用于撤銷一部分輔助指令
1. Canvas的繪制目標
對軟件Canvas
來說,其繪制目標是一個建立在Surface
之上的位圖Bitmap
當通過
Surface.lockCanvas()
方法獲取一個Canvas
時會以Surface
的內存創建一個Bitmap
,通過Canvas
所繪制的內容都會直接反映到Surface
中硬件
Canvas
的繪制目標有兩種。一種是HardwareLayer
,可以將其理解為一個紋理(GL Texture
),或者更簡單地認為它是一個硬件加速下的位圖(Bitmap
)。另一種被稱為DisplayList
,它并不是一塊Buffer
,而是一個指令序列。DisplayList
會將Canvas
的繪制指令編譯并優化為硬件繪制指令,并且可以在需要時將這些指令回放到一個HardwareLayer
上,而不需要重新使用Canvas
進行繪制。Bitmap
、HardwareLayer
以及DisplayList
都可以稱為Canvas
的畫布
從使用角度來說,Bitmap
與HardwareLayer
十分相似。開發者可以將一個Bitmap
通過Canvas
繪制到另一個Bitmap
上,也可以將一個HardwareLayer
繪制到另一個HardwareLayer
上。二者的區別僅在于使用時采用了硬件加速還是軟件加速。
另外,將DisplayList
回放到HardwareLayer
上,與繪制一個Bitmap
或HardwareLayer
的結果并沒有什么不同。只不過DisplayList
并不像Bitmap
那樣存儲了繪制的結果,而是存儲了繪制的過程。
2. 坐標變換
Canvas
提供了配套使用的save()/restore()
方法用以撤銷不需要的變換。它們可以嵌套調用,在這種情況下restore()
將會把坐標系狀態返回到與其配對的save
所創建的保存點。另外,也可以通過保存某個save()
的返回值,并將這個返回值傳遞給restoreToCount()
方法的方式來顯示地指定一個保存點
坐標系的變換,使得控件在onDraw()
方法中使用Canvas
時,使用的是控件自身的坐標系。而這個控件自身的坐標系就是通過Canvas
的變換指令從窗口坐標系沿著控件樹一步一步變化出來的
6.4.2 View.invalidate()與臟區域
當一個控件的內容發生變化而需要重繪的時候,它會通過invalidate()
方法將其需要重繪的區域沿著控件樹提交給ViewRootImpl
,并保存到ViewRootImpl
的mDirty
成員中,最后通過scheduleTraversals()
引發一次遍歷,進而進行重繪。在回溯過程中,會將沿途的控件標記為臟,即設置PFLAG_DIRTY
或PFLAGE_DIRTY_OPAQUE
(不透明)兩者之一添加到View.mPrivateFlags
成員中。如果控件時“實心”的,則將標記設為PFLAGE_DIRTY_OPAQUE
,否則為PFLAGE_DIRTY
。控件系統在重繪過程中區分這兩種標記以決定是否為此控件繪制背景,如果是實心的就會跳過背景的繪制工作從而提高效率。
在一個方法可以連續調用多個控件的invalidate()
方法,而不用擔心會由于多次重繪而產生的效率問題。另外,多次調用invalidate()
方法會使得ViewRootImpl
多次接收到設置臟區域的請求,ViewRootImpl
會將這些臟區域累加到mDirty
中,進而在隨后的“遍歷”中一次性地完成所有臟區域的重繪。
6.4.3 開始繪制
繪制控件樹的入口就在performDraw()
,其工作也很簡單,一是調用draw()
執行實際的繪制工作,二是在必要時,向WMS
通知繪制已經完成。draw()
方法中產生了硬件加速繪制和軟件繪制兩個分支,分支的條件為mAttachInfo.mHardwareRender()
是否存在并且有效。在ViewRootImpl.setView()
中會調用enableHardwareAcceleration()
方法,倘若窗口的LayoutParams.flags
中包含FLAG_HARDWARE_ACCELERATED
標記,這個方法會通過HardwareRenderer.createGlRenderer()
創建一個HardwareRender
并保存在mAttachInfo
中。因此mAttachInfo
所保存的HardwareRenderer
是否存在便成為區分使用硬件加速繪制還是軟件繪制的依據。
6.4.4 軟件繪制原理
軟件繪制由ViewRootImpl.drawSoftware()
完成,主要分為以下4步工作:
- 通過
Surface.lockCanvas()
獲取一個用于繪制的Canvas
- 對
Canvas
進行變換以實現滾動效果 - 通過
mView.draw()
將根控件繪制在Canvas
上 - 通過
Surface.unlockCanvasAndPost()
顯示繪制后的內容
其中,第二步和第三步是控件繪制過程中的兩個基本階段,即首先通過Canvas
的變換指令將Canvas
的坐標系變換到控件自身的坐標系之下,然后再通過控件的View.draw(Canvas)
方法將控件的內容繪制在這個變換后的坐標系中。
注意,在View
中還有draw(Canvas)
的另一個重載,即View.draw(ViewGroup,Canvas,long)
。后者是在父控件的繪制過程中所調用的(參數ViewGroup
是其父控件),并且參數Canvas
所在的坐標系為其父控件的坐標系。View.draw(ViewGroup,Canvas,long)
會根據控件的位置、旋轉、縮放以及動畫對Canvas
進行坐標系的變換,是的Canvas
的坐標系從父控件的坐標系變化到本控件的坐標系,并且會在變化完成后調用draw(Canvas)
來在變換后的坐標系中進行繪制。當然,該重載方法除了坐標系變換,還包括了硬件加速、繪圖緩存以及動畫計算等工作。
1. 純粹的繪制:View.draw(Canvas)
純粹的繪制主要涉及以下4步:
- 繪制背景,注意背景不會受到滾動的影響
- 調用
onDraw()
方法繪制控件自身的內容 - 通過調用
dispatchDraw()
繪制子控件 - 繪制特殊的裝飾,即滾動條
2. 確定子控件的繪制順序:dispatchDraw()
繪制順序依次執行以下4步:
- 設置裁剪區域。默認情況下,
ViewGroup
通過Canvas.clipRect()
方法將子控件的繪制限制在自身的區域內。超出區域將會被裁剪。是否需要進行越界內容的裁剪取決于ViewGroup.mGroupFlags
中是否包含CLIP_TO_PADDING_MASK
標記。開發者可通過ViewGroup.setClipToPadding()
方法修改這一行為,使得子控件超出的內容仍得以顯示 - 遍歷繪制所有的子控件,根據
mGroupFlags
中是否存在FLAG_USE_CHILD_DRAWING_ORDER
標記存在兩種不同的情況:- 默認情況下,
dispatchDraw()
會按照mChildren
列表的索引順序進行繪制。 - 倘若存在
FLAG_USE_CHILD_DRAWING_ORDER
標記,則表示此ViewGroup
希望按照其自定義的繪制順序進行繪制。自定義的繪制順序由getChildDrawingOrder()
方法實現
- 默認情況下,
- 在每次遍歷中,調用
drawChild()
方法繪制一個子控件。該方法僅僅是調用子控件的View.draw(ViewGroup,Canvas,long)
- 通過
Canvas.restoreToCount()
撤銷之前所做的裁剪設置
有關裁剪的使用,可參照TabWidget
的實現來加深理解。
3. 變化坐標系:View.draw(ViewGroup,Canvas,long)
該方法的工作流程可參見以下幾步:
-
進行動畫的計算,并將結果存儲在
transformToApply
中,這是進行坐標系變換的第一個因素 -
計算控件內容的滾動量。向
Scroller
設置一個目標的滾動量,以及滾動動畫的持續時間,scroller
會自動計算在動畫國成中本次繪制所需的滾動量。這是進行坐標系變換的第二個因素 - 使用
Canvas.save()
保存Canvas
的當前狀態。此時Canvas
的坐標系為父控件的坐標系。在隨后將Canvas
變換到此空間的坐標系并完成繪制后,會通過Canvas.restoreTo()
將Canvas
重置到此時的狀態,以便Canvas
可以繼續用來繪制父控件的下一個子控件 - 第一次變換,對應控件位置與滾動量。最先處理的是子控件位置
mLeft/mTop
,以及滾動量。子控件的位置mLeft/mTop
是進行坐標變換的第三個因素 - 將動畫產生的變換矩陣應用到
Canvas
中。canvas.concat(transformToApply.getMatrix())
,主要是各種Animation
,如SacleAnimation
等 - 將控件自身的變換矩陣應用到
Canvas
中,canvas.concat(getMatrix())
。如setScaleX/Y()
,setTranslationXY()
等產生的變換??丶陨淼淖儞Q矩陣是進行坐標系變換的第四個因素 - 設置剪裁。當父控件的
mGroupFlags
包含FLAG_CLIP_CHILDREN
時,子控件在繪制之前必須通過canvas.clipRect()
設置裁剪區域。注意要和dispatchDraw()
中的裁剪工作區分:dispatchDraw()
中的裁剪是為了保證所有的子控件繪制的內容不得越過ViewGroup
的邊界。其設置由setClipToPadding()
方法完成。而FLAG_CLIP_CHILDREN
則表示所有子控件的繪制內容不得超出子控件自身的邊界,由setClipChildren()
方法啟用或禁用 - 使用變換過的
Canvas
進行最終繪制,調用dispatchDraw()
和draw(Canvas)
兩個方法 - 恢復
Canvas
的狀態到一切開始之前,canvas.restoreToCount(restoreTo)
。這樣Canvas
又回到了父控件的坐標系,使得父控件的dispatchDraw()
便可以將這個Canvas
交給下一個子控件的draw(ViewGroup, Canvas, long)
方法
4. 以軟件方式繪制控件樹的完成流程
6.4.5 硬件加速繪制的原理
1. 硬件加速繪制簡介
倘若窗口使用硬件加速,則ViewRootImpl
會創建一個HardwareRenderer
并保存在mAttachInfo
中。HardwareRenderer
是用于硬件加速的渲染器,它封裝了硬件加速的圖形庫,并以Android
與硬件加速圖形庫的中間層的身份存在。它負責從Android
的Surface
生成一個HardwareLayer
,供硬件加速圖形庫作為繪制的輸出目標,并提供一系列工廠方法用于創建硬件加速繪制過程中所需的DisplayList
、HardwareLayer
、HardwareCanvas
等工具。
2. 硬件加速繪制的入口HardwareRenderer.draw()
從drawSoftware()
的4個主要工作作為對比來分析該實現:
- 獲取
Canvas
。不同于軟件繪制時用Surface.lockCanvas()
新建一個Canvas
,HardwareRenderer
在HardwareRenderer
創建之初便已被創建并綁定在由Surface
創建的EGLSurface
上 - 對
Canvas
進行變換以實現滾動效果。由于硬件繪制的過程位于HardwareRenderer
內部,因此ViewRootImpl
需要在onHardwarePreDraw()
回調中完成這個操作 - 繪制控件內容。這是硬件繪制和軟件繪制的根本區別。軟件繪制時通過
View.draw()
以遞歸的方式將整個控件樹用給定的Canvas
直接繪制Surface
上。而硬件加速繪制則先通過View.getDisplayList()
獲取根控件的DisplayList
中包含了已編譯過的用于繪制整個控件樹的繪圖指令。如果說軟件繪制是直接繪制,那么硬件繪制則是通過DisplayList
間接繪制 - 將繪制結果顯示出來。硬件加速繪制通過
sEgl.swapBuffers()
將繪制內容顯示出來。本質和Surface.unlockCanvasAndPost()
方法一致,都是通過ANativeWindow::queueBuffer
將繪制內容發布給SurfaceFlinger
3. DisplayList的創建與渲染
總體來看,硬件加速繪制過程中的View.getDisplayList()
與HardwareCanvas.drawDisplayList()
的組合相當于軟件繪制過程中的View.draw()
.
·View.getDisplayList()·的實現體現了DisplayList
的使用方法。DisplayList
渲染與Surface
的繪制十分相似,分為如下三個步驟:
- 通過
DisplayList.start()
創建一個HardwareCanvas
并準備好開始錄制繪圖指令 - 使用
HardwareCanvas
進行與Canvas
一樣的變換與繪制操作 - 通過
DisplayList.end()
完成錄制并回收HardwareCanvas
可見DisplayList
的渲染也是使用我們熟悉的View.draw()
方法完成的,而且View.draw()
方法的實現在硬件加速和軟件繪制下完全一樣。但仍體現了另一個重要區別:軟件繪制的整個過程都是用了來自Surface.lockCanvas()
的同一個Canvas
;而硬件加速時,控件使用由自己的DisplayList
所產生的Canvas
進行繪制,此時每個控件onDraw()
的Canvas
參數各不相同。另外,getDisplayList()
中進行了滾動量的變換,因此在硬件加速繪制的情況下,View.draw(ViewGroup, Canvas,long)
方法不需要進行滾動量變換
4. 硬件加速繪制下的子控件繪制
軟件繪制時的流程:View.draw(Canvas)
(自身)->dispatchDraw()
->View.draw(ViewGroup, Canvas, long)
-> View.draw(Canvas)
(子控件)
硬件加速繪制與軟件繪制在前兩步完全相同,區別在于第三步,即在View.draw(ViewGroup, Canvas, long)
,兩者幾乎完全不同。其根本原因在于硬件加速繪制希望在Canvas
上繪制子控件的DisplayList
,而不是使用View.onDraw()
直接繪制,總結兩者不同之處如下:
- 變化因素的應用方法不同。軟件繪制時通過
Canvas
的變換操作將坐標系變換到子控件自身坐標系,而硬件加速繪制時Canvas
的坐標系仍保持在父控件的坐標系下,然后通過DisplayList
的相關方法將變換因素設置給DisplayList
,HardwareCanvas.drawDisplayList()
會按照這些變換因素再以這些變換繪制(準確地說是回放)DisplayList
- 繪制方法不同。軟件繪制時可以說是直接繪制。硬件加速繪制時使用的是
View.getDisplayList()
與HardwareCanvas.drawDisplayList()
的組合進行間接繪制
5. 硬件加速繪制總結
6.4.6 使用繪圖緩存
繪圖緩存是指一個Bitmap
或一個HardwareLayer
,它保存了控件及其子控件的一個快照。繪圖緩存有兩種類型,即軟件緩存(Bitmap
)和硬件緩存(HardwareLayer
),開發者可以通過View.setLayerType()
為LAYER_TYPE_SOFTWARE
和LAYER_TYPE_HARDWARE
決定此控件使用哪種類型的緩存。默認情況下,控件的緩存類型為LAYER_TYPE_NONE
。但值得注意的是,由于硬件緩存依賴于HardwareCanvas
,所以在軟件繪制的情況下,緩存類型被設置為LAYER_TYPE_HARDWARE
的控件仍然會選擇使用軟件緩存。而在硬件加速繪制的情況下,可以在硬件緩存和軟件緩存中任選其一。另外,View.setLayerType()
可以通過傳入Paint
類型的參數用于實現一些顯示效果,如透明度、Xfermode
以及ColorFilter
等
1.軟件繪制下的軟件緩存
使用軟件緩存進行繪制時使用View.buildDrawingCache()/getDrawingCache()
與canvas.drawBitmap()
的組合替代無緩存模式下的View.draw(Canvas)
。這種模式和硬件加速繪制時的處理如出一轍,View.buildDrawingCache()
的實現方式與View.getDisplayList()
方法幾乎完全一致,只不過它的目標是一個Bitmap
而不是DisplayList
。而View.getDrawingCache()
則返回mDrawingCache
或mUnscaleDrawingCache
,前者會根據兼容模式進行放大或縮小,用于做繪制時的軟件緩存,因為繪制到窗口時需要根據兼容模式進行縮放。而后者反映了控件的真實尺寸,往往被用作控件截圖等用途
2. 硬件加速繪制下的繪圖緩存
繪圖緩存的實現位于View.getDisplayList()
,如果將DisplayList
理解為一種緩存,那么硬件加速繪制下的繪圖緩存則是在DisplayList
上建立的另一級緩存,即二級繪圖緩存。
硬件加速時,使用軟件緩存的方式與軟件繪制的流程一樣,只是繪制到DisplayList
上。而使用硬件緩存時,HardwareLayer
就是我們所說的硬件緩存,其處理在View.getHardwareLayer()
上
3. 繪圖緩存的利弊
使用繪圖緩存的原則:
- 不要為十分輕量級的控件啟用繪圖緩存。因為緩存繪制的開銷可能大于控件重繪開銷
- 為很少發生內容改變的控件啟用繪圖緩存。因為啟用繪圖緩存的控件在
invalidate()
時會產生額外的緩存繪制繪制操作 - 當父控件要頻繁改變子控件的位置或變換時對其子控件啟用繪圖緩存。這會避免頻繁地重繪子控件
6.4.7 控件動畫
控件系統存在三種方式實現控件的動畫。
1.ValueAnimator
,ObjectAnimator
,ViewPropertyAimator
。當動畫運行時,ValueAnimator
會將AnimationHandler
不斷地拋給Choreographer
,并在VSYNC
事件到來時修改指定的控件屬性,控件屬性的變化引發invalidate()
操作進而進行重繪。
2.LayoutTransition
,用于ViewGroup
中。
3.View.startAnimation()
,與控件繪制內部過程聯系緊密,因此針對此方法展開分析動畫的實現原理
1. 啟動動畫
從startAnimation()
方法啟動的動畫依托于Animation
類的子類。啟動動畫時首先將給定的Animaiton
通過setAnimaiton()
保存到mCurrentAnimaiton
成員中,再通過invalidate()
方法觸發一次重繪
2. 計算動畫變換
既然動畫是以坐標系變換的方式產生效果的,因此動畫計算的代碼位于View.draw(ViewGroup,Canvas,long)
中:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
.........
Transformation transformToApply = null;
//獲取startAnimation()所給予的Animation對象
final Animation a = getAnimation();
if (a != null) {
//通過drawAnimation()方法計算當前時間點的變換(Transformation)。結果保存在parent.mChildTransformation中
more = drawAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
transformToApply = parent.getChildTransformation();
} else {
//在介紹繪制原理時提到,把transformToApply應用到坐標系變換中
.............
}
}
在drawAnimation()
中,通過Animation.getTransformation()
計算當前時間點的變換,并將其保存到父控件的mChildTransformation
成員中,然后在View.draw(ViewGroup, Canvas, long)
方法中將這個變換以坐標系變換的方式應用到Canvas
或者DisplayList
中,從而對最終的繪制結果產生影響。倘若動畫還將繼續,則調用invalidate()
以便在下次VSYNC
時間到來時進行下一幀的計算與繪制
3. 動畫的結束
動畫的結束借由parent.finishAnimatingView()
實現,也就是由父控件完成。交給父控件來完成,是因為在執行動畫將這個控件從父控件中移除時,ViewGroup
會將其從mChildren
中移除,但會同時將其放置到mDisappearingChildren
數組中,并等待動畫結束。由于mDisappearingChildren
中的控件依然會得到繪制,因此在執行了ViewGroup.removeView()
之后,用戶仍然可以看到動畫中的控件,直到動畫結束后才會消失。另外,LayoutTransition
也依賴于這一機制,使得其移出動畫被用戶看到