Android基礎(chǔ)之View的繪制

Android中的任何一個(gè)布局、控件都是直接或間接繼承自View的,要想開發(fā)一個(gè)Android App,就肯定少不了要和View打交道。盡管Android已經(jīng)提供了豐富的控件和布局,但是要想開發(fā)出有自己特色的App,自定義View是必須要掌握的。那么,今天我們就來聊聊View繪制的相關(guān)內(nèi)容。
本文的要點(diǎn)如下:

  • View簡(jiǎn)介
  • MeasureSpecs類
  • onMeasure
    • 單一View的測(cè)量
    • ViewGroup的測(cè)量
  • onLayout
    • 單一View的layout過程
    • ViewGroup的layout過程
  • onDraw
  • 總結(jié)

View簡(jiǎn)介

在Android系統(tǒng)中View是所有控件的基類,其中也包括ViewGroup在內(nèi),ViewGroup是代表著控件的集合,其中可以包含多個(gè)View控件。
從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個(gè)界面的控件形成了一個(gè)樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測(cè)量與繪制下層的控件,并傳遞交互事件。
在每棵控件樹的頂部都存在著一個(gè)ViewParent對(duì)象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統(tǒng)一調(diào)度和分配,從而對(duì)整個(gè)視圖進(jìn)行整體控制,如下圖所示:



繪制出整個(gè)界面肯定是要遍歷整個(gè)View樹,對(duì)這棵樹的所有節(jié)點(diǎn)分別進(jìn)行測(cè)量,布局和繪制。萬事皆有源頭,繪制得從根節(jié)點(diǎn)頂級(jí)View開始畫起,即DecorView。
系統(tǒng)內(nèi)部會(huì)依次調(diào)用DecorView的measure,layout和draw三大流程方法。measure方法又會(huì)調(diào)用onMeasure方法對(duì)它所有的子元素進(jìn)行測(cè)量,如此反復(fù)調(diào)用下去就能完成整個(gè)View樹的遍歷測(cè)量。同樣的,layout和draw兩個(gè)方法里也會(huì)調(diào)用相似的方法去對(duì)整個(gè)View樹進(jìn)行遍歷布局和繪制。

View的構(gòu)造函數(shù):共有4個(gè),具體如下:

    // 如果View是在Java代碼里面new的,則調(diào)用第一個(gè)構(gòu)造函數(shù)
 public CarsonView(Context context) {
        super(context);
    }

    // 如果View是在.xml里聲明的,則調(diào)用第二個(gè)構(gòu)造函數(shù)
    // 自定義屬性是從AttributeSet參數(shù)傳進(jìn)來的
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 不會(huì)自動(dòng)調(diào)用
    // 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
    // 如View有style屬性時(shí)
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用
    // 不會(huì)自動(dòng)調(diào)用
    // 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
    // 如View有style屬性時(shí)
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

View的位置參數(shù)
View的位置由4個(gè)頂點(diǎn)決定(View的位置是相對(duì)于父控件而言的

  • Top:子View上邊界到父view上邊界的距離
  • Left:子View左邊界到父view左邊界的距離
  • Bottom:子View下邊距到父View上邊界的距離
  • Right:子View右邊界到父view左邊界的距離

MeasureSpecs類

MeasureSpecs類是View的內(nèi)部類,用一個(gè)變量封裝了兩個(gè)數(shù)據(jù)(size、mode),其目的是減少對(duì)象的內(nèi)存分配。

public static class MeasureSpec {
        //省略了部分不關(guān)鍵代碼
        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;
        // 通過Mode 和 Size 生成新的SpecMode
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        //獲取測(cè)量模式(Mode)
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
            //原理:保留measureSpec的高2位(即測(cè)量模式)、使用0替換后30位
            //例如10 00..00100 & 11 00..00(11后跟30個(gè)0) = 10 00..00(AT_MOST)
            //這樣就得到了mode的值

        }
        //獲取測(cè)量大?。⊿ize)
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
            // 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個(gè)1),
            //將32,31位替換成0也就是去掉mode,保留后30位的size  
        }
    }

測(cè)量規(guī)格(MeasureSpec) = 測(cè)量模式(mode) + 測(cè)量大小(size)。
其中,測(cè)量模式占最高兩位,測(cè)量大小則是MeasureSpec的低30位。測(cè)量模式(Mode)的類型有3種:UNSPECIFIED、EXACTLYAT_MOST。具體如下:

