1. 程式人生 > >View的繪製流程簡單理解

View的繪製流程簡單理解

在 Android 中,我們會用到很多控制元件(如:TextView 等)、容器(如:LinearLayout 等);

其中控制元件都是繼承於 View ;而容器都是繼承於 ViewGroup ;那麼這些控制元件、容器是如何顯示到螢幕上的呢?

無論是控制元件還是容器都要經過三個最主要的過程,從而顯示到螢幕上的;即:onMeasure()、onLayout 和 onDraw;

1. onMeasure()

Measure 就是測量的意思,顧名思義就是測量控制元件大小的了。繪製流程是從哪裡來的,怎麼到 onMeasure() 這個方法的呢?

繪製流程的開始是從 ViewRootImpl 中的 performTraversals() 方法開始的,

想了解的可以點選這裡,這裡就不再多說了。

下面看 View 中的 measure() 方法,下面是官網的原始碼:

17383    /**
17384     * <p>
17385     * This is called to find out how big a view should be. The parent
17386     * supplies constraint information in the width and height parameters.
17387     * </p>
17388     *
17389     * <p>
17390     * The actual measurement work of a view is performed in
17391     * {@link #onMeasure(int, int)}, called by this method. Therefore, only
17392     * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
17393     * </p>
17394     *
17395     *
17396     * @param widthMeasureSpec Horizontal space requirements as imposed by the
17397     *        parent
17398     * @param heightMeasureSpec Vertical space requirements as imposed by the
17399     *        parent
17400     *
17401     * @see #onMeasure(int, int)
17402     */
17403    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
17404        boolean optical = isLayoutModeOptical(this);
17405        if (optical != isLayoutModeOptical(mParent)) {
17406            Insets insets = getOpticalInsets();
17407            int oWidth  = insets.left + insets.right;
17408            int oHeight = insets.top  + insets.bottom;
17409            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
17410            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
17411        }
17412
17413        // Suppress sign extension for the low bytes
17414        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
17415        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
17416
17417        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
17418                widthMeasureSpec != mOldWidthMeasureSpec ||
17419                heightMeasureSpec != mOldHeightMeasureSpec) {
17420
17421            // first clears the measured dimension flag
17422            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
17423
17424            resolveRtlPropertiesIfNeeded();
17425
17426            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
17427                    mMeasureCache.indexOfKey(key);
17428            if (cacheIndex < 0 || sIgnoreMeasureCache) {
17429                // measure ourselves, this should set the measured dimension flag back
17430                onMeasure(widthMeasureSpec, heightMeasureSpec);
17431                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
17432            } else {
17433                long value = mMeasureCache.valueAt(cacheIndex);
17434                // Casting a long to int drops the high 32 bits, no mask needed
17435                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
17436                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
17437            }
17438
17439            // flag not set, setMeasuredDimension() was not invoked, we raise
17440            // an exception to warn the developer
17441            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
17442                throw new IllegalStateException("onMeasure() did not set the"
17443                        + " measured dimension by calling"
17444                        + " setMeasuredDimension()");
17445            }
17446
17447            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
17448        }
17449
17450        mOldWidthMeasureSpec = widthMeasureSpec;
17451        mOldHeightMeasureSpec = heightMeasureSpec;
17452
17453        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
17454                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
17455    }
17456
17457    /**
17458     * <p>
17459     * Measure the view and its content to determine the measured width and the
17460     * measured height. This method is invoked by {@link #measure(int, int)} and
17461     * should be overriden by subclasses to provide accurate and efficient
17462     * measurement of their contents.
17463     * </p>
17464     *
17465     * <p>
17466     * <strong>CONTRACT:</strong> When overriding this method, you
17467     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
17468     * measured width and height of this view. Failure to do so will trigger an
17469     * <code>IllegalStateException</code>, thrown by
17470     * {@link #measure(int, int)}. Calling the superclass'
17471     * {@link #onMeasure(int, int)} is a valid use.
17472     * </p>
17473     *
17474     * <p>
17475     * The base class implementation of measure defaults to the background size,
17476     * unless a larger size is allowed by the MeasureSpec. Subclasses should
17477     * override {@link #onMeasure(int, int)} to provide better measurements of
17478     * their content.
17479     * </p>
17480     *
17481     * <p>
17482     * If this method is overridden, it is the subclass's responsibility to make
17483     * sure the measured height and width are at least the view's minimum height
17484     * and width ({@link #getSuggestedMinimumHeight()} and
17485     * {@link #getSuggestedMinimumWidth()}).
17486     * </p>
17487     *
17488     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
17489     *                         The requirements are encoded with
17490     *                         {@link android.view.View.MeasureSpec}.
17491     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
17492     *                         The requirements are encoded with
17493     *                         {@link android.view.View.MeasureSpec}.
17494     *
17495     * @see #getMeasuredWidth()
17496     * @see #getMeasuredHeight()
17497     * @see #setMeasuredDimension(int, int)
17498     * @see #getSuggestedMinimumHeight()
17499     * @see #getSuggestedMinimumWidth()
17500     * @see android.view.View.MeasureSpec#getMode(int)
17501     * @see android.view.View.MeasureSpec#getSize(int)
17502     */
17503    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
17504        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
17505                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
17506    }

英語好一點的小夥伴看到註釋便能理解 measure() 方法是來做什麼用的了,下面我簡單的翻譯一下原始碼 17383 - 17402 行的註釋是什麼意思:
measure() 方法是為了找出這個檢視是多大,實際測量的工作在 onMeasure(int, int) 中執行;因為 measure() 方法是 final 修飾,所以我們重寫只需要重寫 onMeasure(int, int) 方法即可。widthMeasureSpec表示父節點施加的水平空間要求(即寬度測量),heightMeasureSpec 表示父節點施加的垂直空間要求(即高度測量)。

繼續往下看,在原始碼 17430 行呼叫 onMeasure(int, int) 方法,緊接著原始碼就給出了 onMeasure(int, int) 方法,再看一下原始碼 17457-17502 的註釋是什麼意思:

onMeasure(int, int) 方法主要測量高度和寬度,該方法由 measure() 方法呼叫,提供精確的測量。在重寫此方法時,必須呼叫 setMeasuredDimension(int, int) 來儲存這個檢視的寬度和高度。否則丟擲 IllegalStateException 異常。

在 onMeasure(int, int) 方法中呼叫了 setMeasuredDimension(int, int) 方法和 getDefaultSize() 方法,setMeasuredDimension(int, int) 是來儲存這個檢視的寬度和高度,那麼 getDefaultSize() 方法就是測量檢視的寬度和高度, getDefaultSize() 方法原始碼如下:

17607    /**
17608     * Utility to return a default size. Uses the supplied size if the
17609     * MeasureSpec imposed no constraints. Will get larger if allowed
17610     * by the MeasureSpec.
17611     *
17612     * @param size Default size for this view
17613     * @param measureSpec Constraints imposed by the parent
17614     * @return The size this view should be.
17615     */
17616    public static int getDefaultSize(int size, int measureSpec) {
17617        int result = size;
17618        int specMode = MeasureSpec.getMode(measureSpec);
17619        int specSize = MeasureSpec.getSize(measureSpec);
17620
17621        switch (specMode) {
17622        case MeasureSpec.UNSPECIFIED:
17623            result = size;
17624            break;
17625        case MeasureSpec.AT_MOST:
17626        case MeasureSpec.EXACTLY:
17627            result = specSize;
17628            break;
17629        }
17630        return result;
17631    }

來看一些對 getDefaultSize() 方法的解釋:

如果測量沒有被限制,那麼該方法將返回檢視的預設大小,如果限制,那麼可以根據需要檢視將變得更大。方法中兩個引數:size 是檢視的預設大小;measureSpec 是父類施加的約束。最後返回該檢視的大小。

