Android原始碼分析之View繪製流程

大逗大人發表於2019-03-01

 在Android的知識體系中,View扮演著很重要的的角色,簡單來理解,View就是Android在視覺上的呈現。在介面上Android提供了一套GUI庫,裡面有很多控制元件,但很多時候系統提供的控制元件都不能很好的滿足我們的需求,這時候就需要自定義View了,但僅僅瞭解基本控制元件的使用是無法做出複雜的自定義控制元件的。為所有了更好的自定義View,就需要掌握View的底層工作原理,比如View的測量、佈局以及繪製流程,掌握這幾個流程後,基本上就可以做出一個比較完善的自定義View了。

1、初始ViewRoot和DecorView

 ViewRoot對應ViewRootImpl,它是連線WindowManager(實現類是WindowManagerImpl)和DecorView的紐帶,View繪製的三大流程均是通過ViewRoot來完成的。那麼一個activity是何時開始繪製的尼?當建立activity成功並且onResume方法呼叫後,就會將DecorView新增進WindowManager中。程式碼如下:

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ...
        // TODO Push resumeArgs into the activity for consideration
        //回撥activity的onResume方法
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            ...
            if (r.window == null && !a.mFinished && willBeVisible) {
                //拿到activity對應的PhoneWindow
                r.window = r.activity.getWindow();
                //拿到activity的根View->decorView
                View decor = r.window.getDecorView();
                //隱藏decorView
                decor.setVisibility(View.INVISIBLE);
                //拿到WindowManager->WindowManagerImpl
                ViewManager wm = a.getWindowManager();
                ...
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        //將根佈局新增到WindowManager中
                        wm.addView(decor, l);
                    } else {
                        ...
                    }
                }
                ...

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                ...
                WindowManager.LayoutParams l = r.window.getAttributes();
                if ((l.softInputMode
                        & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                        != forwardBit) {
                    ...
                    if (r.activity.mVisibleFromClient) {
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        //更新Window,重新測量、擺放、繪製介面
                        wm.updateViewLayout(decor, l);
                    }
                }
                ...
            }
            ...

        } else {
            //出錯則關閉當前activity
            try {
                ActivityManager.getService()
                    .finishActivity(token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
    }

複製程式碼

 在呼叫wm.addView(decor, l);中,就會去建立ViewRootImpl,然後在ViewRootImpl中進行繪製。程式碼如下:

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            ...
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            // do this last because it fires off messages to start doing things
            try {
                //交給ViewRootImpl繼續執行
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }


複製程式碼

 在ViewRootImpl中,在正式向WMS新增Window之前,系統會呼叫requestLayout();來對UI進行繪製,通過檢視requestLayout();可以發現系統最終呼叫的是performTraversals()這個方法,在這個方法裡呼叫了View的measure、layout、draw方法。程式碼如下:

    private void performTraversals() {
        ...
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            ...

            if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;

                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (lp.verticalWeight > 0.0f) {
                        height += (int) ((mHeight - height) * lp.verticalWeight);
                        childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }

                    if (measureAgain) {
                        if (DEBUG_LAYOUT) Log.v(mTag,
                                "And hey let's measure once more: width=" + width
                                + " height=" + height);
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }

                    layoutRequested = true;
                }
            }
        } else {
            ...
        }

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);

        }

        ...

        if (!cancelDraw && !newSurface) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
        } else {
           ...
        }

        mIsInTraversal = false;
    }

複製程式碼

 對於程式碼中performMeasureperformLayoutperformDraw這三個方法有沒有一點熟悉?,沒錯,它們就對應著View的measure、layout、draw,到這裡就開始真正的繪製UI了。嗯,先來梳理一下從activity的onResume到開始繪製UI的流程,如下:

在這裡插入圖片描述