UNSPECIFIED:父View不約束子View(即子View可以獲取任意尺寸),多用于系統(tǒng)內(nèi)部View(ListView,ScrollView等),自定義View一般用不到。(這個(gè)模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,并不是真的說你想要多大最后就真有多大)
EXACTLY(精確模式):父View會(huì)為子View指定一個(gè)確切尺寸,子View必須在該尺寸之內(nèi)。對(duì)應(yīng)LayoutParams中的match_parent或具體數(shù)值。
AT_MOST(最大模式):父容器為子視圖指定一個(gè)最大尺寸SpecSize,View的大小不能大于這個(gè)值。對(duì)應(yīng)LayoutParams中的wrap_content。

onMeasure

View的繪制流程中,第一步就是測(cè)量,即onMeasure()方法。我們知道,自定義View的類型可以分為兩種,一種繼承View,一種繼承ViewGroup(繼承現(xiàn)有View的也可以根據(jù)繼承的View的不同歸入這兩種類型之中)。那么測(cè)量的過程也有兩種:

  1. 單一View的測(cè)量
  2. ViewGroup的測(cè)量
    那么下面我們就分別來看看兩種流程的不同。

單一View的measure過程

這種情況相對(duì)而言比較簡(jiǎn)單,不用考慮子View,只有一個(gè)原始的View,通過measure()即可完成測(cè)量,具體過程如下:



那么我們來看看整個(gè)流程的源碼:

//measure是測(cè)量流程的開始,由于是final類型,因此不能被重寫,
//主要是用來進(jìn)行基本測(cè)量邏輯的判斷。
//里面調(diào)用onMeasure進(jìn)行測(cè)量邏輯。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {           
            onMeasure(widthMeasureSpec, heightMeasureSpec);//具體的測(cè)量邏輯
        } else {
            ...    
    }

measure方法是測(cè)量過程中最先調(diào)用的方法,View的這個(gè)方法是被它的父控件調(diào)用的。由于measure是final類型,不能被子類重寫,那么就只能重寫onMeasure方法來實(shí)現(xiàn)測(cè)量邏輯了。

//在onMeasure中就做了兩件事,
//1是根據(jù)View寬/高的測(cè)量規(guī)格用getDefaultSize()方法計(jì)算View的寬/高值,
//2是用setMeasuredDimension()方法存儲(chǔ)測(cè)量后的View寬 / 高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}
protected int getSuggestedMinimumWidth() {
        //如果有設(shè)置背景,則獲取背景的寬度,如果沒有設(shè)置背景,則取xml中android:minWidth的值。
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

onMeasure里面用到了getSuggestedMinimumWidth和getSuggestedMinimumHeight,這兩個(gè)方法差不多的,我們就以getSuggestedMinimumWidth為例。mMinWidth屬性對(duì)應(yīng)的就是xml布局里的android:minWidth屬性,設(shè)置最小寬度。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始寬度,這個(gè)寬度跟背景的類型有關(guān)。比如我們給View的背景設(shè)置一張圖片,那這個(gè)方法返回的寬度就是圖片的寬度,而如果我們給View背景設(shè)置的是顏色,那么這個(gè)方法返回的寬度則是0。
所以,這個(gè)方法的返回的寬度是:如果View沒有設(shè)置背景,那就返回xml布局里的android:minWidth屬性定義的值,默認(rèn)為0;如果View設(shè)置了背景,就返回背景的寬度和mMinWidth中的最大值。

//存儲(chǔ)測(cè)量后的View寬 / 高,該方法即為我們重寫onMeasure()所要實(shí)現(xiàn)的最終目的。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        //判斷該view布局模式是否有一些特殊的邊界
        boolean optical = isLayoutModeOptical(this);
        ////判斷view和該view的父view的布局模式情況,如果兩者不同步,則進(jìn)行子view的size大小的修改
        if (optical != isLayoutModeOptical(mParent)) {
//有兩種情況會(huì)進(jìn)入到該if條件,
//一是子view有特殊的光學(xué)邊界,而父view沒有,此時(shí)optical為true,
//一種是父view有一個(gè)特殊的光學(xué)邊界,而子view沒有,此時(shí)optical為false

            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        //存儲(chǔ)測(cè)量后的View寬 / 高的實(shí)際邏輯
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
//根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;// 設(shè)置默認(rèn)大小
        // 獲取寬/高測(cè)量規(guī)格的模式 & 測(cè)量大小
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            // 模式為UNSPECIFIED時(shí),使用提供的默認(rèn)大小 = 參數(shù)Size
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            //模式為AT_MOST,EXACTLY時(shí),
            //使用View測(cè)量后的寬/高值 = measureSpec中的Size
            result = specSize;
            break;
        }
        return result;
    }

可以看出,View在當(dāng)測(cè)量模式為UNSPECIFIED時(shí),返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小。其實(shí)這對(duì)我們自定義控件并沒有什么影響,因?yàn)閁NSPECIFIED模式是給系統(tǒng)內(nèi)部用的。我們的重點(diǎn)還是應(yīng)該放在AT_MOST和EXACTLY兩種情況下。對(duì)于這兩種情況,getDefaultSize十分簡(jiǎn)單粗暴,直接返回了specSize,也就是View的測(cè)量規(guī)格里的測(cè)量尺寸。

這里就出現(xiàn)了一個(gè)問題在AT_MOST和EXACTLY兩種情況下返回的尺寸竟然都是specSize。
因此在自定義View控件時(shí),我們需要重寫onMeasure方法并設(shè)置wrap_content時(shí)自身的大小。否則在xml布局中使用wrap_content時(shí)與match_parent的效果將會(huì)是一樣。

至此,單一View的寬/高值已經(jīng)測(cè)量完成,即對(duì)于單一View的measure過程已經(jīng)完成。
小小的總結(jié)一下,其是前面的源碼只是為了對(duì)View的測(cè)量有個(gè)完整的概念,清楚整個(gè)流程,主要我們實(shí)現(xiàn)還是在onMeasure方法中,因此可以寫一個(gè)自定義View的onMeasure方法的通用模版,其實(shí)最關(guān)鍵的也就是在AT_MOST模式時(shí)進(jìn)行特殊處理,畢竟父類的onMeasure已經(jīng)實(shí)現(xiàn)了大部分邏輯:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 必須調(diào)用,因?yàn)楦割愡€是實(shí)現(xiàn)了很多東西的。
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 寬的測(cè)量模式
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
       // 寬的測(cè)量尺寸
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        // 高度的測(cè)量模式
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        // 高度的測(cè)量尺寸
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //根據(jù)View的邏輯得到,比如TextView根據(jù)設(shè)置的文字計(jì)算wrap_content時(shí)的大小。
        //這兩個(gè)數(shù)據(jù)變量要根據(jù)實(shí)現(xiàn)需求計(jì)算。
        int wrapWidth,wrapHeight;
        
        // 如果有測(cè)量模式是AT_MOST則需要進(jìn)行特殊處理
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            //長(zhǎng)寬都是AT_MOST,則都需要計(jì)算
            setMeasuredDimension(wrapWidth, wrapHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            //只有寬是AT_MOST,則長(zhǎng)用測(cè)量尺寸
            setMeasuredDimension(wrapWidth, heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            //只有長(zhǎng)是AT_MOST,則寬用測(cè)量尺寸
            setMeasuredDimension(widthSpecSize, wrapHeight);
        }
}

ViewGroup的measure過程

ViewGroup的情況就復(fù)雜一些了,畢竟它還有一堆子View要考慮,大多數(shù)時(shí)候要先確定子View的大小,再確定ViewGroup的的大小。不過原理也很簡(jiǎn)單,就是遍歷測(cè)量所有子View的尺寸然后將所有子View的尺寸進(jìn)行合并,最終得到ViewGroup父視圖的測(cè)量值。

看過源碼就知道ViewGroup并沒有重寫View的onMeasure方法,為什么呢?顯然這需要它的子類去根據(jù)相應(yīng)的邏輯去實(shí)現(xiàn),比如LinearLayout與RelativeLayout對(duì)child View的測(cè)量邏輯顯然是不同的。這個(gè)也是單一View的measure過程與ViewGroup過程最大的不同。

不過,ViewGroup倒是提供了一個(gè)measureChildren的方法,貌似可以用來測(cè)量child的樣子,看看源碼:

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

這個(gè)方法的邏輯就很清晰嘛,就是遍歷子View,調(diào)用measureChild方法對(duì)其進(jìn)行測(cè)量,那么我們接著來看看measureChild方法:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //取出子View的LayoutParams
        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方法里,會(huì)取出child的LayoutParams,再結(jié)合父控件的測(cè)量規(guī)格已被占用的空間Padding,作為參數(shù)傳遞給getChildMeasureSpec方法,在getChildMeasureSpec里會(huì)組合生成child控件的測(cè)量規(guī)格。

