1. 程式人生 > >Android View 的工作流程和原理

Android View 的工作流程和原理

生成 它的 isl canvas mask lis mat cep 少見

參考資料 << Android 開發藝術探索 >>

前言

在日常開發中,我們每天都在和各種 View 打交道,比如TextView,Button等,我們直接拿過來就可以使用,那麽 Android 是怎麽把 View 繪制到屏幕上呢,接下來我們結合源碼來具體分析。

在具體結合源碼分析前,先了解一個比較重要的概念 ViewRoot

ViewRoot

先看一張圖 Android 窗口構成圖解
技術分享圖片

ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 根布局 DecorView(看上圖) 的紐帶, View 的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 建立關聯。

View 的繪制流程是從 ViewRoot 的 performTraversals 方法開始的,它經過 measure、layout 和 draw 三個過程才能最終將一個 View 繪制出來,其中 measure 用來測量 View 的寬和高,layout 用來確定 View 在父容器中的放置位置,而 draw 則負責將 View 繪制在屏幕上。針對 performTraversals的大致流程如下:
技術分享圖片

performTraversals 會依次調用 performMeasure、performLayout 和 performDraw 三個方法,這三個方法分別完成頂級 View 的 measure、layout 和 draw 這三大流程,其中在 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對所有的子元素進行 measure 過程,這個時候 measure 流程就從父容器傳遞到子元素中了,這樣就完成了一次 measure 過程。接著子元素會重復父容器的 measure 過程,如此反復就完成了整個 View 樹的遍歷。同理,performLayout 和 performDraw 的傳遞流程和 performMeasure 是類似的,唯一不同的是,performDraw 的傳遞過程是在 draw 方法中通過 dispatchDraw 來實現的,不過這並沒有本質區別。

接下來結合源碼來分析這三個過程。

Measure 測量過程

這裏分兩種情況,View 的測量過程和 ViewGroup 的測量過程。

View 的測量過程

View 的 測量過程由其 measure 方法來完成,源碼如下:

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

可以看到 measure 方法是一個 final 類型的方法,這意味著子類不能重寫此方法。

在 13 行 measure 中會調用 onMeasure 方法,這個方法是測量的主要方法,繼續看 onMeasure 的實現

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

setMeasuredDimension 方法的作用是設置 View 寬和高的測量值,我們主要看 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;
}

可以看到要得到測量的尺寸需要用到 MeasureSpec,MeasureSpec 是什麽鬼呢,敲黑板了,重點來了。
MeasureSpec 決定了 View 的測量過程。確切來說,MeasureSpec 在很大程度上決定了一個 View 的尺寸規格。
來看 MeasureSpec 類的實現

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        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(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode 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);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        public static String toString(int measureSpec) {
            //省略...
        }
    }

可以看出 MeasureSpec 中有兩個主要的值,SpecMode 和 SpecSize, SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。

SpecMode 有三種模式:

  1. UNSPECIFIED
    不限制:父容器不對 View 有任何限制,要多大給多大,這種情況比較少見,一般不會用到。

  2. EXACTLY
    限制固定值:父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。

  3. AT_MOST
    限制上限:父容器指定了一個可用大小即 SpecSize,View 的大小不能大於這個值,具體是什麽值要看不同 View 的具體實現。它對應於 LayoutParams 中的 wrap_content。

MeasureSpec 中三個主要的方法來處理 SpecMode 和 SpecSize

  1. makeMeasureSpec 打包 SpecMode 和 SpecSize
  2. getMode 解析出 SpecMode
  3. getSize 解析出 SpecSize

不知道童鞋們之前有沒有註意到 onMeasure 有兩個參數 widthMeasureSpec 和 heightMeasureSpec,那這兩個值從哪來的呢,這兩個值都是由父視圖經過計算後傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小,但是最外層的根視圖 也就是 DecorView ,它的 widthMeasureSpec 和 heightMeasureSpec 又是從哪裏得到的呢?這就需要去分析 ViewRoot 中的源碼了,在 performTraversals 方法中調了 measureHierarchy 方法來創建 MeasureSpec 源碼如下:

  private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;

        childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

      //省略代碼...          
}

裏面調用了 getRootMeasureSpec 方法生成 MeasureSpec,繼續查看 getRootMeasureSpec 源碼

 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can‘t resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