2、理解MeasureSpec

 在測量過程中,MeasureSpec非常重要,它代表一個32位的int值,高2位代表代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize則是指在某種測量模式下的大小。  SpecMode有三類,每一類都代表不同的含義,如下:

  • UNSPECIFIED:父容器不對View有任何限制,要多大給多大。用的比較少,一般見於ScrollView,ListView(大小不確定,同時大小還是變的。會通過多次測量才能真正決定好寬高)等系統控制元件。
  • EXACTLY:父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應LayoutParams中的match_parent和具體數值這兩種模式。
  • AT_MOST:父容器指定了一個可用大小即SpecSize,view的大小不能大於這個值,具體是什麼要看不同View的具體實現。它對應LayoutParams的wrap_content。

 MeasureSpec通過將SpecMode與SpecSize打包成一個int值來避免過多的記憶體物件分配,為了方便操作,提供了打包和解包的操作。

//解包
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//打包
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize,childWidthMode);
複製程式碼

 嗯,來舉個例子。當ScrollView巢狀ListView時,ListView只能顯示一個item,這時候的解決方案基本上都是將所有item展示出來。如下:

public void onMeasure(){
  //MeasureSpec打包操作
  int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
  super.onMeasure(widthMeasureSpec, expandSpec);	
}
複製程式碼

 那為什麼這麼寫就能夠展開所有item尼?因為在ListView的onMeasure中,當heightMode為MeasureSpec.AT_MOST時就會將所有的item高度相加。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            //拿到第一個item的View
            final View child = obtainView(0, mIsScrap);

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);
            //拿到第一個item的寬
            childWidth = child.getMeasuredWidth();
            //拿到第一個item的高
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }
        ...
        //當mode為MeasureSpec.UNSPECIFIED時高度則為第一個item的高度,而ScrollView、ListView等滑動元件在測量子View時,傳入的型別就是MeasureSpec.UNSPECIFIED
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }
        //當傳入型別為heightMode時則計算全部item高度,所有需要重寫ListView的onMeasure並且傳入型別為MeasureSpec.AT_MOST
        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }
        //傳入測量出來的寬高
        setMeasuredDimension(widthSize, heightSize);
        mWidthMeasureSpec = widthMeasureSpec;
    }
複製程式碼

 但是前面size時為什麼是Integer.MAX_VALUE >> 2尼?按理說值Integer.MAX_VALUE就可以了。這是因為MeasureSpec代表一個32位的int值,高兩位代表mode,如果直接與MeasureSpec.AT_MOST一起打包,mode就可能會變成其他型別。而Integer.MAX_VALUE >> 2後,高兩位就變為00了,這樣在跟MeasureSpec.AT_MOST一起打包,mode就不會變了,是MeasureSpec.AT_MOST。

//示例:
int 32位:010111100011100將這個數向右位移2位,則變成000101111000111
然後將000101111000111與MeasureSpec.AT_MOST打包這樣在listView的onMeasure裡拿到的mode就是MeasureSpec.AT_MOST型別了。
複製程式碼

3、measure過程

 在performTraversals()這個方法中,首先呼叫了View的measure方法,在此方法裡就完成了對自己的測量,如果是一個ViewGroup的話,除了完成自己的測量,還會在onMeasure裡對子控制元件進行測量。

3.1、View的measure過程

 View的測量過程由measure方法實現,這個方法是一個final型別的方法,意味著子類不能重寫此方法,在此方法裡呼叫了onMeasure方法,這個是我們自定義控制元件時重寫的方法並在此方法里根據子控制元件的寬高來給控制元件設定寬高。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            //如果快取不存在或者忽略快取則呼叫onMeasure方法
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                //直接從快取裡拿值
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        //新增快取
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
複製程式碼

 在VIew的預設onMeasure裡呼叫的是setMeasuredDimension方法,setMeasuredDimension就是給mMeasuredWidth與mMeasuredHeight賦值,一般情況下mMeasuredWidth與mMeasuredHeight的值就是控制元件真正的寬高了。

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
        private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
複製程式碼

3.2、ViewGroup的measure過程

 對於ViewGroup,它沒有重寫View的onMeasure方法,但是我們要基於ViewGroup做自定義控制元件時,一般都會重寫onMeasure方法,否則可能會導致這個控制元件的wrap_content無法使用。在此方法裡會去遍歷所有子控制元件,然後對每個子控制元件進行測量。在測量子控制元件時必須呼叫子控制元件的measure方法,否則測量無效,最後根據Mode來判斷是否將得到的寬高給這個控制元件。ViewGroup提供一個measureChild方法,當然我們也可以自己來實現。

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
複製程式碼

 補充一點,在測量過程中一般用的比較多的都是EXACTLY與AT_MOST這兩種測量模式,那麼UNSPECIFIED在那裡有應用尼?系統控制元件裡,那在那些系統控制元件中啊?嗯,那就是ListView與ScrollView中,在這兩個控制元件測量子控制元件的過程中,都傳遞了UNSPECIFIED這個型別,首先來看ListView的測量子控制元件的程式碼。

private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
        LayoutParams p = (LayoutParams) child.getLayoutParams();
        if (p == null) {
            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
            child.setLayoutParams(p);
        }
        p.viewType = mAdapter.getItemViewType(position);
        p.isEnabled = mAdapter.isEnabled(position);
        p.forceAdd = true;

        final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
                mListPadding.left + mListPadding.right, p.width);
        final int lpHeight = p.height;
        final int childHeightSpec;
        //當子控制元件的高設定為match_parent或者wrap_content時,拿到高度lpHeight是小於0的,所以會走else
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
        }
        child.measure(childWidthSpec, childHeightSpec);
        ...
    }
複製程式碼

 在ScrollView中,重寫了measureChildWithMargins這個方法,在此方法裡對子控制元件進行測量,並且對子控制元件傳遞的高度都是UNSPECIFIED型別的。

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        //高度的mode是MeasureSpec.UNSPECIFIED
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

複製程式碼

 在ListView中,當mode為MeasureSpec.UNSPECIFIED時,計算的就是第一個item的高度,這也就是當ListView或者ScrollView巢狀ListView時,只會顯示一個item的原因。  到此,測量過程就梳理完畢了,嗯,當在自定義ViewGroup時,最後一定要將測量出來的寬高傳遞給setMeasuredDimension,否則該控制元件不會顯示。

4、layout過程

 layout是來確定控制元件的位置,在前面將控制元件的寬高測量完畢後,會將控制元件的left、top、right、bottom的的位置傳給layout,如果是一個ViewGroup的話,則會在onLayout方法裡對所有子控制元件進行佈局,在onLayout方法裡呼叫child.layout方法。

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //在View裡,onLayout是空實現,一般在ViewGroup裡都是重寫onlayout
            onLayout(changed, l, t, r, b);
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }
複製程式碼

 left、top、right、bottom這幾個引數非常重要,因為這四個值一旦確定了,控制元件在父容器中的位置也就確定了,且該控制元件的寬就是right-left,高就是bottom-top

5、draw過程

 draw就比較簡單了,它的作用就是將View繪製到螢幕上面。View的繪製過程遵循如下幾步:

  1. 繪製背景drawBackground(canvas);
  2. 繪製自己onDraw(canvas);
  3. 繪製childrendispatchDraw(canvas);
  4. 繪製裝飾onDrawForeground(canvas);  程式碼如下:
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 view's 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);

            drawAutofilledHighlight(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);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
        ...
    }
複製程式碼

 上面就是View的繪製流程了,比較簡單。關於如何自己呼叫onDraw來繪製控制元件可以閱讀Android自定義控制元件三部曲文章索引這一系列文章,這一系列關於自定義控制元件寫的非常詳細。補充一點,在View裡有一個特殊的方法setWillNotDraw方法,它主要是設定優化標誌位的。如果一個View不需要繪製任何內容,那麼就會將這個標誌設為true,系統會進行相應的優化,在ViewGroup裡會預設啟動這個優化標誌位。這個標誌位的對實際開發的意義是:當我們的自定義控制元件繼承於ViewGroup並且本身不具有繪製功能時,就可以開啟這個標記位從而便於系統進行後續的優化。當然,當明確知道一個ViewGroup需要通過onDraw來繪製內容時,可以顯示的關閉WILL_NOT_DRAW這個標記。

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
複製程式碼

 View的繪製流程到這就梳理完畢了,看到這裡基本上就對View的繪製流程有一定的瞭解了,最後感謝《Android藝術探索》這本書。

參考

深入理解 Android 之 View 的繪製流程 Android View的繪製流程

相關文章