在 getDefaultSize() 方法中 MeasureSpec 有三種類型,繼續來看一下官網原始碼:

19600    public static class MeasureSpec {
19601        private static final int MODE_SHIFT = 30;
19602        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
19603
19604        /**
19605         * Measure specification mode: The parent has not imposed any constraint
19606         * on the child. It can be whatever size it wants.
19607         */
19608        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
19609
19610        /**
19611         * Measure specification mode: The parent has determined an exact size
19612         * for the child. The child is going to be given those bounds regardless
19613         * of how big it wants to be.
19614         */
19615        public static final int EXACTLY     = 1 << MODE_SHIFT;
19616
19617        /**
19618         * Measure specification mode: The child can be as large as it wants up
19619         * to the specified size.
19620         */
19621        public static final int AT_MOST     = 2 << MODE_SHIFT;
19622        ...
19623    }

下面來看一下對這三種類型的解釋:

 UNSPECIFIED:父節點沒有對子節點施加任何約束。它可以是任意大小。(一般不用)

EXACTLY:父節點為子節點確定了精確的大小。孩子會得到這些界限不管它想要多大。

AT_MOST:子元素可以是它想要的任意大小,直到指定的大小。

到這裡 View 的一次測量便結束了,還不算很複雜。

一般情況下,頁面都是比較複雜的,會包含多個子檢視,下面通過 ViewGroup 來看一下頁面是如何測量的,下面是原始碼:

5396    /**
5397     * Ask all of the children of this view to measure themselves, taking into
5398     * account both the MeasureSpec requirements for this view and its padding.
5399     * We skip children that are in the GONE state The heavy lifting is done in
5400     * getChildMeasureSpec.
5401     *
5402     * @param widthMeasureSpec The width requirements for this view
5403     * @param heightMeasureSpec The height requirements for this view
5404     */
5405    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
5406        final int size = mChildrenCount;
5407        final View[] children = mChildren;
5408        for (int i = 0; i < size; ++i) {
5409            final View child = children[i];
5410            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
5411                measureChild(child, widthMeasureSpec, heightMeasureSpec);
5412            }
5413        }
5414    }
5415
5416    /**
5417     * Ask one of the children of this view to measure itself, taking into
5418     * account both the MeasureSpec requirements for this view and its padding.
5419     * The heavy lifting is done in getChildMeasureSpec.
5420     *
5421     * @param child The child to measure
5422     * @param parentWidthMeasureSpec The width requirements for this view
5423     * @param parentHeightMeasureSpec The height requirements for this view
5424     */
5425    protected void measureChild(View child, int parentWidthMeasureSpec,
5426            int parentHeightMeasureSpec) {
5427        final LayoutParams lp = child.getLayoutParams();
5428
5429        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
5430                mPaddingLeft + mPaddingRight, lp.width);
5431        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
5432                mPaddingTop + mPaddingBottom, lp.height);
5433
5434        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
5435    }

先看 ViewGroup 中官網對 measureChildren() 方法的解釋:

所有子檢視自己去測量自己,同時考慮該檢視的基本要求及其填充,我們忽略了那些處於隱藏狀態的檢視,以便太繁瑣。

在原始碼5411行呼叫 measureChild() 方法進行測量自己,該方法呼叫 getChildMeasureSpec() 方法分別測量高度和寬度;後面的測量就和測量 View 一樣了。

到這裡,對 ViewGroup 的測量也有所瞭解;

下面來看我寫的一個 demo:

public class MyView extends View {

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //在重寫此方法時,必須呼叫 setMeasuredDimension(int, int) 來儲存這個檢視的寬度和高度。
        //否則丟擲 IllegalStateException 異常。
        setMeasuredDimension(300,200);
    }
    ...
}

