Android View的繪制流程

Android View的繪制流程

源碼版本為 Android 10(Api 29)夹姥,不同Android版本可能有一些差別

View 的繪制從哪里開始

《Activity常見問題》的 Activity 在 onResume 之后才顯示的原因是什么袖瞻? 部分中我們知道了View是在 onResume() 回調(diào)之后才顯示出來的,顯示過程主要是通過 WindowManagerImpl#addView() -> WindowManagerGlobal#addView() -> ViewRootImpl#setView() 這個過程煞躬,我們再次看一下 ViewRootImpl#setView() 的代碼(核心代碼):

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     synchronized (this) {
         if (mView == null) {
             mView = view;
             // 調(diào)用 requestLayout() 方法肛鹏,進行布局(包括measue逸邦、layout、draw)
             requestLayout();
             
             mOrigWindowType = mWindowAttributes.type;
             mAttachInfo.mRecomputeGlobalAttributes = true;
             collectViewAttributes();
             // 通過調(diào)用 Session 的 addToDisplay() 方法
             res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                     getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                     mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                     mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                     mTempInsets);
             setFrame(mTmpFrame);
         }
     }
 }

有這樣一行 requestLayout() 龄坪,表示請求布局昭雌,我們界面的繪制也是從這一行代碼開始的,接下來健田,我們就來看一下跟蹤一下這段代碼(ViewRootImpl#requestLayout()):

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

調(diào)用 ViewRootImpl#scheduleTraversals() 方法:

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

scheduleTraversals() 方法中通過 mChoreographer.postCallback() 方法發(fā)送一個要執(zhí)行的實現(xiàn)了 RunnableTraversalRunnable 的對象 mTraversalRunnable

  1. mChoreographer.postCallback() 方法內(nèi)部就是通過Handler機制

  2. TraversalRunnableViewRootImpl 的內(nèi)部類

     final class TraversalRunnable implements Runnable {
         @Override
         public void run() {
             doTraversal();
         }
     }
    

調(diào)用 ViewRootImpl#doTraversal() 方法

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

調(diào)用 ViewRootImpl#performTraversals() 方法烛卧,該方法中關(guān)于繪制的代碼

private void performTraversals(){
    ……
    // 方法測量組件的大小
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
    ……
    // 方法用于子組件的定位(放在窗口的什么地方)
    performLayout(lp,desiredWindowWidth,desiredWindowHeight);
    ……
    // 繪制組件內(nèi)容
    performDraw();
    ……
}

而在performMeasure()performLayout()performDraw()方法的調(diào)用過程可以用下面的圖來表示:

Android View繪制流程.png

從圖中可以看出系統(tǒng)的View類已經(jīng)寫好了measure()妓局、layout()draw()方法:

  1. 在系統(tǒng)View類中measure()方法用了final修飾总放,不能被重寫(我覺得這應(yīng)該是google不想讓開發(fā)者更改measure()方法里面的邏輯而設(shè)計的,但是開發(fā)者有時又有需求需要自己測量好爬,所以提供了onMeasure()方法可以重寫)局雄;
    代碼
  2. 在系統(tǒng)View類中的layout()方法在調(diào)用onLayout()方法前調(diào)用了setFrame()方法,這個方法作用是判斷View的位置是否發(fā)生改變存炮,如果沒有發(fā)生改變炬搭,就不調(diào)用onLayout()方法,主要是為了提高性能穆桂,在View類中onLayout()方法是空實現(xiàn)宫盔,這是因為view沒有子類,而當在自定義的控件如果是直接繼承ViewGroup時就必須重寫onLayout()方法享完;
    代碼
  3. 在系統(tǒng)View類中的draw()方法灼芭,開發(fā)者一般不會重寫,因為當我們?nèi)绻貙?code>draw()時般又,就需要按照系統(tǒng)定義好的步驟一步一步的畫彼绷,否則會顯示不出來,相對來說比較麻煩茴迁。而如果我們實現(xiàn)onDraw()方法寄悯,我們只要關(guān)注我們畫的內(nèi)容即可(畫出來的內(nèi)容就是顯示到界面的內(nèi)容);
    代碼
  4. 當開發(fā)者在自定義控件時一般只需重寫onMeasure()笋熬、onLayout()onDraw()方法就可以了热某。

測量 -- measure

源碼追蹤

ViewRootImpl中的performMeasure()方法:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

調(diào)用了mViewmeasure()方法,mView就是 DecorView胳螟,是通過 ViewRootImpl#setView() 傳入進來的昔馋,也就是調(diào)用了FrameLayout#measure()方法,FrameLayout繼承ViewGroup糖耸,ViewGroup沒有重寫也不能重寫measure()方法盗迟,所以最終調(diào)用的是View類中的measure()方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
        // measure ourselves, this should set the measured dimension flag back
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    ...
}

在View類中的measure()中系統(tǒng)幫我們做了很多的處理并且不想讓開發(fā)者重寫measure的邏輯项棠,所以使用了final修飾符進行修飾,并且調(diào)用了onMeasure()方法引镊,所以在Activity中View樹的測量過程中骂因,最終是從FrameLayout#onMeasure()方法開始的,FrameLayoutonMeasure()方法如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    ......
    // 判斷孩子控件的Visibility屬性,如果為gone時,就跳過希坚,因為gone屬性不占用空間
    if (count > 1) {// 判斷是否有孩子控件
        for (int i = 0; i < count; i++) {
            // 通過LayoutParams參數(shù)獲取孩子控件的margin、padding值
            ...
            // 調(diào)用孩子控件的measure()方法測量自身大小
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

FrameLayoutonMeasure()方法中陵且,先是獲取了孩子控件的個數(shù)裁僧,然后獲取每一個孩子控件并判斷visibility屬性;最終調(diào)用孩子View#measure()方法測量自身大小慕购。

當開發(fā)者有需要重新測量控件時聊疲,只需要重寫onMeasure()方法即可,系統(tǒng)在View的measure()方法中會回調(diào)onMeasure()方法沪悲,使測量值生效获洲。

下面是繼承自View時重寫的onMeasure()方法,我就只是簡單的將寬和高都設(shè)置成500:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(500,500);
}

下面是繼承ViewGroup時重寫的onMeasure()方法殿如,將所有孩子控件的寬和高的和計算出來作為父控件的寬和高贡珊,最終調(diào)用setMeasuredDimension()方法設(shè)置值:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMeasure = 0,heightMeasure = 0;
    // 直接調(diào)用系統(tǒng)方法測量每一個孩子控件的寬和高
    measureChildren(widthMeasureSpec,heightMeasureSpec);
    /**
     * 系統(tǒng)在調(diào)用measureChildren(widthMeasureSpec,heightMeasureSpec)的過程中,
     * 如果孩子控件依然是ViewGroup類型的涉馁,那么又會調(diào)用measureChildren()方法飞崖,否則會調(diào)用
     * child.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法測量每一個孩子控件
     * 的寬和高,直到所有的孩子控件都測量完成谨胞。
     * 這就可以說明measure的過程可以看成是一個遞歸的過程。
     */
 
    // 獲取孩子控件的個數(shù)
    int childCount = getChildCount();
    // 循環(huán)測量每一個孩子控件的寬和高蒜鸡,得到的和就作為控件的寬和高
    for (int i = 0; i < childCount; i++) {
        View view = getChildAt(i);
        // 獲取每一個孩子的寬和高
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
        // 把每一個孩子控件的寬和高加上
        widthMeasure += width;
        heightMeasure += height;
    }
 
    // 調(diào)用setMeasuredDimension()方法保存寬和高胯努,表示measure的結(jié)束
    setMeasuredDimension(widthMeasure,heightMeasure);
}

另外在measure的過程中還可能會用到MeasureSpec類(View的內(nèi)部類):

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
 
    // 父控件不沒有對子施加任何約束,子可以是任意大蟹攴馈(也就是未指定)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
 
    // 父控件決定子的確切大小叶沛,表示width和height屬性設(shè)置成match_parent或具體值
    public static final int EXACTLY     = 1 << MODE_SHIFT;
 
    // 子最大可以達到的指定大小,當設(shè)置為wrap_content時忘朝,模式為AT_MOST
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    /*
     * 通過模式和大小創(chuàng)建一個測量規(guī)范
     * @param size the size of the measure specification
     * @param mode the mode of the measure specification
     * @return the measure specification based on size and mode
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
 
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
 
    /*
     * 獲取模式
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
 
    /*
     * 獲取大小
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
 
    ...
}

MeasureSpecs使用了二進制去減少對象的分配灰署,用最高的兩位數(shù)來表示模式(Mode),剩下的30位表示大小(size)局嘁。

Mode有三種:UNSPECIFIED(未指定溉箕,沒有約束,可以任意大小)悦昵、EXACTLY(精確肴茄,表示match_parent或者具體的大小值)、AT_MOST(最大值但指,表示wrap_content)

measure總結(jié):

  1. View的measure()方法被final修飾寡痰,子類不可以重寫抗楔,但可以通過重寫onMeasure()方法來測量大小,當然也可以不重寫onMeasure()方法使用系統(tǒng)默認測量大欣棺埂连躏;

  2. 如果想要讓自己設(shè)置的值生效,就必須調(diào)用setMeasuredDimension()方法設(shè)置寬和高贞滨;

  3. 如果在ActivityonCreate()方法或onResume()方法里面直接調(diào)用getWidth()/getHeight()入热、getMeasureWidth()/getMeasureHeight()獲取控件的大小得到的結(jié)果很可能是0,因為在onCreate()onResume()的時候系統(tǒng)還沒有調(diào)用measure()方法(getMeasureWidth()getMeasureHeight()的賦值在View的setMeasuredDimension()方法中疲迂,所以在調(diào)用完View的setMeasuredDimension()方法之后getMeasuredWidth()getMeasuredHeight()就已經(jīng)有值了才顿。而getWidth()getHeight()要在onLayout()方法完成之后才會被賦值),如果一定要在onCreate()方法或onResume()方法里面獲取控件的大小尤蒿,可以通過以下方法得到:

     view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
         @Override
         public void onGlobalLayout() {
             int width = view.getWidth();
             int height = view.getHeight();
             view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
         }
     });
    
  4. 通過setMeasuredDimension()方法設(shè)置的值并不一定就是控件的最終大小郑气,組件真正的大小最終是由setFrame()方法決定的,該方法一般情況下會參考measure出來的尺寸值腰池;

  5. 子視圖View的大小是由父容器View和子視圖View布局共同決定的尾组;

  6. 如果控件是ViewGroup的子類,那就必須測量每一個孩子控件的大小示弓,可以調(diào)用系統(tǒng)的measureChildren()方法測量跨跨,也可以自己測量囱皿;

  7. Android系統(tǒng)對控件的測量過程可以看做是一個遞歸的過程耕渴。

擺放 -- layout

源碼追蹤

ViewRootImpl中的performLayout()方法:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    ...
    final View host = mView;
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
    for (int i = 0; i < numValidRequests; ++i) {
        final View view = validLayoutRequesters.get(i);
        Log.w("View", "requestLayout() improperly called by " + view +
                " during layout: running second layout pass");
        view.requestLayout();
    }
}

代碼中的host是View樹中的根視圖(DecroView)添诉,也就是最外層容器艾帐,容器的位置安排在左上角(0,0),其大小默認會填滿 mContentParent容器。該方法作用是確定孩子控件的位置葡公,所以該方法只針對ViewGroup容器類蒲凶,最終調(diào)用了Viewlayout()方法確定每一個孩子控件的位置:

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) {
        onLayout(changed, l, t, r, b);
        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;
}

layout()中確定位置之前會判斷是否需要重新測量控件的大小,如果需要,就會調(diào)用onMeasure()方法重新測量控件蹬癌,接下來執(zhí)行 setOpticalFrame()setFrame()方法確定自身的位置與大小董济,這一步并不會繪制出來封豪,只是將控件位子和大小值保存起來缘琅;接著調(diào)用onLayout()方法鸽心,在View中onLayout()方法是空實現(xiàn)糯景。onLayout()方法的作用是當當前控件是容器控件時,那就必須重寫onLayout()方法確定每一個孩子控件的位置省骂,而當孩子控件還是ViewGroup的子類時钞澳,繼續(xù)調(diào)用onLayout()方法,直到所有的孩子控件都有了確定的位置和大小,這個過程和measure一樣奄侠,也可以看做是一個遞歸的過程卓箫。下面是FrameLayout#onLayout()方法源碼:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
 
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    ...// 遍歷所有的孩子控件
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        ... // 判斷控件的visible屬性書否為gone,如果為gone就不占用位置
        ... // 計算childLeft弯洗、childTop旅急、childRight、childBottom牡整,確定位置
        child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

layout總結(jié):

  1. View的布局邏輯是由父View藐吮,也就是ViewGroup容器布局來實現(xiàn)的。因此逃贝,我們?nèi)绻远xView一般都無需重寫onLayout()方法谣辞,但是如果自定義一個ViewGroup容器的話,就必須實現(xiàn)onLayout()方法沐扳,因為該方法在ViewGroup類中是抽象的泥从,ViewGroup的所有子類必須實現(xiàn)onLayout()方法(如果我們定義的容器控件是繼承FrameLayout或其他已經(jīng)繼承了ViewGroup類的容器控件時,如果沒有必要可以不用實現(xiàn)onLayout()方法沪摄,因為FrameLayout類中已經(jīng)實現(xiàn)了)躯嫉;
  2. 如果view控件使用了gone屬性時,在onLayout()方法遍歷中就會跳過當前的View杨拐,因為gone屬性表示不占用位置祈餐;
  3. 當layout()方法執(zhí)行完成之后,調(diào)用getHeight()/getWidth()方法就能夠得到控件的寬和高的值了哄陶;
  4. View的layout過程和measure過程類似帆阳,都可以看做是一個遞歸的過程;
  5. 在Activity中屋吨,layout的過程是從DecorView控件開始的舱痘。

繪制 -- draw

源碼追蹤

ViewRootImpl中的performDraw()方法:

private void performDraw() {  
    ...  
    final boolean fullRedrawNeeded = mFullRedrawNeeded;  
    mFullRedrawNeeded = false;  
    mIsDrawing = true;  
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");  
    try {  
        draw(fullRedrawNeeded);  
    } finally {  
        mIsDrawing = false;  
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);  
    }  
  
    ...  
}  
  
private void draw(boolean fullRedrawNeeded) {  
    ...  
    if (!drawSoftware(surface, attachInfo, yoff, scalingRequired, dirty)) {  
        return;  
    }  
    ...  
}  
  
/** 
 * @return true if drawing was succesfull, false if an error occurred 
 */  
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,  
        boolean scalingRequired, Rect dirty) {  
    Canvas canvas;  
    ...  
    try {  
        ...  
        int left = dirty.left;  
        int top = dirty.top;  
        int right = dirty.right;  
        int bottom = dirty.bottom;  
        canvas = mSurface.lockCanvas(dirty);  
        if (!canvas.isOpaque() || yoff != 0) {  
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);  
        }  
        mView.draw(canvas);  
        ...  
    } finally {  
        surface.unlockCanvasAndPost(canvas);  
    }  
    ...  
    return true;  
}

canvas對象是從surface中獲取到的,surface是中提供了一套雙緩存機制离赫,這樣就提高了繪圖的效率.通過代碼可以看到最后調(diào)用了mViewdraw()方法,mViewecorView塌碌,也就是FrameLayout渊胸,在FrameLayoutViewGroup中都是沒有重寫draw()方法的,所以最終調(diào)用的是View中的draw()方法:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background   繪制視圖View的背景
     *      2. If necessary, save the canvas' layers to prepare for fading 保存畫布canvas的邊框參數(shù)
     *      3. Draw view's content 繪制視圖View的內(nèi)容(調(diào)用了onDraw()方法)
     *      4. Draw children 繪制當前視圖View的子視圖(調(diào)用dispatchDraw()方法)
     *      5. If necessary, draw the fading edges and restore layers 繪制邊框的漸變效果并重置畫布
     *      6. Draw decorations (scrollbars for instance) 繪制前景台妆、滾動條等修飾
     */
 
    // Step 1, draw the background, if needed 繪制視圖View的背景
    int saveCount;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    // skip step 2 & 5 if possible (common case)
    ...
 
    // Step 2, save the canvas' layers 保存畫布canvas的邊框參數(shù)
    saveCount = canvas.getSaveCount();
 
    // Step 3, draw the content 繪制視圖View的內(nèi)容(調(diào)用了onDraw()方法)
    if (!dirtyOpaque) onDraw(canvas);
 
    // Step 4, draw the children 繪制當前視圖View的子視圖(調(diào)用dispatchDraw()方法)
    dispatchDraw(canvas);
 
    // Step 5, draw the fade effect and restore layers 繪制邊框的漸變效果并重置畫布
    ...
    canvas.restoreToCount(saveCount);
 
    // Step 6, draw decorations (foreground, scrollbars) 繪制前景翎猛、滾動條等修飾
    onDrawForeground(canvas);
}

