View的工作原理和自定義

初識ViewRoot和DecorView

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

  2. View的繪製流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程最終將一個view繪製出來。

  • measure過程決定來View的寬/高,Measure以後,可通過getMeasureWidth和getMeasureHeight方法來獲取測量寬高。
  • layout過程決定來View四個頂點的座標和事件的View的寬高,layout完成後可通過getTop、getLeft等來獲取四個頂點座標,getWidth和getHeight來獲取最終寬高。
  • draw過程則決定來View的顯示,只有draw方法完成後,View的內容才能呈現到屏幕上。
  1. DecorView作爲頂層View,一般情況下內部會包含一個豎直方向的LinearLayout,這個LinearLayout裏面分上下兩部分(titlebar和content)。我們在Activity中setContentView設置的佈局就是駕到來id爲content的FrameLayout中。可以通過ViewGroup content = findViewById(R.id.content)得到content,通過content.getChildAt(0)的到我們設置的View。DecorView其實是一個FrameLayout,View層的事件都先經過DecorView,然後再傳給我們的View。

DecorView被加載到Window中的過程

  • 從Activity的startActivity開始,最終調用到ActivityThread的handleLaunchActivity方法來創建Activity,首先,會調用performLaunchActivity方法,內部會執行Activity的onCreate方法,從而完成DecorView和Activity的創建。然後,會調用handleResumeActivity,裏面首先會調用performResumeActivity去執行Activity的onResume()方法,執行完後會得到一個ActivityClientRecord對象,然後通過r.window.getDecorView()的方式得到DecorView,然後會通過a.getWindowManager()得到WindowManager,最終調用其addView()方法將DecorView加進去。
  • WindowManager的實現類是WindowManagerImpl,它內部會將addView的邏輯委託給WindowManagerGlobal,可見這裏使用了接口隔離和委託模式將實現和抽象充分解耦。在WindowManagerGlobal的addView()方法中不僅會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView通過root.setView()把DecorView加載到Window中。這裏的ViewRootImpl是ViewRoot的實現類,是連接WindowManager和DecorView的紐帶。View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯.

理解MeasureSpec

MeasureSpec

MeasureSpec代表一個32爲int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。

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

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        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);
            }
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        //...
    }

SpecMode有三類,每一類都有不同含義

  1. UNSPECIFIED

不確定模式,父容器不對View做任何限制,要多大給多大,這種模式一般用於系統內部,表示一種測量狀態。

  1. EXACTLY模式

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

  1. AT_MOST模式

最大值模式,父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼值要看不同View的具體實現。它對應於LayoutParams中的wrap_content。

MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,爲了方便操作,其提供了打包和解包的方法,打包方法爲makeMeasureSpec,解包方法爲getMode和getSize。

MeasureSpec和LayoutParams的對應關係

在View測量的時候,系統會將LayoutParams在父容器約束下轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬高。MeasureSpec是由父容器的SpecMode和本身的LayoutParams共同決定的。

對於頂級view(DecorView)和普通view來說,MeasureSpec的轉換過程略有不同。對於DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同決定對於普通的View,它的MeasureSpec由父視圖的MeasureSpec和其自身的LayoutParams共同決定。 MeasureSpec一旦確定,onMeasure中就可以確定View的測量寬高。

DecorView的MeasureSpec確定

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

可以看到,是根據它的LayoutParams的參數來區分的:

  • LayoutParams.MATCH_PARENT:精確模式,大小是窗口的大小
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但不能超過窗口大小。
  • 固定大小:精確模式,大小爲LayoutParams中指定的大小。

普通View的MeasureSpec確定

