Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義控件知識儲備-View的繪制流程

自定義控件知識儲備-View的繪制流程

編輯:關於Android編程

在自定義控件這個學習系列裡,首先寫篇文章記錄一下View的繪制流程,壓壓驚:-P。也為以後的自定義控件實踐打個基礎。雖然講解View工作流程的文章很多,其中不乏很多精品文章,不過自己能從中理清思路,以自己之言總結出來,也是十分必要的。好的,我要開始裝…不,總結了。

1. 前言

當我們打開手機,開始看朋友圈,刷微博的時候,我們有考慮過在我們眼前的一個個View是如何從無到有的展示在我們眼前的麼?有考慮過它們的感受麼?(神經病才去考慮(ノ??)ノ彡┻━┻……)。

當我們在一張紙上畫畫的時候,哪怕是簡單的一只小雞,我們也不得不考慮下面幾點:

這只雞得畫多大呀?多寬,多高?不能大的超過紙的范圍吧? 這只雞畫在紙的哪裡呢?是紙的中間還是靠下面一點呢? 確定好大小和位置了,該怎麼畫呢?公雞母雞?這只雞是什麼形狀(當然是雞形)?什麼顏色?

其實在屏幕上“畫”一個View跟上述的流程也很相似。同樣是經過了測量流程、布局流程以及繪制流程。我們都知道,Android界面布局是以一棵樹的結構形式展現的,看我們的xml布局文件也看的出來。而繪制出整個界面肯定是要遍歷整個View樹,對這棵樹的所有節點分別進行測量,布局和繪制。萬事皆有源頭,繪制這棵樹得從根節點頂級View開始畫起,也就是DecorView。至於啥是DecorView,大家可以自行去查閱資料。

系統內部會依次調用DecorView的measurelayoutdraw三大流程方法。measure方法又會調用onMeasure方法對它所有的子元素進行測量,如此反復調用下去就能完成整個View樹的遍歷測量。同樣的,layoutdraw兩個方法裡也會調用相似的方法去對整個View樹進行遍歷布局和繪制。

下面就以這三個流程來了解一下View從無到有的不容易。

 

2. 測量流程-measure

測量流程得分情況來看,如果是單身View,那自然是沒話說,自己照顧好自己,本分的測量好自己就行。而如果是為人父母的ViewGroup,那就得顧家了,除了測量好自己,還得去調用孩子們的measure方法讓孩子們都測量好自己。甚至很多時候,ViewGroup得先測量好孩子們,最後才能確定自己的測量大小。一把辛酸淚…(ノへ ̄、)

下面分別來看看View和ViewGroup的測量過程:

2.1 View的measure過程

View類的measure方法的簽名如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

看到這個方法,我得提出兩個問題:

形參widthMeasureSpecheightMeasureSpec是幾個意思?是用來測量自身大小的寬高麼? measure方法是final修飾的,那怎麼通過重寫此方法來實現自定義控件的測量方式呢?

要回答第1個問題,首先得弄清楚:在界面的繪制過程中,View的這個方法是被它的父控件調用的,也就是說widthMeasureSpecheightMeasureSpec是通過父控件傳遞進來的,如果這兩個參數是完全用來決定孩子View的大小,那孩子們也太沒主動權了。

 

事實上,這兩個參數在很大程度上是決定了一個View的尺寸的,只不過孩子View可能各有各的特點,它們是能根據自身的特點來進行調整的,具體的呢以後再說。先來具體的看看MeasureSpec:

測量規格MeasureSpec

widthMeasureSpec這樣的32位的int類型的數肯定是有自己的故事滴,它的高2位代表測量模式Mode,低30位代表測量大小Size。系統提供了一個MeasureSpec類來對這個參數進行操作,代碼如下:

  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 getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }


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

上面的代碼也不復雜,都是通過位運算來進行操作的。(我在平時位運算用的少,所以我還得慢慢捋一捋才看的明白。╥﹏╥…)不過,這樣做的好處就是更省內存,因為要是我來做的話,肯定是為這樣的測量規格定義一個類,裡面有mode和size兩個屬性,這樣每次就會new很多測量規格的對象了。