由以上代碼可以看得到胖翰,系統(tǒng)在View類中的draw()方法已經(jīng)將背景、邊框切厘、修飾等都繪制出來了萨咳,而且在第三步和第四步的時候調(diào)用了onDraw()方法和dispatchDraw()方法,這樣開發(fā)者在自定義控件的時候就只需要重寫onDraw()方法或者dispatchDraw()方法疫稿,也就是只需要關(guān)注最終顯示的內(nèi)容就可以了培他,而不需要去繪制其他的修飾。
View里面的onDraw()方法和dispatchDraw()方法都是空實現(xiàn)遗座,也就是留給開發(fā)者去實現(xiàn)里面的具體邏輯舀凛。同時,在開發(fā)者實現(xiàn)onDraw()方法和dispatchDraw()方法時也可以不用去繪制其他的修飾了途蒋。需要說明一點猛遍,View中的draw()方法并不是和measure()方法一樣被final修飾,draw()方法沒有被final修飾号坡,所以是可以重寫的懊烤,但是當我們重寫draw()方法時,必須和系統(tǒng)中Viewdraw()方法一樣宽堆,一步一步的實現(xiàn)腌紧,否則就不能將控件繪制出來,所以在自定義控件的時候日麸,一般都是重寫onDraw()dispatchDraw()方法寄啼。

如果自定義控件是繼承至View時,就重寫onDraw()方法代箭,在onDraw()方法中繪制的結(jié)果就是最終顯示的結(jié)果墩划。

以下代碼就是在界面上以坐標(150,150)繪制了一個半徑為35的紅色實心圓:

public class CircleView extends View {
    Paint paint;
    public CircleView(Context context) {
        this(context, null);
    }
 
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(150,150,35,paint);
    }
}
View 繪制流程_circle.png

如果自定義控件是繼承至ViewGroup時,就重寫dispatchDraw()方法嗡综,這里直接查看系統(tǒng)FrameLayoutdispatchDraw()方法:

@Override
protected void dispatchDraw(Canvas canvas) {
    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);
            }
        }
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
   ...
}

在代碼里面調(diào)用了drawChild(canvas, child, drawingTime)方法用來繪制孩子:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

而在drawChild()方法中就是調(diào)用了viewdraw(Canvas canvas, ViewGroup parent, long drawingTime)方法(三個參數(shù)的方法)來繪制自己乙帮,如果孩子控件還是ViewGroup的子類,又會重新調(diào)用drawChild()方法遞歸處理极景,直到所有的孩子控件繪制完成察净,也就表示控件繪制完成了。其實這也和measure盼樟、layout的過程一樣氢卡,可以看做是一個遞歸的過程。