通過上述代碼,DecorView 的 MeasureSpec 的產生過程就很明確了,具體來說其遵守如下規則,根據它的 LayoutParams 中的寬和高的參數來劃分。

  • LayoutParams.MATCH_PARENT:限制固定值,大小就是窗口的大小 windowSize
  • LayoutParams.WRAP_CONTENT:限制上限,大小不定,但是不能超過窗口的大小 windowSize
  • 固定大小:限制固定值,大小為 LayoutParams 中指定的大小 rootDimension

對於 DecorView 而言, rootDimension 的值為 lp.width 和 lp.height 也就是屏幕的寬和高,所以說 根視圖 DecorView 的大小默認總是會充滿全屏的。那麽我們使用的 View 也就是 ViewGroup 中 View 的 MeasureSpec 產生過程又是怎麽樣的呢,在 ViewGroup 的測量過程中會具體介紹。

先回頭看 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;
    }

現在理解起來是不是很簡單呢,如果 specMode 是 AT_MOST 或 EXACTLY 就返回 specSize,這也是系統默認的行為。之後會在 onMeasure 方法中調用 setMeasuredDimension 方法來設定測量出的大小,這樣 View 的 measure 過程就結束了,接下來看 ViewGroup 的 measure 過程。

ViewGroup 的測量過程

ViewGroup中定義了一個 measureChildren 方法來去測量子視圖的大小,如下所示

 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

從上述代碼來看,除了完成自己的 measure 過程以外,還會遍歷去所有在頁面顯示的子元素,
然後逐個調用 measureChild 方法來測量相應子視圖的大小

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

measureChild 的思想就是取出子元素的 LayoutParams,然後再通過 getChildMeasureSpec 來創建子元素的 MeasureSpec,接著將 MeasureSpec 直接傳遞給 View 的 measure 方法來進行測量。

那麽 ViewGroup 是如何創建來創建子元素的 MeasureSpec 呢,我們繼續看 getChildMeasureSpec 方法源碼:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面的代碼理解起來很簡單,為了更清晰地理解 getChildMeasureSpec 的邏輯,這裏提供一個表,表中對 getChildMeasureSpec 的工作原理進行了梳理,表中的 parentSize 是指父容器中目前可使用的大小,childSize 是子 View 的 LayoutParams 獲取的值,從 measureChild 方法中可看出

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

表如下:

技術分享圖片

通過上表可以看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams,就可以快速地確定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以進一步確定出子元素測量後的大小了。

至此,View 和 ViewGroup 的測量過程就告一段落了。來個小結。

MeasureSpec 的模式和生成規則
MeasureSpec 中 specMode 有三種模式:

  1. UNSPECIFIED
    不限制:父容器不對 View 有任何限制,要多大給多大,這種情況比較少見,一般不會用到。

  2. EXACTLY
    限制固定值:父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值。它對應於 LayoutParams 中的 match_parent 和具體的數值這兩種模式。

  3. AT_MOST
    限制上限:父容器指定了一個可用大小即 SpecSize,View 的大小不能大於這個值,具體是什麽值要看不同 View 的具體實現。它對應於 LayoutParams 中的 wrap_content。

生成規則:

  1. 對於普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定。
  2. 對於不同 ViewGroup 中的不同 View 生成規則參照上表。

MeasureSpec 測量過程:
measure 過程主要就是從頂層父 View 向子 View 遞歸調用 view.measure 方法,measure 中調 onMeasure 方法的過程。

說人話呢就是,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在 XML 文件中指定視圖的大小,然後視圖本身會對最終的大小進行拍板。

那麽測量過後,怎麽獲取 View 的測量結果呢
一般情況下 View 測量大小和最終大小是一樣的,我們可以使用 getMeasuredWidth 方法和 getMeasuredHeight 方法來獲取視圖測量出的寬高,但是必須在 setMeasuredDimension 之後調用,否則調用這兩個方法得到的值都會是0。為什麽要說是一般情況下是一樣的呢,在下文介紹 Layout 中會具體介紹。

Layout 布局過程

測量結束後,視圖的大小就已經測量好了,接下來就是 Layout 布局的過程。上文說過 ViewRoot 的 performTraversals 方法會在 measure 結束後,執行 performLayout 方法,performLayout 方法則會調用 layout 方法開始布局,代碼如下

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(mTag, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }
 try {
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    //...省略代碼
    } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;

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

layout 方法接收四個參數,分別代表著左、上、右、下的坐標,當然這個坐標是相對於當前視圖的父視圖而言的,然後會調用 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft、mRight、mTop、mBottom 這四個值,View 的四個頂點一旦確定,那麽 View 在父容器中的位置也就確定了,接著會調用 onLayout 方法,這個方法的用途是父容器確定子元素的位置,和 onMeasure 方法類似

