Android View的繪製流程知識點總結
Android View的繪製流程知識點總結
前言
本文屬於《Android開發藝術探索》(以下簡稱「藝術探索」)第四章——View的工作原理讀書筆記。
同樣,還是博客園那位博主的文章:Android View繪製13問13答。他的 Andoird N問N答 系列文章總結的非常好。同時,在第三章讀書筆記中提到的CSDN博主廢墟的樹,這篇從ViewRootImpl類分析View繪製的流程,把繪製的三大流程講的非常清楚。感謝前輩們的分享。
1.繪製流程從哪裡開始
以Activity為例,下圖是相關方法的調用流程:
可以看到,最終performTraversals()方法觸發了View的繪製。該方法內部,依次調用了performMeasure(),performLayout(),performDraw(),將View的measure,layout,draw過程,從頂層View分發了下去。
上面體現了Activity中View的繪製過程是如何被觸發的,其實通過閱讀《藝術探索》第8章——理解Window和WindowManager,可以知道,Dialog,PopupWindow中View的繪製過程也是一樣的,只是觸發的方式不同。例如Dialog中,是調用dialog.show()時,觸發了WindowManagerImpl的addView()(上圖步驟2),後面的流程就一樣了。
2.繪製流程第一步——measure
什麼是MeasureSpec
MeasureSpec是一個specSize和specMode信息的32位int值,其中高兩位表示specMode,低30位表示specSize。specMode指測量模式,specSize指在某種測量模式下的規格大小。specMode包括:
- UNSPECIFIED
- EXACTLY
- AT_MOST
View需要MeasureSpec信息,來確定自己能顯示多大。頂層View的MeasureSpec由ViewRootImpl#getRootMeasureSpec()方法獲得,子View的MeasureSpec,由父容器和其自身的LayoutParams共同決定,通過ViewGroup提供的getChildMeasureSpec()方法獲得。
getRootMeasureSpec()源碼如下:
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window cant 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; }
getChildMeasureSpec()方法較長,《藝術探索》中的一圖以表格形式呈現了該方法:
View的measure過程
父容器調用子View的measure方法把上一步獲得的MeasureSpec信息傳遞過去,子View的measure方法調用View#onMeasure(),onMeasure調用setMeasuredDimension()設置自身大小。該過程如圖:
getSuggestedMinimumHeight(),getSuggestedMinimumWidth():
protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); }protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
作用一目了然。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; }
由前面的分析可知,當View的寬高屬性為wrap_content時,其父View通過getChildMeasureSpec方法確定的其測量模式為AT_MOST。再由getDefaultSize()可知,其最終的寬高會被設置為specSize,即父View所剩空間的大小,這也就是為什麼自定義View不對AT_MOST模式做處理,其wrap_content和match_parent效果一樣。
ViewGroup的measure過程
ViewGroup的measure()和onMeasure()方法是從View繼承過來的,沒有做任何重寫(measure方法是final限定,不能重寫)。其onMeasure()方法由各個子類各自重寫,實現自己的需求。ViewGroup的子類在onMeasure()中做的事其實都差不多:
- 遍歷子View
- 調用measureChild*讓子View確定自己的大小
- 根據所有子View的大小確定自己的大小
2步驟中*是個通配符,意思是measureChild一類的方法,如measureChildHorizontal,measureChildWithMargins。這些方法內部會調用getChildMeasureSpec確定子View的測量模式,會調用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)觸發子View的測量。
3.繪製流程第二步——layout
View源碼中,layout方法中會先調用setFrame給自身的left,top,right,bottom屬性賦值,至此自己在父View中的位置就確定了。然後會調用onLayout方法,該方法在View中是一個空實現,具體的實現由其子View(一般指ViewGroup)實現,如LinearLayout。
4.繪製流程第三步——draw
繪製流程經過ViewRootImpl中的performMeasure(),performLayout(),現在到了perfromDraw()。performDraw經過如下圖所示的方法調用,觸發了頂層View的draw方法:
drawSoftware()方法內部調用了mView.draw(canvas),mView就是「1.繪製流程從哪裡開始」第4步setView()所設置的根View。View的draw方法源碼中的注釋具有參考價值,如下:
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 views 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); // 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); // were done... return; }}
可以看到一般情況下View的draw流程分為四步:
- 繪製背景
- 繪製自身內容(onDraw)
- 遍歷子View,調用其draw方法把繪製過程分發下去(dispatchDraw)
- 繪製裝飾(onDrawForeground)
其中步驟二在自定義View的時候經常需要去實現,以繪製自己想要的效果。步驟三dispatchDraw在View中是個空實現。ViewGroup實現了dispatchDraw(),其中調用了ViewGroup#drawChild方法,而drawChild()僅僅是調用了child.draw():
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
自定義View
自定義View的幾種方式
1.繼承自View,重寫onDraw方法
場景:顯示的內容需要高度定製,如圖表
2.繼承自ViewGroup
場景:流式布局等,對於內容布局由特殊要求的。通過重寫onLayout方法達到目的。
3.繼承自特定的View(如TextView,ImageView)
場景:需要擴展系統控制項功能,如圓角ImageView
4.繼承自特定的ViewGroup(如LinearLayout)
場景:不需要自定義布局方式,只需要將常用的View組合起來,比如app設置界面,常常每一個item都是左一個圖標,中間一行文字,右邊一個箭頭,這是就可以繼承自LinearLayout,默認水平方向布局,暴露出setIcon,setText等自定義的方法設置圖標,文字即可。
自定義View注意事項
- 讓View支持wrap_conent
- 讓View支持padding
- 盡量避免使用Handler,一般都可以用View自帶的post方法代替
- 在onDeatchFromWindow時,停止View的動畫或線程(如果有的話)
- 如果存在嵌套滑動,處理好滑動衝突
總結
沒有總結。。以上只是看完《藝術探索》用自己的理解寫讀書筆記加強記憶而已。各路大神總結的很清楚,需要時查閱他們博客就行了,比如要點提煉|開發藝術之View。
推薦閱讀: