自定義View Draw過程- 最易懂的自定義View原理系列(4)
阿新 • • 發佈:2019-02-16
前言
- 自定義
View
是Android
開發者必須瞭解的基礎 - 網上有大量關於自定義
View
原理的文章,但存在一些問題:內容不全、思路不清晰、無原始碼分析、簡單問題複雜化 等 - 今天,我將全面總結自定義View原理中的
Draw
過程,我能保證這是市面上的最全面、最清晰、最易懂的
目錄
1. 作用
繪製View
檢視
2. 儲備知識
3. draw過程詳解
類似measure
過程、layout
過程,draw
過程根據View的型別分為2種情況:
接下來,我將詳細分析這2種情況下的draw
過程
3.1 單一View的draw過程
應用場景
在無現成的控制元件View
View
- 如:製作一個支援載入網路圖片的
ImageView
控制元件 - 注:自定義
View
在多數情況下都有替代方案:圖片 / 組合動畫,但二者可能會導致記憶體耗費過大,從而引起記憶體溢位等問題。
- 如:製作一個支援載入網路圖片的
具體使用
繼承自View
、SurfaceView
或 其他View
;不包含子View
原理(步驟)
View
繪製自身(含背景、內容);- 繪製裝飾(滾動指示器、滾動條、和前景)
具體流程
下面我將一個個方法進行詳細分析:draw
過程的入口 = draw()
/**
* 原始碼分析:draw()
* 作用:根據給定的 Canvas 自動渲染 View(包括其所有子 View)。
* 繪製過程:
* 1. 繪製view背景
* 2. 繪製view內容
* 3. 繪製子View
* 4. 繪製裝飾(漸變框,滑動條等等)
* 注:
* a. 在呼叫該方法之前必須要完成 layout 過程
* b. 所有的檢視最終都是呼叫 View 的 draw ()繪製檢視( ViewGroup 沒有複寫此方法)
* c. 在自定義View時,不應該複寫該方法,而是複寫 onDraw(Canvas) 方法進行繪製
* d. 若自定義的檢視確實要複寫該方法,那麼需先呼叫 super.draw(canvas)完成系統的繪製,然後再進行自定義的繪製
*/
public void draw(Canvas canvas) {
...// 僅貼出關鍵程式碼
int saveCount;
// 步驟1: 繪製本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 若有必要,則儲存圖層(還有一個復原圖層)
// 優化技巧:當不需繪製 Layer 時,“儲存圖層“和“復原圖層“這兩步會跳過
// 因此在繪製時,節省 layer 可以提高繪製效率
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步驟2:繪製本身View內容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:預設為空實現,需複寫
// ViewGroup中:需複寫
// 步驟3:繪製子View
// 由於單一View無子View,故View 中:預設為空實現
// ViewGroup中:系統已經複寫好對其子檢視進行繪製我們不需要複寫
dispatchDraw(canvas);
// 步驟4:繪製裝飾,如滑動條、前景色等等
onDrawScrollBars(canvas);
return;
}
...
}
下面,我們繼續分析在draw()
中4個步驟呼叫的drawBackground()
、 onDraw()
、dispatchDraw()
、onDrawScrollBars(canvas)
/**
* 步驟1:drawBackground(canvas)
* 作用:繪製View本身的背景
*/
private void drawBackground(Canvas canvas) {
// 獲取背景 drawable
final Drawable background = mBackground;
if (background == null) {
return;
}
// 根據在 layout 過程中獲取的 View 的位置引數,來設定背景的邊界
setBackgroundBounds();
.....
// 獲取 mScrollX 和 mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
// 若 mScrollX 和 mScrollY 有值,則對 canvas 的座標進行偏移
canvas.translate(scrollX, scrollY);
// 呼叫 Drawable 的 draw 方法繪製背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 步驟2:onDraw(canvas)
* 作用:繪製View本身的內容
* 注:
* a. 由於 View 的內容各不相同,所以該方法是一個空實現
* b. 在自定義繪製過程中,需由子類去實現複寫該方法,從而繪製自身的內容
* c. 謹記:自定義View中 必須 且 只需複寫onDraw()
*/
protected void onDraw(Canvas canvas) {
... // 複寫從而實現繪製邏輯
}
/**
* 步驟3: dispatchDraw(canvas)
* 作用:繪製子View
* 注:由於單一View中無子View,故為空實現
*/
protected void dispatchDraw(Canvas canvas) {
... // 空實現
}
/**
* 步驟4: onDrawScrollBars(canvas)
* 作用:繪製裝飾,如 滾動指示器、滾動條、和前景等
*/
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
至此,單一View
的draw
過程已分析完畢。
總結
單一View的draw
過程解析如下:
即 只需繪製
View
自身
3.2 ViewGroup的draw過程
應用場景
利用現有的元件根據特定的佈局方式來組成新的元件具體使用
繼承自ViewGroup
或 各種Layout
;含有子View
如:底部導航條中的條目,一般都是上圖示(ImageView)、下文字(TextView),那麼這兩個就可以用自定義ViewGroup組合成為一個Veiw,提供兩個屬性分別用來設定文字和圖片,使用起來會更加方便。
原理(步驟)
ViewGroup
繪製自身(含背景、內容);
2.ViewGroup
遍歷子View
& 繪製其所有子View;
類似於單一
View
的draw
過程ViewGroup
繪製裝飾(滾動指示器、滾動條、和前景)
自上而下、一層層地傳遞下去,直到完成整個View
樹的draw
過程
- 具體流程
下面我將對每個步驟和方法進行詳細分析:draw
過程的入口 = draw()
/**
* 原始碼分析:draw()
* 與單一View的draw()流程類似
* 作用:根據給定的 Canvas 自動渲染 View(包括其所有子 View)
* 繪製過程:
* 1. 繪製view背景
* 2. 繪製view內容
* 3. 繪製子View
* 4. 繪製裝飾(漸變框,滑動條等等)
* 注:
* a. 在呼叫該方法之前必須要完成 layout 過程
* b. 所有的檢視最終都是呼叫 View 的 draw ()繪製檢視( ViewGroup 沒有複寫此方法)
* c. 在自定義View時,不應該複寫該方法,而是複寫 onDraw(Canvas) 方法進行繪製
* d. 若自定義的檢視確實要複寫該方法,那麼需先呼叫 super.draw(canvas)完成系統的繪製,然後再進行自定義的繪製
*/
public void draw(Canvas canvas) {
...// 僅貼出關鍵程式碼
int saveCount;
// 步驟1: 繪製本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 若有必要,則儲存圖層(還有一個復原圖層)
// 優化技巧:當不需繪製 Layer 時,“儲存圖層“和“復原圖層“這兩步會跳過
// 因此在繪製時,節省 layer 可以提高繪製效率
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步驟2:繪製本身View內容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:預設為空實現,需複寫
// ViewGroup中:需複寫
// 步驟3:繪製子View
// ViewGroup中:系統已複寫好對其子檢視進行繪製,不需複寫
dispatchDraw(canvas);
// 步驟4:繪製裝飾,如滑動條、前景色等等
onDrawScrollBars(canvas);
return;
}
...
}
由於 步驟2:drawBackground()
、步驟3:onDraw()
、步驟5:onDrawForeground()
,與單一View的draw過程類似,此處不作過多描述
- 下面直接進入與單一
View
draw
過程最大不同的步驟4:dispatchDraw()
/**
* 原始碼分析:dispatchDraw()
* 作用:遍歷子View & 繪製子View
* 注:
* a. ViewGroup中:由於系統為我們實現了該方法,故不需重寫該方法
* b. View中預設為空實現(因為沒有子View可以去繪製)
*/
protected void dispatchDraw(Canvas canvas) {
......
// 1. 遍歷子View
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 2. 繪製子View檢視 ->>分析1
more |= drawChild(canvas, transientChild, drawingTime);
}
....
}
}
/**
* 分析1:drawChild()
* 作用:繪製子View
*/
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
// 最終還是呼叫了子 View 的 draw ()進行子View的繪製
return child.draw(canvas, this, drawingTime);
}
至此,ViewGroup
的draw
過程已分析完畢。
總結
ViewGroup
的draw
過程如下:
4. 其他細節問題:View.setWillNotDraw()
/**
* 原始碼分析:setWillNotDraw()
* 定義:View 中的特殊方法
* 作用:設定 WILL_NOT_DRAW 標記位;
* 注:
* a. 該標記位的作用是:當一個View不需要繪製內容時,系統進行相應優化
* b. 預設情況下:View 不啟用該標記位(設定為false);ViewGroup 預設啟用(設定為true)
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
// 應用場景
// a. setWillNotDraw引數設定為true:當自定義View繼承自 ViewGroup 、且本身並不具備任何繪製時,設定為 true 後,系統會進行相應的優化。
// b. setWillNotDraw引數設定為false:當自定義View繼承自 ViewGroup 、且需要繪製內容時,那麼設定為 false,來關閉 WILL_NOT_DRAW 這個標記位。
5. 總結
- 本文全面總結了自定義
View
的Draw
過程,總結如下