好了,喝口水,接著往下說。既然測量規格是由測量模式mode和測量大小size組成的,size好說,那測量模式mode代表什麼含義呢。由上面的代碼可知,測量模式有三類:

UNSPECIFIED

父控件不對你有任何限制,你想要多大給你多大,想上天就上天。這種情況一般用於系統內部,表示一種測量狀態。(這個模式主要用於系統內部多次Measure的情形,並不是真的說你想要多大最後就真有多大)

EXACTLY

父控件已經知道你所需的精確大小,你的最終大小應該就是這麼大。

AT_MOST

你的大小不能大於父控件給你指定的size,但具體是多少,得看你自己的實現。

上面的三種模式的區別我們弄清楚了,但是父控件是怎樣給它的孩子們構建好測量大小和測量模式的呢?這其中必有蹊跷。好吧,冤有頭債有主,我們得去ViewGroup類裡去找找看。ViewGroup裡提供了一個靜態方法getChildMeasureSpec用來獲取子控件的測量規格,下面是代碼和詳細注釋:

    /**
     *
     * 目標是將父控件的測量規格和child view的布局參數LayoutParams相結合,得到一個
     * 最可能符合條件的child view的測量規格。  

     * @param spec 父控件的測量規格
     * @param padding 父控件裡已經占用的大小
     * @param childDimension child view布局LayoutParams裡的尺寸
     * @return child view 的測量規格
     */
    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) {
        // 當父控件的測量模式 是 精確模式,也就是有精確的尺寸了
        case MeasureSpec.EXACTLY:
            //如果child的布局參數有固定值,比如"layout_width" = "100dp"
            //那麼顯然child的測量規格也可以確定下來了,測量大小就是100dp,測量模式也是EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 

            //如果child的布局參數是"match_parent",也就是想要占滿父控件
            //而此時父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果child的布局參數是"wrap_content",也就是想要根據自己的邏輯決定自己大小,
            //比如TextView根據設置的字符串大小來決定自己的大小
            //那就自己決定呗,不過你的大小肯定不能大於父控件的大小嘛
            //所以測量模式就是AT_MOST,測量大小就是父控件的size
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 當父控件的測量模式 是 最大模式,也就是說父控件自己還不知道自己的尺寸,但是大小不能超過size
        case MeasureSpec.AT_MOST:
            //同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也優先滿足孩子的需求
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            //child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無法確定自己大小
            //但同樣的,child的尺寸上限也是父控件的尺寸上限size
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            //child想要根據自己邏輯決定大小,那就自己決定呗
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                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);
    }

根據上面的代碼,可以列出默認情況下,View的測量規格的生成規則:

測量規格圖

現在我們知道了,View的測量規格是由父控件的測量規格自身的LayoutParams<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPrmyzay+9raotcSho7Kix9LU2sbVzajH6b/2z8KjrLvhwvrX48nPw+ax7bjxwO+1xLnm1PKho7WrysfEx8rH1NrG1c2ox+m/9s/Co6y2+NTaztLDx9fUtqjS5b/YvP7W0KOs09DKsbryyse4+b7dzNjT0LXEwt+8rciltcO1vbLiwb+55rjxtcSho8v50tSjrNXGztW6w9StwO2jrNLUsrux5NOmzfKx5LLFysfJz7LfoaM8L3A+DQo8cD694srNzepNZWFzdXJlU3BlY6Osvs3Iw87Sw8e72LW90ru/qsq8zOGz9rXEtdoyuPbOyszio7o8L3A+DQo8YmxvY2txdW90ZT4NCgltZWFzdXJlt723qMrHZmluYWzQ3srOtcSjrMTH1PXDtM2ouf3W2NC0tMu3vbeowLTKtc/W19S2qNLlv9i8/rXEsuLBv7e9yr3E2KO/PC9ibG9ja3F1b3RlPg0KPHA+ztLDx8C0v7S/tG1lYXN1cmW3vbeotcTKtc/Wo7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... 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("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } ... }

看到了我們熟悉的onMeasure方法啦,所以我們想要實現自己自定義控件的測量方式,就得重寫onMeasure方法。再來跟進看看onMeasure方法的實現:

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

這方法一層嵌一層的,還是從裡往外接著看吧,對於getSuggestedMinimumWidthgetSuggestedMinimumHeight方法,顧名思義,就是得到建議的最小的寬/高。什麼意思呢?以getSuggestedMinimumWidth為例:

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

mMinWidth屬性對應的就是xml布局裡的android:minWidth屬性,設置最小寬度。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始寬度,這個寬度跟背景的類型有關。比如我們給View的背景設置一張圖片,那這個方法返回的寬度就是圖片的寬度,而如果我們給View背景設置的是顏色,那麼這個方法返回的寬度則是0。具體的大家可以自行查閱Drawable尺寸的相關資料。所以,這個方法的返回的寬度是:如果View沒有設置背景,那就返回xml布局裡的android:minWidth屬性定義的值,默認為0;如果View設置了背景,就返回背景的寬度和mMinWidth中的最大值。

再來看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;//這裡的size就是上面getSuggestedMinimumWidth/height的返回值
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;//測量規格裡的尺寸
            break;
        }
        return result;
    }