很簡單,裡面只是返回了控制元件的固定值,需要記住的是在重寫控制元件的 onMeasure() 方法一定要重寫 setMeasuredDimension() 方法。

2. onLayout()

測量完畢後,下一步就開始進行佈局了,以 View 為例子看一下原始碼:

16600    /**
16601     * Assign a size and position to a view and all of its
16602     * descendants
16603     *
16604     * <p>This is the second phase of the layout mechanism.
16605     * (The first is measuring). In this phase, each parent calls
16606     * layout on all of its children to position them.
16607     * This is typically done using the child measurements
16608     * that were stored in the measure pass().</p>
16609     *
16610     * <p>Derived classes should not override this method.
16611     * Derived classes with children should override
16612     * onLayout. In that method, they should
16613     * call layout on each of their children.</p>
16614     *
16615     * @param l Left position, relative to parent
16616     * @param t Top position, relative to parent
16617     * @param r Right position, relative to parent
16618     * @param b Bottom position, relative to parent
16619     */
16620    @SuppressWarnings({"unchecked"})
16621    public void layout(int l, int t, int r, int b) {
16622        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
16623            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
16624            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
16625        }
16626
16627        int oldL = mLeft;
16628        int oldT = mTop;
16629        int oldB = mBottom;
16630        int oldR = mRight;
16631
16632        boolean changed = isLayoutModeOptical(mParent) ?
16633                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
16634
16635        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
16636            onLayout(changed, l, t, r, b);
16637            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
16638
16639            ListenerInfo li = mListenerInfo;
16640            if (li != null && li.mOnLayoutChangeListeners != null) {
16641                ArrayList<OnLayoutChangeListener> listenersCopy =
16642                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
16643                int numListeners = listenersCopy.size();
16644                for (int i = 0; i < numListeners; ++i) {
16645                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
16646                }
16647            }
16648        }
16649
16650        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
16651        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
16652    }
16653
16654    /**
16655     * Called from layout when this view should
16656     * assign a size and position to each of its children.
16657     *
16658     * Derived classes with children should override
16659     * this method and call layout on each of
16660     * their children.
16661     * @param changed This is a new size or position for this view
16662     * @param left Left position, relative to parent
16663     * @param top Top position, relative to parent
16664     * @param right Right position, relative to parent
16665     * @param bottom Bottom position, relative to parent
16666     */
16667    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
16668    }

先看一下官網對 layout() 方法的介紹:

為檢視及其所有子檢視分配大小和位置,這是佈局機制的第二階段(首先是測量)。派生類不應重寫此方法,帶有子類的派生類應該重寫 onLayout() 。四個引數分別為:左位置,頂部位置,右位置和底部位置。

在原始碼16636行,呼叫了 onLayout() 方法,可以發現這個方法是個空方法,來看一下原始碼對 onLayout() 方法的解釋:

從layout呼叫時,該檢視應該為其每個子檢視分配大小和位置。帶有子類的派生類應該重寫此方法,並對其每個子類呼叫佈局。

設定控制元件的位置應該是在裝有控制元件的容器中進行的,那麼在 View 中 onLayout() 裡面是空的也不足為奇了。既然是在容器中進行佈局的,那麼來看一下 ViewGroup 中的 onLayout() 方法:

4973    /**
4974     * {@inheritDoc}
4975     */
4976    @Override
4977    protected abstract void onLayout(boolean changed,
4978            int l, int t, int r, int b);

 

可以看到,在 ViewGroup 中,onLayout() 方法是一個空的抽象方法,這也就意味著,所有的容器都要重寫 onLayout() 方法進行佈局;那麼我們就來重寫 onLayout() 的方法,如下面程式碼:

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            //測量第一個子View大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            //設定第一個子View左、上、右、下的座標
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
        }
    }
}

上面程式碼中,首先重寫 onMeasure() 方法,在裡面拿出第一個子 View 進行測量其大小;其次重寫了 onLayout() 方法,在裡面拿出第一個子 View 設定它的座標。寫完後,就去佈局檔案中試一下:

<com.example.qd.permission.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@mipmap/ic_launcher_round"/>
</com.example.qd.permission.MyViewGroup>

佈局檔案中也很簡單,裡面只有一個 ImageView 控制元件,下面是執行結果:

可以看到正常顯示出來了,我們可以設定它的座標來改變位置。

3. onDraw()

看完前兩個階段,進入到最後一個階段:繪製過程,先看一下原始碼:

16143    /**
16144     * Manually render this view (and all of its children) to the given Canvas.
16145     * The view must have already done a full layout before this function is
16146     * called.  When implementing a view, implement
16147     * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
16148     * If you do need to override this method, call the superclass version.
16149     *
16150     * @param canvas The Canvas to which the View is rendered.
16151     */
16152    @CallSuper
16153    public void draw(Canvas canvas) {
16154        final int privateFlags = mPrivateFlags;
16155        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
16156                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
16157        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
16158
16159        /*
16160         * Draw traversal performs several drawing steps which must be executed
16161         * in the appropriate order:
16162         *
16163         *      1. Draw the background
16164         *      2. If necessary, save the canvas' layers to prepare for fading
16165         *      3. Draw view's content
16166         *      4. Draw children
16167         *      5. If necessary, draw the fading edges and restore layers
16168         *      6. Draw decorations (scrollbars for instance)
16169         */
16170
16171        // Step 1, draw the background, if needed
16172        int saveCount;
16173
16174        if (!dirtyOpaque) {
16175            drawBackground(canvas);
16176        }
16177
16178        // skip step 2 & 5 if possible (common case)
16179        final int viewFlags = mViewFlags;
16180        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
16181        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
16182        if (!verticalEdges && !horizontalEdges) {
16183            // Step 3, draw the content
16184            if (!dirtyOpaque) onDraw(canvas);
16185
16186            // Step 4, draw the children
16187            dispatchDraw(canvas);
16188
16189            // Overlay is part of the content and draws beneath Foreground
16190            if (mOverlay != null && !mOverlay.isEmpty()) {
16191                mOverlay.getOverlayView().dispatchDraw(canvas);
16192            }
16193
16194            // Step 6, draw decorations (foreground, scrollbars)
16195            onDrawForeground(canvas);
16196
16197            // we're done...
16198            return;
16199        }
....         ...
16345    }

可以看出,官網給出瞭解釋和 onDraw() 中的6個步驟:
手動將此檢視(及其所有子檢視)呈現到給定畫布。在呼叫此函式之前,檢視必須已經完成了完整的佈局,在實現檢視時,需要重寫 onDraw() 方法,而不是去重寫這個方法;如果您確實需要重寫此方法,請呼叫超類版本。

再來看一下6個步驟:

繪製遍歷執行幾個繪製步驟,這些步驟必須以適當的順序執行:

1. 畫的背景

2. 如果需要,儲存畫布的圖層以備褪色

3.畫檢視的內容

4. 繪製子檢視

5. 如果需要,繪製衰落邊緣並恢復圖層

6. 繪製裝飾(例如滾動條)

也不再多說什麼了,官網已經解釋的很清楚了;下面來看一下具體的使用方法:

public class MyView extends View {
    private Paint mPaint;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    } 

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.RED);
        //設定背景的座標位置
        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(30);
        String text = "wu";
        //設定文字座標位置
        canvas.drawText(text, 0, getHeight() / 2, mPaint);
    }
}

程式碼很簡單,在裡面新建了 Paint 物件,然後在畫布上畫出來我們想要的佈局;在佈局檔案中應用它:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <com.example.qd.permission.MyView
        android:layout_width="80dp"
        android:layout_height="50dp"/>
</LinearLayout>

來看一下效果圖:

可以看出,雖然辣眼睛,但是效果已經達到了。

到此為止,View 繪製的三個主要流程也就講完了