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()中做的事其實都差不多:

  1. 遍歷子View
  2. 調用measureChild*讓子View確定自己的大小
  3. 根據所有子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流程分為四步:

  1. 繪製背景
  2. 繪製自身內容(onDraw)
  3. 遍歷子View,調用其draw方法把繪製過程分發下去(dispatchDraw)
  4. 繪製裝飾(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注意事項

  1. 讓View支持wrap_conent
  2. 讓View支持padding
  3. 盡量避免使用Handler,一般都可以用View自帶的post方法代替
  4. 在onDeatchFromWindow時,停止View的動畫或線程(如果有的話)
  5. 如果存在嵌套滑動,處理好滑動衝突

總結

沒有總結。。以上只是看完《藝術探索》用自己的理解寫讀書筆記加強記憶而已。各路大神總結的很清楚,需要時查閱他們博客就行了,比如要點提煉|開發藝術之View。


推薦閱讀:
相关文章