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

在自定義控件這個學(xué)習(xí)系列里劳曹,首先寫篇文章記錄一下View的繪制流程,壓壓驚:-P欠气。也為以后的自定義控件實(shí)踐打個基礎(chǔ)。雖然講解View工作流程的文章很多镜撩,其中不乏很多精品文章预柒,不過自己能從中理清思路,以自己之言總結(jié)出來袁梗,也是十分必要的宜鸯。好的,我要開始裝...不遮怜,總結(jié)了淋袖。

1. 前言

當(dāng)我們打開手機(jī),開始看朋友圈锯梁,刷微博的時候即碗,我們有考慮過在我們眼前的一個個View是如何從無到有的展示在我們眼前的么?有考慮過它們的感受么陌凳?(神經(jīng)病才去考慮(ノ??)ノ彡┻━┻......)剥懒。

當(dāng)我們在一張紙上畫畫的時候,哪怕是簡單的一只小雞合敦,我們也不得不考慮下面幾點(diǎn):

  • 這只雞得畫多大呀初橘?多寬,多高蛤肌?不能大的超過紙的范圍吧壁却?
  • 這只雞畫在紙的哪里呢?是紙的中間還是靠下面一點(diǎn)呢裸准?
  • 確定好大小和位置了展东,該怎么畫呢?公雞母雞炒俱?這只雞是什么形狀(當(dāng)然是雞形)盐肃?什么顏色爪膊?

其實(shí)在屏幕上“畫”一個View跟上述的流程也很相似。同樣是經(jīng)過了測量流程砸王、布局流程以及繪制流程推盛。我們都知道,Android界面布局是以一棵樹的結(jié)構(gòu)形式展現(xiàn)的谦铃,看我們的xml布局文件也看的出來耘成。而繪制出整個界面肯定是要遍歷整個View樹,對這棵樹的所有節(jié)點(diǎn)分別進(jìn)行測量驹闰,布局和繪制瘪菌。萬事皆有源頭,繪制這棵樹得從根節(jié)點(diǎn)頂級View開始畫起嘹朗,也就是DecorView师妙。至于啥是DecorView,大家可以自行去查閱資料屹培。

系統(tǒng)內(nèi)部會依次調(diào)用DecorView的measure默穴,layoutdraw三大流程方法。measure方法又會調(diào)用onMeasure方法對它所有的子元素進(jìn)行測量褪秀,如此反復(fù)調(diào)用下去就能完成整個View樹的遍歷測量蓄诽。同樣的,layoutdraw兩個方法里也會調(diào)用相似的方法去對整個View樹進(jìn)行遍歷布局和繪制溜歪。

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

后退,我要開始裝逼了

2. 測量流程-measure

測量流程得分情況來看蝴猪,如果是單身View调衰,那自然是沒話說,自己照顧好自己自阱,本分的測量好自己就行嚎莉。而如果是為人父母的ViewGroup,那就得顧家了沛豌,除了測量好自己趋箩,還得去調(diào)用孩子們的measure方法讓孩子們都測量好自己。甚至很多時候加派,ViewGroup得先測量好孩子們叫确,最后才能確定自己的測量大小。一把辛酸淚...(ノへ ̄芍锦、)

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

2.1 View的measure過程

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

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

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

  1. 形參widthMeasureSpecheightMeasureSpec是幾個意思?是用來測量自身大小的寬高么娄琉?
  2. measure方法是final修飾的次乓,那怎么通過重寫此方法來實(shí)現(xiàn)自定義控件的測量方式呢吓歇?

要回答第1個問題,首先得弄清楚:在界面的繪制過程中票腰,View的這個方法是被它的父控件調(diào)用的城看,也就是說widthMeasureSpecheightMeasureSpec是通過父控件傳遞進(jìn)來的,如果這兩個參數(shù)是完全用來決定孩子View的大小杏慰,那孩子們也太沒主動權(quán)了测柠。

呵呵噠

事實(shí)上,這兩個參數(shù)在很大程度上是決定了一個View的尺寸的逃默,只不過孩子View可能各有各的特點(diǎn)鹃愤,它們是能根據(jù)自身的特點(diǎn)來進(jìn)行調(diào)整的,具體的呢以后再說完域。先來具體的看看MeasureSpec:

測量規(guī)格MeasureSpec

widthMeasureSpec這樣的32位的int類型的數(shù)肯定是有自己的故事滴,它的高2位代表測量模式Mode瘩将,低30位代表測量大小Size吟税。系統(tǒng)提供了一個MeasureSpec類來對這個參數(shù)進(jìn)行操作,代碼如下:

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