可以看出,View在當測量模式為UNSPECIFIED時,返回的就是上面getSuggestedMinimumWidth/Height()方法裡的大小。其實這對我們自定義控件並沒有什麼影響,因為上文有提到過,UNSPECIFIED一般用於系統內部的測量過程,對我們正常邏輯沒什麼影響。我們的重點還是應該放在AT_MOSTEXACTLY兩種情況下。對於這兩種情況,getDefaultSize十分簡單粗暴,直接返回了specSize,也就是View的測量規格裡的測量尺寸。

這裡寫圖片描述,不知道大家在看完上面的代碼以後,有沒有發現一個“碧油雞”,在AT_MOSTEXACTLY兩種情況下返回的尺寸竟然都是specSize,這意味著什麼呢?

自定義View控件時,我們需要重寫onMeasure方法並設置wrap_content時自身的大小。否則在xml布局中使用wrap_content時與match_parent的效果一樣。

為什麼呢?如果View在xml布局中使用wrap_content,根據上面提到的規則表格,它的測量模式是AT_MOST模式,測量尺寸specSize是parentSize,而getDefaultSize方法在AT_MOST裡直接返回specSize,也就是等於父容器的剩余空間大小,這和match_parent是一樣的。所以我們需要自己來處理AT_MOST模式下的寬高。

一個重寫onMeasure方法來支持wrap_content屬性的模版如下:

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

        int wrapWidth,wrapHeight;//根據View的邏輯得到,比如TextView根據設置的文字計算wrap_content時的大小

        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(wrapWidth, wrapHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(wrapWidth, heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, wrapHeight);
        }
}

以上代碼可以直接應用到我們的自定義控件裡去,當然最重要的還是大家得對AT_MOST模式留點心,記得對它特別對待就行。

好的,我們再看最外層的方法setMeasuredDimension

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

 private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }    

setMeasuredDimension方法裡調用了setMeasuredDimensionRaw方法,在這個方法裡面,終於看到了我們熟悉的mMeasuredWidthmeasuredHeight的賦值語句。從此以後,我們就可以安心的調用View的getMeasureWidth()getMeasureHeight()方法了!(≧?≦)?

2.2 ViewGroup的measure過程

ViewGroup並沒有重寫View的onMeasure方法,這需要它的子類去根據相應的邏輯去實現,比如LinearLayout與RelativeLayout對child view的測量邏輯顯然是不同的。不過,ViewGroup倒是提供了一個measureChildren的方法,貌似可以用來測量child的樣子,看看源碼:

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

上面的代碼邏輯很清晰,就是遍歷每個孩子,調用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方法裡,會取出child的LayoutParams,再結合父控件的測量規格和已被占用的空間Padding,作為參數傳遞給getChildMeasureSpec方法,在getChildMeasureSpec裡會組合生成child控件的測量規格。getChildMeasureSpec方法的邏輯在上面的MeasureSpec部分有詳細說明。最後,當然還是得調用child的measure方法啦,讓孩子根據父母的指引去測量自己。

