Android基礎之View的繪制

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

  • View簡介
  • MeasureSpecs類
  • onMeasure
    • 單一View的測量
    • ViewGroup的測量
  • onLayout
    • 單一View的layout過程
    • ViewGroup的layout過程
  • onDraw
  • 總結

View簡介

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



繪制出整個界面肯定是要遍歷整個View樹,對這棵樹的所有節點分別進行測量,布局和繪制。萬事皆有源頭,繪制得從根節點頂級View開始畫起,即DecorView。
系統內部會依次調用DecorView的measure,layout和draw三大流程方法。measure方法又會調用onMeasure方法對它所有的子元素進行測量,如此反復調用下去就能完成整個View樹的遍歷測量。同樣的,layout和draw兩個方法里也會調用相似的方法去對整個View樹進行遍歷布局和繪制。

View的構造函數:共有4個,具體如下:

    // 如果View是在Java代碼里面new的,則調用第一個構造函數
 public CarsonView(Context context) {
        super(context);
    }

    // 如果View是在.xml里聲明的,則調用第二個構造函數
    // 自定義屬性是從AttributeSet參數傳進來的
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 不會自動調用
    // 一般是在第二個構造函數里主動調用
    // 如View有style屬性時
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用
    // 不會自動調用
    // 一般是在第二個構造函數里主動調用
    // 如View有style屬性時
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

View的位置參數
View的位置由4個頂點決定(View的位置是相對于父控件而言的

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

MeasureSpecs類

MeasureSpecs類是View的內部類,用一個變量封裝了兩個數據(size、mode),其目的是減少對象的內存分配。

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

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

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

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

onMeasure

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

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

單一View的measure過程

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



那么我們來看看整個流程的源碼:

//measure是測量流程的開始,由于是final類型,因此不能被重寫,
//主要是用來進行基本測量邏輯的判斷。
//里面調用onMeasure進行測量邏輯。
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);//具體的測量邏輯
        } else {
            ...    
    }

measure方法是測量過程中最先調用的方法,View的這個方法是被它的父控件調用的。由于measure是final類型,不能被子類重寫,那么就只能重寫onMeasure方法來實現測量邏輯了。

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

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

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

//存儲測量后的View寬 / 高,該方法即為我們重寫onMeasure()所要實現的最終目的。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        //判斷該view布局模式是否有一些特殊的邊界
        boolean optical = isLayoutModeOptical(this);
        ////判斷view和該view的父view的布局模式情況,如果兩者不同步,則進行子view的size大小的修改
        if (optical != isLayoutModeOptical(mParent)) {
//有兩種情況會進入到該if條件,
//一是子view有特殊的光學邊界,而父view沒有,此時optical為true,
//一種是父view有一個特殊的光學邊界,而子view沒有,此時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;
        }
        //存儲測量后的View寬 / 高的實際邏輯
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
//根據View寬/高的測量規格計算View的寬/高值
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:
            // 模式為UNSPECIFIED時,使用提供的默認大小 = 參數Size
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            //模式為AT_MOST,EXACTLY時,
            //使用View測量后的寬/高值 = measureSpec中的Size
            result = specSize;
            break;
        }
        return result;
    }

可以看出,View在當測量模式為UNSPECIFIED時,返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小。其實這對我們自定義控件并沒有什么影響,因為UNSPECIFIED模式是給系統內部用的。我們的重點還是應該放在AT_MOST和EXACTLY兩種情況下。對于這兩種情況,getDefaultSize十分簡單粗暴,直接返回了specSize,也就是View的測量規格里的測量尺寸。

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

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

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);
        //根據View的邏輯得到,比如TextView根據設置的文字計算wrap_content時的大小。
        //這兩個數據變量要根據實現需求計算。
        int wrapWidth,wrapHeight;
        
        // 如果有測量模式是AT_MOST則需要進行特殊處理
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            //長寬都是AT_MOST,則都需要計算
            setMeasuredDimension(wrapWidth, wrapHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            //只有寬是AT_MOST,則長用測量尺寸
            setMeasuredDimension(wrapWidth, heightSpecSize);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            //只有長是AT_MOST,則寬用測量尺寸
            setMeasuredDimension(widthSpecSize, wrapHeight);
        }
}

ViewGroup的measure過程

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

看過源碼就知道ViewGroup并沒有重寫View的onMeasure方法,為什么呢?顯然這需要它的子類去根據相應的邏輯去實現,比如LinearLayout與RelativeLayout對child View的測量邏輯顯然是不同的。這個也是單一View的measure過程與ViewGroup過程最大的不同

不過,ViewGroup倒是提供了一個measureChildren的方法,貌似可以用來測量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);
            }
        }
    }

這個方法的邏輯就很清晰嘛,就是遍歷子View,調用measureChild方法對其進行測量,那么我們接著來看看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方法里,會取出child的LayoutParams,再結合父控件的測量規格已被占用的空間Padding,作為參數傳遞給getChildMeasureSpec方法,在getChildMeasureSpec里會組合生成child控件的測量規格。

getChildMeasureSpec是ViewGroup里提供的一個靜態方法,用來用來獲取子控件的測量規格。

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

        // 當父控件的測量模式是AT_MOST時,父view強加給子view一個最大的值
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也會優先滿足孩子的需求
                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想要根據自己邏輯決定大小,那就自己決定唄
                //同樣的,child的尺寸上限也是父控件的尺寸上限size
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

         // 當父view的模式為UNSPECIFIED時,父容器不對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);
    }

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

至此,ViewGroup的測量流程也基本結束了,整體的流程如下圖:


不過還有一個問題不知道大家注意到沒有,measureChild方法只考慮了父View的padding,但是沒考慮到子View的margin。這就會導致子view在使用match_parent屬性的時候,margin屬性會有問題。
當然,ViewGroup也考慮到了這個問題,為此也提供了另一個測量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方法,根據名字也能看出來,比measureChild方法多考慮了個margin,源碼也跟前面的差不多,只是將margin考慮了進去,所以一般情況下,這個方法使用的更多一些。

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

onLayout

類似measure過程,layout過程根據View的類型分為2種情況,單一View和ViewGroup。對于單身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;

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

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //調用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;
            //監聽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);
        }
    }

盡管代碼有點長,不過不難看出layout方法首先會調用isLayoutModeOptical這個方法,判斷是否有光學邊界的(光學邊界這里暫時用不到,其實關鍵是我也不會,想深入了解的請自行谷歌),之后調用setFrame或者setOpticalFrame方法來給View的四個頂點屬性賦值,即mLeft,mRight,mTop,mBottom四個值。那么我們再來看看setOpticalFrame方法:

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

可以看到,這個setOpticalFrame方法,最終也是調用了setFrame,那好我們可以直接繼續看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中先比較了新位置和老位置是否有差異,如果有差異則會調用sizechanged來更新View的位置。

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

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

ViewGroup的layout過程

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

其實,ViewGroup的Layout的關鍵就是實現onLayout方法,在ViewGroup中onLayout方法被聲明成了抽象方法,這就強制繼承ViewGroup的類都得自己去實現自己定位子元素的邏輯。

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

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

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

              // 計算當前子View的四個位置值
                //位置的計算邏輯
                //TODO
                // 需自己實現,也是自定義View的關鍵

                // 對計算后的位置值進行賦值
                int mLeft  = Left
                int mTop  = Top
                int mRight = Right
                int mBottom = Bottom

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

onDraw

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

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

        // 步驟2:繪制本身View內容
        if (!dirtyOpaque) 
            onDraw(canvas);
        // View 中:默認為空實現,需復寫
        // ViewGroup中:需復寫

        // 步驟3:繪制子View
        // 由于單一View無子View,故View 中:默認為空實現
        // ViewGroup中:系統已經復寫好對其子視圖進行繪制我們不需要復寫
        dispatchDraw(canvas);

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

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

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

那么我們再來一步一步看看源碼中都做了什么:
1. 繪制背景 -- drawBackground()

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

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

        //省略部分代碼

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

2.繪制自己 -- onDraw()
由于不同的控件都有自己不同的繪制實現,所以View的onDraw方法肯定是空方法。在自定義繪制過程中,需由子類去實現復寫該方法,從而繪制自身的內容。也就是說我們在自定義View的時候要根據實際需求對onDraw方法進行實現。

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,即遍歷調用drawChild方法,drawChild方法又調用了child的draw(canvas, this, drawingTime)方法,最后還是調用到了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);
        }
    }

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

總結

View的繪制流程可以總結為下圖:


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

onMeasure()方法:單一View,一般重寫此方法,針對wrap_content情況,規定View默認的大小值,避免于match_parent情況一致。ViewGroup,若不重寫,就會執行和單子View中相同邏輯,不會測量子View。一般會重寫onMeasure()方法,循環測量子View。
onLayout()方法:單一View,不需要實現該方法。ViewGroup必須實現,該方法是個抽象方法,實現該方法,來對子View進行布局。
onDraw()方法:無論單一View,或者ViewGroup都需要實現該方法。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370