上面的代碼也不復(fù)雜姿现,都是通過位運(yùn)算來進(jìn)行操作的肠仪。(我在平時位運(yùn)算用的少,所以我還得慢慢捋一捋才看的明白备典。╥﹏╥...)不過异旧,這樣做的好處就是更省內(nèi)存,因?yàn)橐俏襾碜龅脑捥嵊叮隙ㄊ菫檫@樣的測量規(guī)格定義一個類吮蛹,里面有mode和size兩個屬性,這樣每次就會new很多測量規(guī)格的對象了拌屏。

好了潮针,喝口水,接著往下說倚喂。既然測量規(guī)格是由測量模式mode和測量大小size組成的每篷,size好說,那測量模式mode代表什么含義呢端圈。由上面的代碼可知焦读,測量模式有三類:

  • UNSPECIFIED

    父控件不對你有任何限制,你想要多大給你多大舱权,想上天就上天矗晃。這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量狀態(tài)刑巧。(這個模式主要用于系統(tǒng)內(nèi)部多次Measure的情形喧兄,并不是真的說你想要多大最后就真有多大)

  • EXACTLY

    父控件已經(jīng)知道你所需的精確大小无畔,你的最終大小應(yīng)該就是這么大。

  • AT_MOST

    你的大小不能大于父控件給你指定的size吠冤,但具體是多少浑彰,得看你自己的實(shí)現(xiàn)。

上面的三種模式的區(qū)別我們弄清楚了拯辙,但是父控件是怎樣給它的孩子們構(gòu)建好測量大小和測量模式的呢郭变?這其中必有蹊蹺。好吧涯保,冤有頭債有主诉濒,我們得去ViewGroup類里去找找看。ViewGroup里提供了一個靜態(tài)方法getChildMeasureSpec用來獲取子控件的測量規(guī)格夕春,下面是代碼和詳細(xì)注釋:

    /**
     *
     * 目標(biāo)是將父控件的測量規(guī)格和child view的布局參數(shù)LayoutParams相結(jié)合未荒,得到一個
     * 最可能符合條件的child view的測量規(guī)格。  
     
     * @param spec 父控件的測量規(guī)格
     * @param padding 父控件里已經(jīng)占用的大小
     * @param childDimension child view布局LayoutParams里的尺寸
     * @return child view 的測量規(guī)格
     */
    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) {
        // 當(dāng)父控件的測量模式 是 精確模式及志,也就是有精確的尺寸了
        case MeasureSpec.EXACTLY:
            //如果child的布局參數(shù)有固定值片排,比如"layout_width" = "100dp"
            //那么顯然child的測量規(guī)格也可以確定下來了,測量大小就是100dp速侈,測量模式也是EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            
            //如果child的布局參數(shù)是"match_parent"率寡,也就是想要占滿父控件
            //而此時父控件是精確模式,也就是能確定自己的尺寸了倚搬,那child也能確定自己大小了
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果child的布局參數(shù)是"wrap_content"冶共,也就是想要根據(jù)自己的邏輯決定自己大小,
            //比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
            //那就自己決定唄每界,不過你的大小肯定不能大于父控件的大小嘛
            //所以測量模式就是AT_MOST捅僵,測量大小就是父控件的size
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 當(dāng)父控件的測量模式 是 最大模式,也就是說父控件自己還不知道自己的尺寸盆犁,但是大小不能超過size
        case MeasureSpec.AT_MOST:
            //同樣的命咐,既然child能確定自己大小,盡管父控件自己還不知道自己大小谐岁,也優(yōu)先滿足孩子的需求
            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想要根據(jù)自己邏輯決定大小窜司,那就自己決定唄
            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);
    }

根據(jù)上面的代碼,可以列出默認(rèn)情況下航揉,View的測量規(guī)格的生成規(guī)則:

測量規(guī)格圖

(注:圖片來自任玉剛博客 任玉剛:Android View系統(tǒng)解析(下)

現(xiàn)在我們知道了塞祈,View的測量規(guī)格是由父控件的測量規(guī)格自身的LayoutParams共同決定的。并且在普通情況下帅涂,會滿足上面表格里的規(guī)則议薪。但是那是在普通情況下尤蛮,而在我們自定義控件中,有時候是根據(jù)特有的邏輯去得到測量規(guī)格的斯议。所以产捞,掌握好原理,以不變應(yīng)萬變才是上策哼御。

解釋完MeasureSpec坯临,就讓我們回到一開始提出的第2個問題:

  1. measure方法是final修飾的,那怎么通過重寫此方法來實(shí)現(xiàn)自定義控件的測量方式呢恋昼?

我們來看看measure方法的實(shí)現(xiàn):

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方法啦看靠,所以我們想要實(shí)現(xiàn)自己自定義控件的測量方式,就得重寫onMeasure方法液肌。再來跟進(jìn)看看onMeasure方法的實(shí)現(xiàn):

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屬性對應(yīng)的就是xml布局里的android:minWidth屬性,設(shè)置最小寬度空幻。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始寬度烁峭,這個寬度跟背景的類型有關(guān)。比如我們給View的背景設(shè)置一張圖片秕铛,那這個方法返回的寬度就是圖片的寬度约郁,而如果我們給View背景設(shè)置的是顏色,那么這個方法返回的寬度則是0但两。具體的大家可以自行查閱Drawable尺寸的相關(guān)資料鬓梅。所以,這個方法的返回的寬度是:如果View沒有設(shè)置背景谨湘,那就返回xml布局里的android:minWidth屬性定義的值绽快,默認(rèn)為0;如果View設(shè)置了背景紧阔,就返回背景的寬度和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;//測量規(guī)格里的尺寸
            break;
        }
        return result;
    }

可以看出,View在當(dāng)測量模式為UNSPECIFIED時擅耽,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小活孩。其實(shí)這對我們自定義控件并沒有什么影響,因?yàn)樯衔挠刑岬竭^乖仇,UNSPECIFIED一般用于系統(tǒng)內(nèi)部的測量過程憾儒,對我們正常邏輯沒什么影響询兴。我們的重點(diǎn)還是應(yīng)該放在AT_MOSTEXACTLY兩種情況下。對于這兩種情況起趾,getDefaultSize十分簡單粗暴诗舰,直接返回了specSize,也就是View的測量規(guī)格里的測量尺寸阳掐。

真相只有一個
始衅,不知道大家在看完上面的代碼以后,有沒有發(fā)現(xiàn)一個“碧油雞”缭保,在AT_MOSTEXACTLY兩種情況下返回的尺寸竟然都是specSize汛闸,這意味著什么呢?

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

為什么呢钳恕?如果View在xml布局中使用wrap_content别伏,根據(jù)上面提到的規(guī)則表格,它的測量模式是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;//根據(jù)View的邏輯得到托嚣,比如TextView根據(jù)設(shè)置的文字計算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);
        }
}

以上代碼可以直接應(yīng)用到我們的自定義控件里去巩检,當(dāng)然最重要的還是大家得對AT_MOST模式留點(diǎn)心,記得對它特別對待就行示启。

好的兢哭,我們再看最外層的方法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方法里調(diào)用了setMeasuredDimensionRaw方法,在這個方法里面夫嗓,終于看到了我們熟悉的mMeasuredWidthmeasuredHeight的賦值語句迟螺。從此以后,我們就可以安心的調(diào)用View的getMeasureWidth()getMeasureHeight()方法了啤月!(≧?≦)?

2.2 ViewGroup的measure過程

ViewGroup并沒有重寫View的onMeasure方法煮仇,這需要它的子類去根據(jù)相應(yīng)的邏輯去實(shí)現(xiàn),比如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);
            }
        }
    }

上面的代碼邏輯很清晰夹姥,就是遍歷每個孩子杉武,調(diào)用measureChild方法對其進(jìn)行測量,接著來看看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轻抱,再結(jié)合父控件的測量規(guī)格和已被占用的空間Padding,作為參數(shù)傳遞給getChildMeasureSpec方法旦部,在getChildMeasureSpec里會組合生成child控件的測量規(guī)格祈搜。getChildMeasureSpec方法的邏輯在上面的MeasureSpec部分有詳細(xì)說明。最后士八,當(dāng)然還是得調(diào)用child的measure方法啦容燕,讓孩子根據(jù)父母的指引去測量自己。

在我看來婚度,我們在自己自定義控件時蘸秘,上面的這兩個方法幾乎不會用到。因?yàn)?code>measureChildren太過簡單粗暴蝗茁,我們一般都會考慮孩子們之間的邏輯關(guān)系(順序醋虏、間隔等),再計算他們的測量規(guī)格哮翘。不過這個方法也給我們一點(diǎn)啟示颈嚼,就是:

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

measureChild方法只考慮了父控件的padding佩研,但是沒考慮到child view的margin,這就會導(dǎo)致child view在使用match_parent屬性的時候霞揉,margin屬性會有問題旬薯。(什么?你說你自定義的ViewGroup對孩子不支持margin屬性不就不會有問題了么...是是是适秩,那當(dāng)我沒說....)當(dāng)然绊序,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來說倍奢,調(diào)用layout方法確定好自己的位置,設(shè)置好位置屬性的值(mLeft/mRgiht,mTop/mBottom)就行垒棋。而對于父母ViewGroup來說卒煞,還得通過調(diào)用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<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;
    }
    
 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方法首先會調(diào)用setFrame方法來給View的四個頂點(diǎn)屬性賦值畔裕,即mLeft,mRight乖订,mTop扮饶,mBottom四個值,此時這個View的位置就確定了垢粮。同時我們也就能通過調(diào)用getWidth()getHeight()方法來獲取View的實(shí)際寬高了贴届。