onLayout 源碼如下:

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

納尼,怎麽是個空方法,沒錯,就是一個空方法,因為 onLayout 過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置,我們繼續看 ViewGroup 中的 onLayout 方法

@Override  
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

可以看到,ViewGroup 中的 onLayout 方法竟然是一個抽象方法,這就意味著所有 ViewGroup 的子類都必須重寫這個方法。像 LinearLayout、RelativeLayout 等布局,都是重寫了這個方法,然後在內部按照各自的規則對子視圖進行布局的。所以呢我們如果要自定義 ViewGroup 那麽就要重寫 onLayout 方法。
技術分享圖片

public class TestViewGroup extends ViewGroup {

    public TestViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
        }
    }
}

xml 中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.will.testdemo.customview.TestViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        >

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:text="@string/hello_world"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            />

    </com.will.testdemo.customview.TestViewGroup>

  </LinearLayout>

顯示效果如下:

技術分享圖片

不知道童鞋們發現了沒,我給自定義的 ViewGroup 設置了背景色,看效果貌似占滿全屏了,可是我在 xml 中設置的 wrap_content 啊,這是什麽情況,我們回頭看看 ViewGroup 中 View 的 MeasureSpec 的創建規則

技術分享圖片

從表中可看出因為 ViewGroup 的父布局設置的 match_parent 也就是限制固定值模式,而 ViewGroup 設置的 wrap_content,那麽最後 ViewGroup 使用的是 父布局的大小,也就是窗口大小 parentSize,那麽如果我們給 ViewGroup 設置固定值就會使用 我們設置的值,來改下代碼。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <com.will.testdemo.customview.TestViewGroup
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_blue_bright"
        >

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimaryDark"
            android:text="@string/hello_world"
            android:textColor="@android:color/white"
            android:textSize="15sp"
            />

    </com.will.testdemo.customview.TestViewGroup>

</LinearLayout>

效果如下:

技術分享圖片

表中的其他情況,建議童鞋們自己寫下代碼,會理解的更好。

之前說過,一般情況下 View 測量大小和最終大小是一樣的,為什麽呢,因為最終大小在 onLayout 中確定,我們來改下代碼:

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() > 0) {
            View childView = getChildAt(0);
            childView.layout(0, 0, childView.getMeasuredWidth()+100, childView.getMeasuredHeight()+200);
        }
    }

顯示效果
技術分享圖片

沒錯,onLayout 就是這麽任性,所以要獲取 View 的真實大小最好在 onLayout 之後獲取。那麽如何來獲取 view 的真實大小呢,可以通過下面的代碼來獲取

  tv_hello.post(Runnable {
        log(" getMeasuredWidth() = ${tv_hello.measuredWidth}")
        log(" getWidth() = ${tv_hello.width}")
 }

打印如下:

01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog:  getMeasuredWidth() = 239
01-18 23:33:20.947 2836-2836/com.will.testdemo I/debugLog:  getWidth() = 339

可以看到實際高度和測試的高度是不一樣的,因為我們在 onLayout 中做了修改。

因為 View 的繪制過程和 Activity 的生命周期是不同步的,所以我們可能在 onCreate 中獲取不到值。這裏提供幾種方法來獲取

1.Activity 的 onWindowFocusChanged 方法

 override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (hasFocus){
            //獲取 view 的大小
        }
    }

2.view.post(runnable) 也就是我上面使用的方法
3.ViewTreeObserver 這裏童鞋們搜索下就可以找到使用方法,篇幅較長就不舉例子了

Draw 繪制過程

確定了 View 的大小和位置後,那就要開始繪制了,Draw 過程就比較簡單,它的作用是將 View 繪制到屏幕上面。View 的繪制過程遵循如下幾步:

  1. 繪制背景 background.draw (canvas)
  2. 繪制自己(onDraw)
  3. 繪制 children(dispatchDraw)
  4. 繪制裝飾(onDrawScrollBars)
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 的繪制過程的傳遞是通過 dispatchDraw 實現的,dispatchdraw 會遍歷調用所有子元素的 draw 方法,如此 draw 事件就一層一層的傳遞下去。和 Layout 一樣 View 是不會幫我們繪制內容部分的,因此需要每個視圖根據想要展示的內容來自行繪制,重寫 onDraw 方法。具體可參考 TextView 或者 ImageView 的源碼。

最後

View 的工作流程和原理到這就分析完了,難點主要是 MeasureSpec 測量過程,需要童鞋們認真揣摩。

Android View 的工作流程和原理