在我看來,我們在自己自定義控件時,上面的這兩個方法幾乎不會用到。因為measureChildren太過簡單粗暴,我們一般都會考慮孩子們之間的邏輯關系(順序、間隔等),再計算他們的測量規格。不過這個方法也給我們一點啟示,就是:

測量子元素時,對可見性為GONE的View要做特殊處理,一般來說就是跳過對它們的測量,來優化布局。

measureChild方法只考慮了父控件的padding,但是沒考慮到child view的margin,這就會導致child view在使用match_parent屬性的時候,margin屬性會有問題。(什麼?你說你自定義的ViewGroup對孩子不支持margin屬性不就不會有問題了麼…是是是,那當我沒說….)當然,ViewGroup裡為此也提供了另一個測量child的方法:

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

measureChildWithMargins方法,顧名思義,比measureChild方法多考慮了個margin。看源碼也看得出來,的確是這樣。所以一般情況下,這個方法使用的更多一些。

3.布局流程-layout

布局的流程就沒有測量流程那麼“蜿蜒曲折”了。對於單身View來說,調用layout方法確定好自己的位置,設置好位置屬性的值(mLeft/mRgiht,mTop/mBottom)就行。而對於父母ViewGroup來說,還得通過調用onLayout方法幫助孩子們確定好位置。來看看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);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList listenersCopy =
                        (ArrayList)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;
    }

 protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            ...

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            ...
        }
        return changed;
    }

從上面的代碼,能看到layout方法首先會調用setFrame方法來給View的四個頂點屬性賦值,即mLeft,mRight,mTop,mBottom四個值,此時這個View的位置就確定了。同時我們也就能通過調用getWidth()getHeight()方法來獲取View的實際寬高了。

接下來,onLayout方法才會被調用,這也意味著我們在自定義ViewGroup時,想要重寫onLayout方法給我們的子元素定位,是可以直接調用getWidth()getHeight()方法來獲取ViewGroup的真實寬高的。在View類裡的onLayout方法是個空方法,而在ViewGroup方法裡聲明成了抽象方法,所以繼承ViewGroup的類都得自己去實現自己定位子元素的邏輯。

最後,在layout方法的最後我們能看到一個OnLayoutChangeListener的集合,看名字我們也猜得出,這是View位置發生改變時的回調接口。所以我們可以通過addOnLayoutChangeListener方法可以監聽一個View的位置變化,並做出想要的響應。(看源碼的時候才發現這個回調接口的,以前都不知道。新技能get!︿( ̄︶ ̄)︿)

4.繪制流程-draw

繪制的流程也就是通過調用View的draw方法實現的。draw方法裡的邏輯看起來更清晰,我就不貼源碼了。一般是遵循下面幾個步驟:

繪制背景 – drawBackground() 繪制自己 – onDraw() 繪制孩子 – dispatchDraw() 繪制裝飾 – onDrawScrollbars()

由於不同的控件都有自己不同的繪制實現,所以View的onDraw方法肯定是空方法。而ViewGroup由於需要照顧孩子們的繪制,所以肯定在dispatchDraw方法裡遍歷調用了child的draw方法。不信?不信咱來看看ViewGroup裡重寫的dispatchDraw方法:

 protected void dispatchDraw(Canvas canvas) {

        ...

        for (int i = 0; i < childrenCount; i++) {
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }

        ...
    }    

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

ViewGroup裡的dispatchDraw方法遍歷調用drawChild方法,drawChild方法又調用了child的draw(canvas, this, drawingTime)方法,最後還是調用到了child的draw(canvas)方法。如此這般,繪制流程也就一層一層的傳遞下去了。

好的,說完了…(*???)…………………………………..

5.總結

我已經無力總結了,沒想到一篇總結的文章寫了我兩天半…。不過,自己在總結的過程中確實也學到了蠻多,加深了對View的繪制流程的理解,也弄清楚了一些模糊的知識點。當然了,也希望我的文章能對正在學Android開發的小伙伴們有所幫助。

當然了,這些都屬於自定義控件的基本功,還需要在實踐中多積累一些相關的經驗,並逐漸做到融會貫通,這樣才能提高自己的水平。keep going!

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved