1. 程式人生 > >Android進階——Android檢視工作機制之measure、layout、draw

Android進階——Android檢視工作機制之measure、layout、draw

前言

自定義View一直是初學者們最頭疼的事情,因為他們並沒有瞭解到真正的實現原理就開始試著做自定義View,碰到很多看不懂的程式碼只能選擇迴避,做多了會覺得很沒自信。其實只要瞭解了View的工作機制後,會發現是挺簡單的,自定義View就是藉助View的工作機制開始將View繪製出來的

Android檢視工作機制簡介

Android檢視工作機制按順序分為以下三步:

  1. measure:確定View的寬高
  2. layout:確定View的位置
  3. draw:繪製出View的形狀

Android檢視工作機制的相關概念

Android檢視工作機制其實挺人性化的,當你真正理解之後,就跟我們畫畫是一個道理的,下面為了更好的理解,我將自定義View的過程擬物化

相關概念:

  • View(照片框):自定義View
  • measure(尺子):測量View大小
  • MeasureSpec(尺子刻度):測量View大小的測量單位
  • layout(照片框的位置):View的具體位置
  • draw(筆):繪製View

畫圖步驟:

  1. 首先畫一個100 x 100的照片框,需要尺子測量出寬高的長度(measure過程)
  2. 然後確定照片框在螢幕中的位置(layout過程)
  3. 最後藉助尺子用手畫出我們的照片框(draw過程)

Android檢視工作機制之MeasureSpec

自定義View第一步是測量,而測量需要測量規格(或測量標準)才能知道View的寬高,所以在測量之前需要認識MeasureSpec類

MeasureSpec類是決定View的measure過程的測量規格(比喻:尺子),它由以下兩部分組成

  • SpecMode:測量模式(比喻:直尺、三角尺等不同型別)
  • SpecSize:測量模式下的規格大小(比喻:尺子的刻度)

MeasureSpec的表示形式是32位的int值

  • 高2位(前面2位):表示測量模式,即SpecMode
  • 低30位(後面30位):表示在測量模式下的測量規格大小,即SpecSize

MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的物件記憶體分配

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; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; 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); } }

我們都知道SpecMode的尺子型別有很多,不同的尺子有不同的功能,而SpecSize刻度是固定的一種,所以SpecMode又分為三種模式

  • UNSPECIFIED:父容器不對View有任何大小的限制,這種情況一般用於系統內部,表示一種測量狀態
  • EXACTLY:父容器檢測出View所需要的精確大小,這時候View的值就是SpecSize
  • AT_MOST:父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值

一、結論:子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定的

  1. 首先要知道LayoutParams有三種情況:MATCH_PARENT、WARP_CONTENT、100dp(精確大小)
  2. 只要子View的MeasureSpec被確定,那麼就可以在measure過程中,測量出子View的寬高

二、通過例子來解釋結論

  1. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一種
    子View的LayoutParams:精確大小(100dp)
    也就是說:子View必須是指定大小,不管父容器載不載得下子View
    所以返回子View的MeasureSpec:EXACTLY
    所以返回子View測量出來的大小:子View自身精確大小

  2. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一種
    子View的LayoutParams:MATCH_PARENT
    也就是說:子View必須佔滿整個父容器,那麼父容器多大,子View就多大
    所以返回子View的MeasureSpec:跟父容器一致
    所以返回子View測量出來的大小:父容器可用大小

  3. 假如父容器LinearLayout的MeasureSpec:EXACTLY、AT_MOST的任意一種
    子View的LayoutParams:WARP_CONTENT
    也就是說:子View必須自適應父容器,父容器不管多小,你都不能超過它,只能自適應的縮小
    所以返回子View的MeasureSpec:AT_MOST(不能超過父容器本身)
    所以返回子View測量出來的大小:父容器可用大小

至於第4種情況,父容器是UNSPECIFIED的時候,由於父容器不知道自己多大,而子View又採用MATCH_PARENT、WARP_CONTENT的時候,子View肯定也不知道自己多大,所以只有當子View採用EXACTLY的時候,才知道自己多大

三、通過圖片分析結論結果

通過上面的例子總結,我們可以通過父容器的測量規格和子View的佈局引數來確定子View的MeasureSpec,這樣便確立了子View的寬高,下面是父容器測量規格和子View佈局引數確立子ViewMeasureSpec的結果圖

Android檢視工作機制之measure過程

measure過程其實和事件分發有點類似,也包括ViewGroup和View,我們通過各自的原始碼來分析其measure的過程

一、ViewGroup的measure過程

ViewGroup原始碼中,提供了一個measureChildren的方法來遍歷呼叫子View的measure方法,而各個子View再遞迴去執行這個過程

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);
    //開始子View的measure過程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

從原始碼中分析,ViewGroup的measure過程特別簡單,並且可以看到子View的MeasureSpec確實是通過父容器的MeasureSpec和自身的LayoutParams決定的,這也就印證了結論所說的話,但是measure的過程還是依靠子View自身的MeasureSpec

二、View的measure過程

View的原始碼中,由於measure方法是個final型別的,所以子類不能重寫此方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {

        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            //這裡呼叫onMeasure
            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;
        }
    }
    ......
}

可以發現,View的measure方法中,會呼叫自身的onMeasure方法(平時,自定義View重寫這個方法,就是對自定義的View根據自己定的規則來確定測量大小),下面繼續追蹤

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

從onMeasure方法中,有getDefaultSize()、getSuggestedMinimumWidth()、getSuggestedMinimumHeight(),它們之間又是什麼呢,繼續追蹤

1、getDefaultSize()

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

很顯然,如果你自定義不重寫onMeasure()方法的話,那麼系統就會採用預設的測量模式來確定你的測量大小,即getDefaultSize(),它的邏輯很簡單,不去看UNSPECIFIED模式,它就是返回specSize,即View測量後的大小

2、getSuggestedMinimumWidth()和getMinimumHeight()

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

getSuggestedMinimumWidth和getSuggestedMinimumHeight原理是一樣的,如果View沒有設定背景,那麼View的寬度為mMinWidth,而mMinWidth對應的就是android:minWidth這個屬性的值,如果這個屬性不指定,那麼mMinWidth預設為0;如果指定了背景,那麼View的寬度就是max(mMinWidth, mBackground.getMinimumWidth()),而這裡的getMinimumWidth()又是什麼,繼續追蹤

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

getMinimumWidth是在Drawable類中的,它返回的是Drawable的原始寬度,如果沒有Drawable,則返回0

到這裡measure過程就結束了,如果是自定義View的話,就重寫onMeasure方法,將其預設的測量方式改為我們自己規定的測量方式,最後獲得我們的寬高

Android檢視工作機制之layout過程

layout過程就比measure過程簡單多了,因為它不用什麼規格之類的東西,下面是View的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) {
        //呼叫onLayout
        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;
}

可以發現,View只需要4個點即可確定一個矩形,就是View的位置,然後呼叫onLayout()

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

onLayout()方法其實就是一個空方法,當我們在自定義View時重寫onLayout()方法,其實就是讓我們重新設定View的位置

Android檢視工作機制之draw過程

draw過程也很簡單,就是將View繪製到螢幕上,它有如下幾個步驟

  1. 繪製背景:drawBackground(canvas)
  2. 繪製自己:if (!dirtyOpaque) onDraw(canvas)
  3. 繪製children:dispatchDraw(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;
    // 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);

        // 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);

        // we're done...
        return;
    }
    ......
}

我們常常就是重寫onDraw()方法來繪製我們的自定義View,否則是沒有影象的,這點在原始碼中也是提供了onDraw()的空實現方法給我們去繪製圖像

Android檢視工作機制中的重繪

一、invalidate()和requestLayout()

invalidate()和requestLayout(),常用於View重繪和更新,其主要區別如下

  • invalidate方法只會執行onDraw方法
  • requestLayout方法只會執行onMeasure方法和onLayout方法,並不會執行onDraw方法。

所以當我們進行View更新時,若僅View的顯示內容發生改變且新顯示內容不影響View的大小、位置,則只需呼叫invalidate方法;若View寬高、位置發生改變且顯示內容不變,只需呼叫requestLayout方法;若兩者均發生改變,則需呼叫兩者,按照View的繪製流程,推薦先呼叫requestLayout方法再呼叫invalidate方法

二、invalidate()和postInvalidate()

  • invalidate方法用於UI執行緒中重新繪製檢視
  • postInvalidate方法用於非UI執行緒中重新繪製檢視,省去使用handler

結語

Android的自定義其實很簡單,對於初學者,可能就是measure過程比較難以理解,不過不要緊,每個人初學都是這樣的,建議多多實踐,花點時間去研究,你會更加熟能生巧,根本不用死記硬背,只要有思路便可以畫出你想要的自定義View,當然,能結合動畫那就更完美了,加油