接下來醉锅,onLayout方法才會被調(diào)用卒密,這也意味著我們在自定義ViewGroup時瞭吃,想要重寫onLayout方法給我們的子元素定位轿亮,是可以直接調(diào)用getWidth()getHeight()方法來獲取ViewGroup的真實(shí)寬高的攻走。在View類里的onLayout方法是個空方法尉辑,而在ViewGroup方法里聲明成了抽象方法殿如,所以繼承ViewGroup的類都得自己去實(shí)現(xiàn)自己定位子元素的邏輯柴墩。

最后君仆,在layout方法的最后我們能看到一個OnLayoutChangeListener的集合翩概,看名字我們也猜得出,這是View位置發(fā)生改變時的回調(diào)接口返咱。所以我們可以通過addOnLayoutChangeListener方法可以監(jiān)聽一個View的位置變化钥庇,并做出想要的響應(yīng)。(看源碼的時候才發(fā)現(xiàn)這個回調(diào)接口的咖摹,以前都不知道评姨。新技能get!︿( ̄︶ ̄)︿)

4.繪制流程-draw

繪制的流程也就是通過調(diào)用View的draw方法實(shí)現(xiàn)的萤晴。draw方法里的邏輯看起來更清晰吐句,我就不貼源碼了。一般是遵循下面幾個步驟:

  1. 繪制背景 -- drawBackground()
  2. 繪制自己 -- onDraw()
  3. 繪制孩子 -- dispatchDraw()
  4. 繪制裝飾 -- onDrawScrollbars()

由于不同的控件都有自己不同的繪制實(shí)現(xiàn)店读,所以View的onDraw方法肯定是空方法嗦枢。而ViewGroup由于需要照顧孩子們的繪制,所以肯定在dispatchDraw方法里遍歷調(diào)用了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方法遍歷調(diào)用drawChild方法侣诺,drawChild方法又調(diào)用了child的draw(canvas, this, drawingTime)方法,最后還是調(diào)用到了child的draw(canvas)方法择葡。如此這般紧武,繪制流程也就一層一層的傳遞下去了。

好的敏储,說完了...(*???).........................................

5.總結(jié)

我已經(jīng)無力總結(jié)了阻星,沒想到一篇總結(jié)的文章寫了我兩天半...
哭泣

。不過已添,自己在總結(jié)的過程中確實(shí)也學(xué)到了蠻多妥箕,加深了對View的繪制流程的理解,也弄清楚了一些模糊的知識點(diǎn)更舞。當(dāng)然了畦幢,也希望我的文章能對正在學(xué)Android開發(fā)的小伙伴們有所幫助。

當(dāng)然了缆蝉,這些都屬于自定義控件的基本功宇葱,還需要在實(shí)踐中多積累一些相關(guān)的經(jīng)驗(yàn),并逐漸做到融會貫通刊头,這樣才能提高自己的水平黍瞧。keep going!

6.預(yù)告

下一篇文章打算記錄LayoutParams相關(guān)的一些知識。敬請關(guān)注......

我是蘑菇君原杂,我為自己帶鹽

7.參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市穿肄,隨后出現(xiàn)的幾起案子年局,更是在濱河造成了極大的恐慌,老刑警劉巖咸产,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矢否,死亡現(xiàn)場離奇詭異,居然都是意外死亡脑溢,警方通過查閱死者的電腦和手機(jī)兴喂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來焚志,“玉大人,你說我怎么就攤上這事畏鼓〗闯辏” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵云矫,是天一觀的道長膳沽。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么挑社? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任陨界,我火速辦了婚禮,結(jié)果婚禮上痛阻,老公的妹妹穿的比我還像新娘菌瘪。我一直安慰自己,他們只是感情好阱当,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布俏扩。 她就那樣靜靜地躺著,像睡著了一般弊添。 火紅的嫁衣襯著肌膚如雪录淡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天油坝,我揣著相機(jī)與錄音嫉戚,去河邊找鬼。 笑死澈圈,一個胖子當(dāng)著我的面吹牛彬檀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播极舔,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼凤覆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拆魏?” 一聲冷哼從身側(cè)響起盯桦,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渤刃,沒想到半個月后拥峦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卖子,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年略号,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洋闽。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡玄柠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诫舅,到底是詐尸還是另有隱情羽利,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布刊懈,位于F島的核電站这弧,受9級特大地震影響娃闲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜匾浪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一皇帮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛋辈,春花似錦属拾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挂洛,卻和暖如春礼预,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虏劲。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工托酸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柒巫。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓励堡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親堡掏。 傳聞我的和親對象是個殘疾皇子应结,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容