對於普通View,View的Measure過程由ViewGroup傳遞而來,先看一下ViewGroup的measureChildWithMargins方法

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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述方法會對子元素進行measure,在調用子元素的measure方法前,會先通過getChildMeasureSpec來得到子元素的MeasureSpec。ViewGroup的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); //子 view剩餘最大空間

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) { //parent的specMode
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) { //子view的LayoutParams是具體值
                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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上述方法主要作用是根據父容器的MeasureSpec同時結合View本身的LayoutParams來確定子元素的MeasureSpec。

普通View的MeasureSpec的創建規則如下:

 

image.png

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

View的工作流程

View的工作流程主要是指measure、layout、draw這三大流程,其中measure確定View的測量寬高,layout確定View的最終寬高和四個頂點的位置(即確定view的位置),draw將View繪製到屏幕上。

View的繪製流程之measure

measure的過程要分情況來看,如果只是一個原始的View,那麼通過measure方法就完成其測量,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,然後各個子View再遞歸這個過程。

  1. View的measure過程

View的measure過程由其measure方法來完成,它是一個final方法,意味子類不能重寫。在measure方法中會調用onMeasure方法:

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //...
        onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

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

我們只要看.AT_MOST和EXACTLY模式,可以簡單理解,其實getDefaultSize返回的大小就是measureSpec的specSize。

對於UNSPECIFIED的情況,一般用於系統內部的測量過程。這種情況getDefaultSize第一個參數size由getSuggestedMinimumWidth()返回

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

可以看到,如果View沒有設置背景,那麼返回android:minWidth這個屬性所指定的值,這個值可以爲0;如果View設置了背景,則返回android:minWidth和背景的最小寬度這兩者中的最大值。
mBackground.getMinimumWidth()返回的就是Drawable的原始寬度,前提是這個Drawable有原始寬度,否則返回0.

從getDefaultSize方法的實現來看,View的寬高由specSize決定,所以,直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於match_parent。原因,結合代碼和MeasureSpec的確定規則知,如果View在佈局中使用wrap_content,那麼它的SpecMode是AT_MOST,在這種模式下,它的寬高等於specSize也就是parentSize,效果和match_parent是一樣的。解決辦法就是給View指定一個默認寬高,並在wrap_content時設置寬高即可。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) { //wrap_content時的默認寬高
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }
  1. ViewGroup的measure過程

對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷調用所有子元素的measure方法。和View不同的是,ViewGroup是一個抽象類,沒有重寫View的onMeasure方法,但提供來一個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);
            }
        }
    }
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);
    }
  • 首先,在ViewGroup中的measureChildren()方法中會遍歷測量ViewGroup中所有的View,當View的可見性處於GONE狀態時,不對其進行測量。
  • 然後,測量某個指定的View時,根據父容器的MeasureSpec和子View的LayoutParams等信息計算子View的MeasureSpec
  • 最後,將計算出的MeasureSpec傳入View的measure方法

這裏ViewGroup沒有定義測量的具體過程,因爲ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類(比如LinearLayout)去實現。不同的ViewGroup子類有不同的佈局特性,這導致它們的測量細節各不相同,如果需要自定義測量過程,則子類可以重寫這個方法。(setMeasureDimension方法用於設置View的測量寬高,如果View沒有重寫onMeasure方法,則會默認調用getDefaultSize來獲得View的寬高)

LinearLayout的onMeasure方法實現解析(只看Vertical模式)

系統會遍歷子元素並對每個子元素執行measureChildBeforeLayout方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,並且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每測量一個子元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的margin等。

在Activity啓動時獲取某個View的測量寬高

由於View的measure過程和Activity的生命週期方法不是同步執行的,如果View還沒有測量完畢,那麼獲得的寬/高就是0。所以在onCreate、onStart、onResume中均無法正確得到某個View的寬高信息。解決方式如下:

  • Activity/View#onWindowFocusChanged:此時View已經初始化完畢,當Activity的窗口得到焦點和失去焦點時均會被調用一次,如果頻繁地進行onResume和onPause,那麼onWindowFocusChanged也會被頻繁地調用。
public void onWindowFocusChanged(boolean hasFocus){
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}
  • view.post(runnable): 通過post可以將一個runnable投遞到消息隊列的尾部,始化好了然後等待Looper調用次runnable的時候,View也已經初始化好了。
protected void onStart(){
    super.onStart();
    view.post(new Runnable(){
        @Overrie
        public void run(){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
  • ViewTreeObserver#addOnGlobalLayoutListener:當View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout方法將被回調。
protected void onStart(){
    super.onStart();
     ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        });
}
  • View.measure(int widthMeasureSpec, int heightMeasureSpec):match_parent時不知道parentSize的大小,測不出;具體數值時,直接makeMeasureSpec固定值,然後調用view..measure就可以了;wrap_content時,在最大化模式下,用View理論上能支持的最大值去構造MeasureSpec是合理的。
int widthMSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMSpec, heightMSpec);

View的繪製流程之Layout

layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它在onLayout中會遍歷所有子元素並調用其layout方法,在layout方法中onLayout方法又會被調用。layout方法確定View本身的位置,而onLayout方法則會確定所有子元素的位置。

先看ViewGroup的

 @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b); //調用View的layout方法
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b); //抽象方法,子類必須自己實現佈局

可以看到ViewGroup的layout方法實際還是調用View的layout方法,而onLayout則是一個空的抽象方法,子類自己實現。

在看View的layout方法:

@SuppressWarnings({"unchecked"})
    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); //會調setFrame方法

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b); //onLayout方法
            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;
    }
    //onLayout也是一個空方法
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

layout方法大致流程:首先會通過setFrame方法來設定View的四個頂點的位置,即View在父容器中的位置。然後,會執行到onLayout空方法,這個方法的用途是父容器確定子元素的位置。(即先確定自己本身位置,再遍歷確定子元素位置)子類如果是ViewGroup類型,則重寫這個方法,實現ViewGroup中所有View控件佈局流程。

LinearLayout的onLayout方法實現(Vertical方向)

其中會遍歷調用每個子View的setChildFrame方法爲子元素確定對應的位置。其中的childTop會逐漸增大,意味着後面的子元素會被放置在靠下的位置。而setChildFrame方法只是調用子view的layout方法而已。這樣父元素在layout方法中完成自己的定位,並通過onLayout方法去調用子元素的layout方法,子元素又通過自己的layout方法確定自己的位置,這樣一層一層傳遞完成整個View樹的layout。

==問題==:View的測量寬高和最終寬高有什麼區別?

在View的默認實現中,View的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成於View的measure過程,而最終寬/高形成於View的layout過程,即兩者的賦值時機不同,測量寬/高的賦值時機稍微早一些。 在一些特殊的情況下則兩者不相等:

  • 比如重寫View的layout方法,使最終寬度總是比測量寬/高大100px。
  • View需要多次measure才能確定自己的測量寬/高,在前幾次測量的過程中,其得出的測量寬/高有可能和最終寬/高不一致,但最終來說,測量寬/高還是和最終寬/高相同。

View的繪製流程之Draw

繪製基本上可以分爲六個步驟:

  • 首先繪製View的背景(canvas);
  • 如果需要的話,保持canvas的圖層,爲padding做準備;
  • 然後,繪製View的內容(onDraw);
  • 接着,繪製View的子View(dispatchDraw);
  • 如果需要的話,繪製View的padding邊緣並恢復圖層;
  • 最後,繪製View的裝飾(onDrawScrollBars例如滾動條等等)。
 public void draw(Canvas canvas) {
        if (mClipBounds != null) {
            canvas.clipRect(mClipBounds);
        }
        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)
         */
         //...
         // 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); //繪製子View

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

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

protected void dispatchDraw(Canvas canvas) {
//View中是空的,由子 View實現,比如ViewGroup中由自己的實現
    }

View繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層層傳遞來下去。

View中setWillNotDraw的作用

 public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

如果一個View不需要繪製任何內容,那麼設置WILL_NOT_DRAW這個標記位爲true以後,系統會進行相應的優化。

  • 默認情況下,View沒有啓用這個優化標記位,但是ViewGroup會默認啓用這個優化標記位。
  • 當我們的自定義控件繼承於ViewGroup並且本身不具備繪製功能時,就可以開啓這個標記位從而便於系統進行後續的優化。
  • 當明確知道一個ViewGroup需要通過onDraw來繪製內容時,我們需要顯示地關閉WILL_NOT_DRAW這個標記位。

自定義View

自定義View分類

  1. 繼承View重寫onDraw

主要用於實現一些不規則的效果。採用這種方式需要自己在onMeasure支持wrap_content,並且padding也需要在onDraw自己處理。

  1. 繼承ViewGroup派生特殊layout

用於實現自己定義的新佈局。需要合適地處理ViewGroup的測量、佈局兩個過程,同時處理子元素的測量和佈局過程。

  1. 繼承特定的View(比如TextView)

用於擴展某種已有的View的功能。這種方法可以不需要自己支持wrap_content和padding。

  1. 繼承特定的ViewGroup(比如LinearLayout)

用於擴展某種佈局。不需要自己處理ViewGroup的測量和佈局這兩個過程。

自定義View須知

  1. 讓View支持wrap_content

直接繼承View或ViewGroup的控件,如果沒在onMeasure中對wrap_content做處理,那麼wrap_content會無效。

  1. 如果有必要,支持padding

直接繼承View的控件,如果不再onDraw中處理padding,那麼padding屬性無法起作用。直接繼承自ViewGroup的控件需要在onMeasure和onLayout中考慮自身padding和子元素margin對其造成的影響,不如會無效。

  1. 儘量不要在View中使用Handler,沒必要

View內部本身提供來post系列方法,完成可以替代Handler

  1. View中如果又線程或動畫,要及時停止,參考View#onDetachedFromWindow

當包含此view的Activity退出或當前View被remove時,View的onDetachedFromWindow方法會被調用,在這個方法停止線程或動畫是很好的時機。

  1. View帶嵌套,需處理好滑動衝突

View有嵌套情形,需要自己處理好滑動衝突(外部攔截法或內部攔截法)

自定義View示例

image.png

1. 繼承View重寫onMeasure和onDraw方法

這種方式需要自己在onMeasure處理wrap_content和在onDraw處理padding。

public class CircleView extends View {

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //處理自定義屬性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); //CircleView_circle_color是屬性集_屬性使用_連接起來的固定格式
        a.recycle();
        
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //支持wrap_content,默認值200
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //處理padding
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
                radius, mPaint);
    }
}

添加自定義屬性步驟:

  • 在values目錄下創建自定義屬性XML文件attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CircleView"> //屬性集合
        <attr name="circle_color" format="color" /> //具體屬性
    </declare-styleable>

</resources>
  • 在View的構造方法解析自定義屬性並做處理
  • 在佈局文件中使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" //使用自定義屬性必須聲明,app是自定義的名稱,可改
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:orientation="vertical" >

    <com.ryg.chapter_4.ui.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"
        android:padding="20dp"
        app:circle_color="@color/light_green" /> //“app”和上面定義的一致即可

</LinearLayout>

2. 繼承ViewGroup派生layout

需要合適處理ViewGroup的測量、佈局以及子元素的測量佈局過程,這裏只看onMeasure和onLayout方法,完整可參看github

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

這裏兩點不規範,一是沒有子元素時不應該把寬高設爲0,而應該根據LayoutParams來處理;第二點在測量時沒有考慮它的padding和子元素的margin。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;
//遍歷子View,調用子View的layout方法
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

同樣沒有考慮它的padding和子元素的margin佔用

Requestlayout,onlayout,onDraw,DrawChild區別與聯繫?

  • requestLayout()方法 :會導致調用 measure()過程 和layout()過程,將會根據標誌位判斷是否需要ondraw。
  • onLayout()方法:如果該View是ViewGroup對象,需要實現該方法,對每個子視圖進行佈局。
  • onDraw()方法:繪製視圖本身 (每個View都需要重載該方法,ViewGroup一般不需要實現該方法)。
  • drawChild():去重新回調每個子視圖的draw()方法。

invalidate() 和 postInvalidate()的區別 ?

  • invalidate()與postInvalidate()都用於刷新View,主要區別是invalidate()在主線程中調用,若在子線程中使用需要配合handler;而postInvalidate()可在子線程中直接調用

參考書籍《Android開發藝術探究》第4章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章