getChildMeasureSpec是ViewGroup里提供的一個(gè)靜態(tài)方法,用來用來獲取子控件的測(cè)量規(guī)格。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //父view的測(cè)量模式
        int specMode = MeasureSpec.getMode(spec);
        //父view的測(cè)量大小
        int specSize = MeasureSpec.getSize(spec);
        //通過父view計(jì)算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個(gè)值)
        int size = Math.max(0, specSize - padding);
        //子view想要的實(shí)際大小和模式(需要計(jì)算)  
        int resultSize = 0;
        int resultMode = 0;
        
        switch (specMode) {
        /// 當(dāng)父控件的測(cè)量模式 是 精確模式,也就是有精確的尺寸了
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //如果child的布局參數(shù)有固定值(大于0),比如"layout_width" = "100dp"
                //那么顯然child的測(cè)量規(guī)格也可以確定下來了,測(cè)量大小就是100dp,測(cè)量模式也是EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //當(dāng)子view的LayoutParams為MATCH_PARENT時(shí)(-1)  
                //此時(shí)父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了
                //子view大小為父view大小,模式為EXACTLY  
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //當(dāng)子view的LayoutParams為WRAP_CONTENT時(shí)(-2) 
                //比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
                //那就自己決定唄,不過你的大小肯定不能大于父控件的大小嘛
                //所以測(cè)量模式就是AT_MOST,測(cè)量大小就是父控件的size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 當(dāng)父控件的測(cè)量模式是AT_MOST時(shí),父view強(qiáng)加給子view一個(gè)最大的值
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也會(huì)優(yōu)先滿足孩子的需求
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                 //child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無法確定自己大小
                //但同樣的,child的尺寸上限也是父控件的尺寸上限size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //child想要根據(jù)自己邏輯決定大小,那就自己決定唄
                //同樣的,child的尺寸上限也是父控件的尺寸上限size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

         // 當(dāng)父view的模式為UNSPECIFIED時(shí),父容器不對(duì)view有任何限制,要多大給多大
         // 多見于ListView、GridView  
        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);
    }

總結(jié)下來邏輯如下:
!](https://upload-images.jianshu.io/upload_images/17755742-2a1a0809d563a88b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

至此,ViewGroup的測(cè)量流程也基本結(jié)束了,整體的流程如下圖:


不過還有一個(gè)問題不知道大家注意到?jīng)]有,measureChild方法只考慮了父View的padding,但是沒考慮到子View的margin。這就會(huì)導(dǎo)致子view在使用match_parent屬性的時(shí)候,margin屬性會(huì)有問題。
當(dāng)然,ViewGroup也考慮到了這個(gè)問題,為此也提供了另一個(gè)測(cè)量child的方法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);
    }

measureChildWithMargins方法,根據(jù)名字也能看出來,比measureChild方法多考慮了個(gè)margin,源碼也跟前面的差不多,只是將margin考慮了進(jìn)去,所以一般情況下,這個(gè)方法使用的更多一些。

至此,自定義View的中最重要、最復(fù)雜的measure過程就全部總結(jié)完了。下面就該Layout過程了。

onLayout

類似measure過程,layout過程根據(jù)View的類型分為2種情況,單一View和ViewGroup。對(duì)于單身View來說,一人吃飽全家不餓,調(diào)用layout方法確定好自己的位置,設(shè)置好位置屬性的值(mLeft/mRgiht,mTop/mBottom)就行。而對(duì)于父母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;
        }
        //當(dāng)前視圖的四個(gè)頂點(diǎn)
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //調(diào)用setFrame / setOpticalFrame方法來給View的四個(gè)頂點(diǎn)屬性賦值,
        //即mLeft,mRight,mTop,mBottom四個(gè)值
        //判斷當(dāng)前View大小和位置是否發(fā)生了變化 & 返回 
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //調(diào)用onLayout方法
            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;
            //監(jiān)聽View位置變化
            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);
        }
    }

盡管代碼有點(diǎn)長(zhǎng),不過不難看出layout方法首先會(huì)調(diào)用isLayoutModeOptical這個(gè)方法,判斷是否有光學(xué)邊界的(光學(xué)邊界這里暫時(shí)用不到,其實(shí)關(guān)鍵是我也不會(huì),想深入了解的請(qǐng)自行谷歌),之后調(diào)用setFrame或者setOpticalFrame方法來給View的四個(gè)頂點(diǎn)屬性賦值,即mLeft,mRight,mTop,mBottom四個(gè)值。那么我們?cè)賮砜纯磗etOpticalFrame方法:

 private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

可以看到,這個(gè)setOpticalFrame方法,最終也是調(diào)用了setFrame,那好我們可以直接繼續(xù)看setFrame方法了:

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

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

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

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            //省略部分代碼
            return changed;
    }

不難看出,setFrame中先比較了新位置和老位置是否有差異,如果有差異則會(huì)調(diào)用sizechanged來更新View的位置。

setFrame后這個(gè)View的位置就確定了。之后我們也就能通過調(diào)用getWidth()和getHeight()方法來獲取View的實(shí)際寬高了。
然后,才會(huì)調(diào)用onLayout方法,由于單一View是沒有子View的,因此在View類里的onLayout方法是個(gè)空方法。

另外,在layout方法的最后我們能看到一個(gè)OnLayoutChangeListener的集合,光看名字我們也知道,這是View位置發(fā)生改變時(shí)的回調(diào)接口。所以我們可以通過addOnLayoutChangeListener方法可以監(jiān)聽一個(gè)View的位置變化,并做出想要的響應(yīng)。(不看源碼根本不知道還有這樣的方法。。。)

ViewGroup的layout過程

ViewGroup的layout過程就比View復(fù)雜一些了,大致分為兩步,首先用layout方法計(jì)算自身ViewGroup的位置,之后在onLayout中遍歷子View并且確定自身子View在ViewGroup的位置(調(diào)用子View 的 layout方法)。

其實(shí),ViewGroup的Layout的關(guān)鍵就是實(shí)現(xiàn)onLayout方法,在ViewGroup中onLayout方法被聲明成了抽象方法,這就強(qiáng)制繼承ViewGroup的類都得自己去實(shí)現(xiàn)自己定位子元素的邏輯。

由于onLayout方法之前的流程和View是一樣的,因此就不再贅述了。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     // changed 當(dāng)前View的大小和位置改變了 
     // left 左部位置
     // top 頂部位置
     // right 右部位置
     // bottom 底部位置

     // 遍歷子View:循環(huán)所有子View
          for (int i=0; i<getChildCount(); i++) {
              View child = getChildAt(i);   

              // 計(jì)算當(dāng)前子View的四個(gè)位置值
                //位置的計(jì)算邏輯
                //TODO
                // 需自己實(shí)現(xiàn),也是自定義View的關(guān)鍵

                // 對(duì)計(jì)算后的位置值進(jìn)行賦值
                int mLeft  = Left
                int mTop  = Top
                int mRight = Right
                int mBottom = Bottom

              // 根據(jù)上述4個(gè)位置的計(jì)算值,設(shè)置子View的4個(gè)頂點(diǎn):調(diào)用子view的layout() & 傳遞計(jì)算過的參數(shù)
              // 即確定了子View在父容器的位置
              child.layout(mLeft, mTop, mRight, mBottom);
              // 該過程類似于單一View的layout過程中的layout()和onLayout(),此處不作過多描述
          }
      }
  }

onDraw

終于,在測(cè)量完畢,布局完成之后,我們來到了View繪制的最后一步,那就是將View繪制到屏幕上。不論是View還是ViewGroup,都是調(diào)用draw方法完成繪制,我們來看看draw的源碼:

public void draw(Canvas canvas) {
      //省略部分代碼
      int saveCount;
      // 步驟1: 繪制本身View背景
      if (!dirtyOpaque) {
            drawBackground(canvas);
      }
      final int viewFlags = mViewFlags;
      if (!verticalEdges && !horizontalEdges) {

        // 步驟2:繪制本身View內(nèi)容
        if (!dirtyOpaque) 
            onDraw(canvas);
        // View 中:默認(rèn)為空實(shí)現(xiàn),需復(fù)寫
        // ViewGroup中:需復(fù)寫

        // 步驟3:繪制子View
        // 由于單一View無子View,故View 中:默認(rèn)為空實(shí)現(xiàn)
        // ViewGroup中:系統(tǒng)已經(jīng)復(fù)寫好對(duì)其子視圖進(jìn)行繪制我們不需要復(fù)寫
        dispatchDraw(canvas);

        // 步驟4:繪制裝飾,如滑動(dòng)條、前景色等等
        onDrawScrollBars(canvas);
        return;
    }
    //省略部分代碼
}

和Measure以及Layout過程一樣,View和ViewGroup是有些區(qū)別的,不過根據(jù)draw方法的源碼來看,整體流程都是下面幾個(gè)步驟:

  1. 繪制背景 -- drawBackground()
  2. 繪制自己 -- onDraw()
  3. 繪制孩子 -- dispatchDraw()
  4. 繪制裝飾 -- onDrawScrollbars()
    View和ViewGroup最大的差別就是dispatchDraw()方法,由于View中不用考慮子View,那么dispatchDraw()就是一個(gè)空實(shí)現(xiàn),而ViewGroup則必須要實(shí)現(xiàn)dispatchDraw()。

那么我們?cè)賮硪徊揭徊娇纯丛创a中都做了什么:
1. 繪制背景 -- drawBackground()

private void drawBackground(Canvas canvas) {
        //mBackground是該View的背景參數(shù),比如背景顏色
        // 獲取背景 drawable
        final Drawable background = mBackground;
        //沒有背景則直接結(jié)束方法
        if (background == null) {
            return;
        }

        //根據(jù)在 layout 過程中獲取的 View 位置的四個(gè)參數(shù)來確定背景的邊界
        setBackgroundBounds();

        //省略部分代碼

        //獲取當(dāng)前View的mScrollX和mScrollY值
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            // 調(diào)用 Drawable 的 draw 方法繪制背景
            background.draw(canvas);
        } else {
            //如果scrollX和scrollY有值,則對(duì)canvas的坐標(biāo)進(jìn)行偏移,再繪制背景
            canvas.translate(scrollX, scrollY);
            // 調(diào)用 Drawable 的 draw 方法繪制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

2.繪制自己 -- onDraw()
由于不同的控件都有自己不同的繪制實(shí)現(xiàn),所以View的onDraw方法肯定是空方法。在自定義繪制過程中,需由子類去實(shí)現(xiàn)復(fù)寫該方法,從而繪制自身的內(nèi)容。也就是說我們?cè)谧远xView的時(shí)候要根據(jù)實(shí)際需求對(duì)onDraw方法進(jìn)行實(shí)現(xiàn)。

3.繪制孩子 -- dispatchDraw()

protected void dispatchDraw(Canvas canvas) {
        ......

         // 1. 遍歷子View
        final int childrenCount = mChildrenCount;
        ......

        for (int i = 0; i < childrenCount; i++) {
                ......
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                        // 繪制子View視圖
                        more |= drawChild(canvas, transientChild, drawingTime);
                }
                ....
        }
    }
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
}

不難看出,dispatchDraw的邏輯就是遍歷繪制子View,即遍歷調(diào)用drawChild方法,drawChild方法又調(diào)用了child的draw(canvas, this, drawingTime)方法,最后還是調(diào)用到了child的draw(canvas)方法,這樣繪制流程也就一層一層的傳遞下去了。

4.繪制裝飾 -- onDrawScrollbars()

public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

這一步的目的是繪制裝飾,如 滾動(dòng)指示器、滾動(dòng)條、和前景等。
至此,View的draw過程分析完畢。

總結(jié)

View的繪制流程可以總結(jié)為下圖:


從View的測(cè)量、布局和繪制原理來看,要實(shí)現(xiàn)自定義View,根據(jù)自定義View的種類不同,可能分別要自定義實(shí)現(xiàn)不同的方法。盡管源碼中調(diào)用的方法很多,但是這些方法其實(shí)不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

onMeasure()方法:?jiǎn)我籚iew,一般重寫此方法,針對(duì)wrap_content情況,規(guī)定View默認(rèn)的大小值,避免于match_parent情況一致。ViewGroup,若不重寫,就會(huì)執(zhí)行和單子View中相同邏輯,不會(huì)測(cè)量子View。一般會(huì)重寫onMeasure()方法,循環(huán)測(cè)量子View。
onLayout()方法:單一View,不需要實(shí)現(xiàn)該方法。ViewGroup必須實(shí)現(xiàn),該方法是個(gè)抽象方法,實(shí)現(xiàn)該方法,來對(duì)子View進(jìn)行布局。
onDraw()方法:無論單一View,或者ViewGroup都需要實(shí)現(xiàn)該方法。

圖片來源:Carson_Ho的自定義View
由于本人水平有限,若是文中有敘述不清晰或不準(zhǔn)確的地方,希望大家能夠指出,謝謝大家!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容