看一下view中三個參數(shù)的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 * 這個方法是在ViewGroup的drawChild()方法中調(diào)用晨缴,用來繪制每一個孩子控件自身译秦。
 * This draw() method is an implementation detail and is not intended to be overridden or
 * to be called from anywhere else other than ViewGroup.drawChild().
 * 這個方法除了在ViewGroup的drawChild()方法中被調(diào)用外,不應(yīng)該在其它任何地方去復(fù)寫或調(diào)用該方法,它屬于ViewGroup筑悴。
 * 而這個方法最終也會調(diào)用View的draw(canvas)一個參數(shù)的方法來進行繪制们拙。
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ...
    if (!hasDisplayList) {
        // 調(diào)用computeScroll()方法,這個方法是用來與Scroller類結(jié)合實現(xiàn)實現(xiàn)動畫效果的
        computeScroll();
        sx = mScrollX;
        sy = mScrollY;
    }
    ...
     if (!hasDisplayList) {
        // Fast path for layouts with no backgrounds
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            // 繼續(xù)調(diào)用dispatchDraw()方法遞歸處理
            dispatchDraw(canvas);
        } else {
            // 調(diào)用View的draw(canvas)一個參數(shù)的方法
            draw(canvas);
        }
    } else {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
    }
    ...
    return more;
}

了解更多關(guān)于Scroller類的相關(guān)內(nèi)容阁吝,可以查看《 Android中的Scroller類》這篇博客砚婆。

draw總結(jié):

  1. View繪制的畫布canvas是從surface對象中獲得,而最終也是繪制到surface中去突勇。surface提供了一個雙緩存機制装盯,可以提高繪制的效率;
  2. 系統(tǒng)在View類中的draw()方法已經(jīng)將背景与境、邊框验夯、修飾等都繪制出來了,如果在自定義View時直接繼承View時是重寫draw()方法摔刁,就必須和系統(tǒng)中Viewdraw()方法一樣挥转,一步一步的實現(xiàn),否則就不能將控件展示出來共屈;
  3. 因為自定義控件一般重寫onDraw()方法绑谣,所以每一個控件都會繪制滾動條和其他的修飾;
  4. 自定義View如果直接繼承制View時拗引,需要重寫onDraw()方法借宵,在onDraw()中繪制的內(nèi)容就是最終展示到界面的內(nèi)容,自定義View如果是直接繼承ViewGroup矾削,那就重寫dispatchDraw()方法壤玫,繪制ViewGroup的孩子控件;
  5. Android繪制的過程和measure哼凯、layout一樣欲间,可以看做是一個遞歸的過程。

總體來說断部,View的繪制從開始到結(jié)束要經(jīng)歷幾個過程:

測量大小猎贴,回調(diào) onMeasure()方法

組件定位,回調(diào) onLayout()方法

組件繪制蝴光,回調(diào) onDraw()方法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末她渴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔑祟,更是在濱河造成了極大的恐慌趁耗,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疆虚,死亡現(xiàn)場離奇詭異苛败,居然都是意外死亡右冻,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門著拭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牍帚,你說我怎么就攤上這事儡遮。” “怎么了暗赶?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵鄙币,是天一觀的道長。 經(jīng)常有香客問我蹂随,道長十嘿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任岳锁,我火速辦了婚禮绩衷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘激率。我一直安慰自己咳燕,他們只是感情好,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布乒躺。 她就那樣靜靜地躺著招盲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嘉冒。 梳的紋絲不亂的頭發(fā)上曹货,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機與錄音讳推,去河邊找鬼顶籽。 笑死,一個胖子當著我的面吹牛娜遵,可吹牛的內(nèi)容都是我干的蜕衡。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼设拟,長吁一口氣:“原來是場噩夢啊……” “哼慨仿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纳胧,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤镰吆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后跑慕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體万皿,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡摧找,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牢硅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹬耘。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖减余,靈堂內(nèi)的尸體忽然破棺而出综苔,到底是詐尸還是另有隱情,我是刑警寧澤位岔,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布如筛,位于F島的核電站,受9級特大地震影響抒抬,放射性物質(zhì)發(fā)生泄漏杨刨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一擦剑、第九天 我趴在偏房一處隱蔽的房頂上張望妖胀。 院中可真熱鬧,春花似錦抓于、人聲如沸做粤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怕品。三九已至,卻和暖如春巾遭,著一層夾襖步出監(jiān)牢的瞬間肉康,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工灼舍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吼和,地道東北人。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓骑素,卻偏偏與公主長得像炫乓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子献丑,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容