Android源碼進階之深入理解View的繪制流程(Draw)機制
前言
前幾篇文章,講述了measure,layout流程等,接下來將詳細分析繪制流程。
測量流程決定了View的大小,布局流程決定了View的位置,那么繪制流程將決定View的樣子,一個View該顯示什么由繪制流程完成;
那我們就開始開車了;
一、performDraw
三大工作流程始于ViewRootImpl#performTraversals,在這個方法內部會分別調用performMeasure,performLayout,performDraw三個方法來分別完成測量,布局,繪制流程。那么我們現在先從performDraw方法看起;
performDraw
- private void performDraw() {
- //...
- final boolean fullRedrawNeeded = mFullRedrawNeeded;
- try {
- draw(fullRedrawNeeded);
- } finally {
- mIsDrawing = false;
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
- }
- }
里面又調用了ViewRootImpl#draw方法,我們來看看ViewRootImpl#draw:
- private void draw(boolean fullRedrawNeeded) {
- ...
- //獲取mDirty,該值表示需要重繪的區域
- final Rect dirty = mDirty;
- if (mSurfaceHolder != null) {
- // The app owns the surface, we won't draw.
- dirty.setEmpty();
- if (animating) {
- if (mScroller != null) {
- mScroller.abortAnimation();
- }
- disposeResizeBuffer();
- }
- return;
- }
- //如果fullRedrawNeeded為真,則把dirty區域置為整個屏幕,表示整個視圖都需要繪制
- //第一次繪制流程,需要繪制所有視圖
- if (fullRedrawNeeded) {
- mAttachInfo.mIgnoreDirtyState = true;
- dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
- }
- //...
- if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
- return;
- }
- }
根據fullRedrawNeeded來判斷是否需要重置dirty區域,最后調用了ViewRootImpl#drawSoftware方法,并把相關參數傳遞進去,包括dirty區域,我們接著看該方法的源碼;
- private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
- boolean scalingRequired, Rect dirty) {
- // Draw with software renderer.
- final Canvas canvas;
- try {
- final int left = dirty.left;
- final int top = dirty.top;
- final int right = dirty.right;
- final int bottom = dirty.bottom;
- //鎖定canvas區域,由dirty區域決定
- canvas = mSurface.lockCanvas(dirty);
- // The dirty rectangle can be modified by Surface.lockCanvas()
- //noinspection ConstantConditions
- if (left != dirty.left || top != dirty.top || right != dirty.right
- || bottom != dirty.bottom) {
- attachInfo.mIgnoreDirtyState = true;
- }
- canvas.setDensity(mDensity);
- }
- try {
- if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
- canvas.drawColor(0, PorterDuff.Mode.CLEAR);
- }
- dirty.setEmpty();
- mIsAnimating = false;
- attachInfo.mDrawingTime = SystemClock.uptimeMillis();
- mView.mPrivateFlags |= View.PFLAG_DRAWN;
- try {
- canvas.translate(-xoff, -yoff);
- if (mTranslator != null) {
- mTranslator.translateCanvas(canvas);
- }
- canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
- attachInfo.mSetIgnoreDirtyState = false;
- //正式開始繪制
- mView.draw(canvas);
- }
- }
- return true;
- }
實例化了Canvas對象,然后鎖定該canvas的區域,由dirty區域決定,接著對canvas進行一系列的屬性賦值,最后調用了mView.draw(canvas)方法,
mView就是DecorView,也就是說從DecorView開始繪制;
二、draw源碼詳解
由于ViewGroup沒有重寫draw方法,因此所有的View都是調用View#draw方法,因此,我們直接看它的源碼
- public void draw(Canvas canvas) {
- ....
- // 1. 繪制本身View背景
- if (!dirtyOpaque) {
- drawBackground(canvas);
- }
- if (!verticalEdges && !horizontalEdges) {
- // Step 3, draw the content
- // 2. 繪制內容,默認空實現 需復寫
- if (!dirtyOpaque) onDraw(canvas);
- // 3. 繪制 children
- dispatchDraw(canvas);
- drawAutofilledHighlight(canvas);
- // 4. 分發Draw (單一View空實現,ViewGroup見下面分析)
- if (mOverlay != null && !mOverlay.isEmpty()) {
- mOverlay.getOverlayView().dispatchDraw(canvas);
- }
- // 5. 繪制裝飾 (前景色,滾動條)
- onDrawForeground(canvas);
- return;
- }
- ....
- }
可以看到,draw過程比較復雜,但是邏輯十分清晰。首先來看一開始的標記位dirtyOpaque,
該標記位的作用是判斷當前View是否是透明的,如果View是透明的,那么根據下面的邏輯可以看出,將不會執行一些步驟,比如繪制背景、繪制內容等;
繪制流程的五個步驟:
- 對View的背景進行繪制;
- 繪制View的內容;
- 對View的子View進行繪制(如果有子View);
- 分發Draw;
繪制View的裝飾(例如:前景色,滾動條);
1、繪制背景
- //繪制背景
- private void drawBackground(Canvas canvas) {
- final Drawable background = mBackground;
- if (background == null) {
- return;
- }
- // 根據在 layout 過程中獲取的 View 的位置參數,來設置背景的邊界
- setBackgroundBounds();
- // 先嘗試用HWUI繪制
- if (canvas.isHardwareAccelerated() && mAttachInfo != null
- && mAttachInfo.mThreadedRenderer != null) {
- mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
- final RenderNode renderNode = mBackgroundRenderNode;
- if (renderNode != null && renderNode.isValid()) {
- setBackgroundRenderNodeProperties(renderNode);
- ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
- return;
- }
- }
- final int scrollX = mScrollX;
- final int scrollY = mScrollY;
- if ((scrollX | scrollY) == 0) {
- //調用 Drawable 的 draw 方法來進行背景的繪制
- background.draw(canvas);
- } else {
- // 若 mScrollX 和 mScrollY 有值,則對 canvas 的坐標進行偏移平移畫布
- canvas.translate(scrollX, scrollY);
- //調用 Drawable 的 draw 方法來進行背景的繪制
- background.draw(canvas);
- canvas.translate(-scrollX, -scrollY);
- }
- }
2、繪制View的內容
- // 繪制View本身內容,空實現,子類必須復寫
- protected void onDraw(Canvas canvas) {
- }
這里調用了View#onDraw方法,View中該方法是一個空實現,因為不同的View有著不同的內容,這需要我們自己去實現,即在自定義View中重寫該方法來實現;
3、子View進行繪制
當前的View是一個ViewGroup類型,那么就需要繪制它的子View,這里調用了dispatchDraw,而View中該方法是空實現,實際是ViewGroup重寫了這個方法,那么我們來看看;
- @Override
- protected void dispatchDraw(Canvas canvas) {
- ...
- // 遍歷子View
- final int childrenCount = mChildrenCount;
- ...
- for (int i = 0; i < childrenCount; i++) {
- while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
- final View transientChild = mTransientViews.get(transientIndex);
- if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
- transientChild.getAnimation() != null) {
- more |= drawChild(canvas, transientChild, drawingTime);
- }
- transientIndex++;
- if (transientIndex >= transientCount) {
- transientIndex = -1;
- }
- }
- final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
- final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- // 調用 drawChild 方法,進行子元素繪制
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- ....
- }
4、分發Draw
- @Override
- protected void dispatchDraw(Canvas canvas) {
- ...
- // 1. 遍歷子View
- final int childrenCount = mChildrenCount;
- ...
- for (int i = 0; i < childrenCount; i++) {
- while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
- final View transientChild = mTransientViews.get(transientIndex);
- if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
- transientChild.getAnimation() != null) {
- more |= drawChild(canvas, transientChild, drawingTime);
- }
- transientIndex++;
- if (transientIndex >= transientCount) {
- transientIndex = -1;
- }
- }
- final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
- final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- // 調用 drawChild 方法,進行子元素繪制
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- ....
- }
5、繪制View
所謂的繪制裝飾,就是指View除了背景、內容、子View的其余部分,例如滾動條等,我們看View#onDrawForeground
- 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);
- }
- //調用 Drawable 的 draw 方法,繪制前景色
- foreground.draw(canvas);
- }
- }
到目前為止,View的繪制流程也講述完畢了;
總結
其實繪制這塊還是很重要的,下次還是要繼續講解下;
學如逆水行舟,不進則退;心似平原走馬,易放難收;
一起加油老鐵們
本文轉載自微信公眾號「Android開發編程」
【編輯推薦】