View的工作流程

Android中View的層級結(jié)構(gòu)及繪制步驟

之前講View的事件分發(fā)機制時,講到了DecorView,其實我們要查看一個一個頁面的DecorView可以通過Android Studio自帶的工具Android Device Monitor,Tools->Android->Android Device Monitor打開,連接手機或者模擬器,打開一個app界面,這時在Android Device Monitor的Device欄我們就可以看到當前連接的設(shè)備(真機或模擬器)中安裝的app的包名了,選中我們剛才打開的那個app的包名,再點上面的按鈕“Dump View Hierarchy for UI Automator”,然后會在右邊出現(xiàn)設(shè)備上當前界面,再右邊就是這個界面對應的布局了,我們看到所有的View都是層級顯示,可以很快搞清楚這個界面的結(jié)構(gòu),而且選中每個節(jié)點,可以很清楚的看到這個節(jié)點View的信息,所以知道DecorView和android.R.id.content所對應的View就很方便了。

DecorView層級結(jié)構(gòu)

如圖所示,DecorView就是最頂層的那個FrameLayout,它里面包括一個LinearLayout和一個id為statusBarBackground的View,LinearLayout中又包括了一個FrameLayout,注意這里的FrameLayout的y值66,也就是從statusBarBackground的下方開始的,而不是從(0,0)的位置。最后要注意的就是這個content了,當我們在一個Activity中調(diào)用setContentView方法時就是將我們的內(nèi)容View添加到這個id為android.R.id.content的FrameLayout中。

View的繪制是從ViewRootImpl的performTraversals方法開始,方法內(nèi)部經(jīng)過performMeasure、performLayout、performDraw三個步驟才能最終將一個View繪制出來,而在這三個方法中又會分別調(diào)用頂層View的measure、layout和draw方法,measure負責測量View的寬高,layout負責確定View在父容器中的位置,draw負責繪制內(nèi)容。

當完成Measure過程后,可以通過getMeasuredWidth和getMeasuredHeight方法來獲取到View測量后的寬高。Layout過程完成后,可以得到View的四個頂點位置,通過getLeft、getTop、getRight、getBottom這四個方法。只有當View的draw完成后Veiw的內(nèi)容才能顯示到屏幕上。

關(guān)于MeasureSpec

MeasureSpec作用與View的測量過程,它可以決定View的測量尺寸,但這并是絕對的,因為可能收到父容器的印象,在測量過程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應的MeasureSpec,然后再根據(jù)這個measureSpec測量出View的寬高。

MeasureSpec實質(zhì)上是一個32位的int值,高2位表示SpecMode,也就是測量模式,低30位表示SpecSize,SpecSize是在某種測量模式下的規(guī)格大小。SpecMode有三種:UNSPECIFIEDEXACTLYAT_MOST,它們分別表示的意思是:

  • UNSPECIFIED:父容器不對View有任何限制。
  • EXACTLY:父容器已經(jīng)測出View所需要的精確大小,這時View的最終大小就是SpecSize所指定的值,當xml中的layout_width和layout_height是match_parent和具體的數(shù)值時,測量該View時會使用這中模式。
  • AT_MOST:父容器指定了一個可用大小即SpecSize,View的大小不能大于這個值,當xml中的layout_width和layout_height是wrap_content時,測量該View就使用這種模式。

說到這三種模式時,我們不得不說下,當我們要可滑動控件中的內(nèi)容全部顯示時,網(wǎng)上一搜,解決辦法基本就是先自定義一個滑動控件,然后重寫其onMeasure方法,代碼如下:

public class ApplyListView extends ListView {

    public ApplyListView(Context context) {
        super(context);
    }


    public ApplyListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }


    public ApplyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

}

估計很多人對onMeasure方法中的代碼的意思不理解,下面我來說下,首先看MeasureSpec中的三個方法:

// 這個方法的作用是根據(jù)大小和模式來生成一個int值,這個int值封裝了模式和大小信息
public static int makeMeasureSpec(int size, int mode) 

// 這個方法的作用是通過一個int值來獲取里面的模式信息
public static int getMode(int measureSpec)

// 這個方法的作用是通過一個int值來獲取里面的大小信息
public static int getSize(int measureSpec)

上面我們說過,MeasureSpec是一個32位的int值,高2位表示mode,低30位表示size,而MeasureSpec類中也定義了如下常量:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
// 0左移30位,也就是int類型的最高兩位是00
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 1左移30位,也就是int類型的最高兩位是01
public static final int EXACTLY     = 1 << MODE_SHIFT;
// 2左移30位,也就是int類型的最高兩位是10
public static final int AT_MOST     = 2 << MODE_SHIFT;

我們再看看自定義控件中的onMeasure方法,第一句是調(diào)用MeasureSpec的makeMeasureSpec方法,這個方法是用來生成一個帶有模式和大小信息的int值的,它的兩個參數(shù)一個是size,一個是mode,size我們傳的是Integer.MAX_VALUE >> 2,因為32位int型最大值就是Integer.MAX_VALUE,右移兩位,表示后面30位的最大值,而我們在手機上的控件不可能那么大,所以這個模式就得選擇MeasureSpec.AT_MOST了。

上面也提到了在測量過程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應的MeasureSpec,然后再根據(jù)這個measureSpec測量出View的寬高,普通View的MeasureSpec是由LayoutParams和父容器共同決定的,對于頂層View也就是DecorView來說,它的MeasureSpec是有窗口的尺寸和它自身的LayoutParams共同決定的。當MeasureSpec確定后就可以在onMeasure中確定View的測量寬高了。

在ViewRootImpl中的measureHierarchy方法中有一段代碼:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

其作用就是確定DecorView的MeasureSpec的創(chuàng)建過程,其中desiredWindowHeight就是屏幕的高,接著看getRootMeasureSpec方法的實現(xiàn):

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

這段代碼基本就明確了DecorView的MeasureSpec的創(chuàng)建過程了。當其LayoutParams為MATCH_PARENT時,為精確模式,大小就是屏幕大小;當其LayoutParams為WRAP_CONTENT時,為最大模式,大小不定,但最大不能超過屏幕大小;當其LayoutParams為固定尺寸時,為精確模式,大小就是其指定的大小。

普通View的measure過程是由ViewGroup傳遞過來的,我們先看看ViewGroup的measureChildWithMargins方法:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

這個方法中我們可以看到對子View進行measure之前,會通過getChildMeasureSpec方法來計算出子View的MeasureSpec,從幾個參數(shù)可以看出,子View的MeasureSpec創(chuàng)建與父容器的MeasureSpec和子View本身的LayoutParams有關(guān),還和View的margin、padding有關(guān)。

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) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = 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);
}

我們看到,確定子View的MeasureSpec,首先是算出父容器除去padding后還剩下的尺寸,然后根據(jù)父容器的MeasureSpec和子View的LayoutParams來確定。

  • 當父容器的MeasureSpec為EXACTLY時:

    • 如果子View的LayoutParams指定了具體值,那么子View的MeasureSpec為EXACTLY;
    • 如果子View的LayoutParams為MATCH_PARENT,那么子View的MeasureSpec為EXACTLY;
    • 如果子View的LayoutParams為WRAP_CONTENT,那么子View的MeasureSpec為AT_MOST;
  • 當父容器的MeasureSpec為AT_MOST時:

    • 如果子View的LayoutParams指定了具體值,那么子View的MeasureSpec為EXACTLY;
    • 如果子View的LayoutParams為MATCH_PARENT,那么子View的MeasureSpec為AT_MOST;
    • 如果子View的LayoutParams為WRAP_CONTENT,那么子View的MeasureSpec為AT_MOST;
  • 當父容器的MeasureSpec為UNSPECIFIED時:

    • 如果子View的LayoutParams指定了具體值,那么子View的MeasureSpec為EXACTLY;
    • 如果子View的LayoutParams為MATCH_PARENT,那么子View的MeasureSpec為UNSPECIFIED;
    • 如果子View的LayoutParams為WRAP_CONTENT,那么子View的MeasureSpec為UNSPECIFIED;

一般我們開發(fā)過程中不會使用到UNSPECIFIED這種模式,它主要用于系統(tǒng)內(nèi)部多次measure的情形。

View和ViewGroup的measure過程

如果是原始的View,那么通過measure方法就完成了測量過程,而如果是ViewGroup,除了完成自己的測量外,還需要遍歷子元素調(diào)用其measure方法,各個子元素再去遞歸執(zhí)行這個過程。

先看View的measure方法,這個方法是final類型的,也就是說子類不能重寫該方法,而在View的measure方法中會調(diào)用onMeasure方法,所以我們只需要看onMeasure的實現(xiàn)即可,在自定義View時,也是重寫onMeasure方法。

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

setMeasuredDimension方法的作用就是設(shè)置View的寬高測量值,而getDefaultSize方法就是根據(jù)MeasureSpec獲取到測量值:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

我們主要看最大模式和精確模式這兩種情況,最后返回的結(jié)果就是specSize這個View的測量值,而View的大小是在layout階段才最終確定的。無限制模式一般用于系統(tǒng)內(nèi)容測量,這種情況下,View的大小為getDefaultSize方法的第一個參數(shù)size,即寬高分別為getSuggestedMinimumWidth()和getSuggestedMinimumHeight()這兩個方法的返回值。這兩個方法如下:

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

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

這段代碼看出,這種模式下View是根據(jù)background和minWidth/minHeight這兩個屬性來決定的,如果沒有設(shè)置背景,那么返回值就是最小值屬性指定的數(shù)值,這個值默認為0,也就是當不指定這個屬性時,最小值就為0了,如果有背景,返回的是mBackground.getMinimumHeight(),這個方法在Drawable類中,源碼如下:

public int getMinimumHeight() {
    final int intrinsicHeight = getIntrinsicHeight();
    return intrinsicHeight > 0 ? intrinsicHeight : 0;
}

所以這個方法返回的就是背景Drawable的原始寬/高,前提是這個Drawbale有原始寬/高,否則就返回0,比如當背景設(shè)置成ShapeDrawable時就沒有原始寬高,而BitmapDrawable就有。

從getDefaultSize方法的實現(xiàn)來看,View的寬高由specSize決定,所以,我們自定義View時,需要重寫onMeasure方法并設(shè)置wrap_content時的自身大小,否則在布局中wrap_content就相當于match_parent,因為當View的layout_width/height為wrap_content時,它的specMode是AT_MOST模式,這種情況下,它的寬高都等于specSzie-padding,也就是父容器當前剩余的空間大小,效果等同于match_parent。那么如何重寫onMeasure方法呢?

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightMeasureSpec);
    } else {
        setMeasuredDimension(widthMeasureSpec, mHeight);
    }

}

我們只需要給View指定一個默認的寬高mWidth和mHeight設(shè)置即可。

ViewGroup的measure過程,除了測量自身的大小,還要遍歷測量所有子元素的大小,各個子元素也要遞歸操作這個過程,與View不同的是,ViewGroup是一個抽象類,因此它沒有重寫View的onMeasure方法,但它提供了一個叫measureChildren的方法:

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

這個方法就是循環(huá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就是根據(jù)子元素的MeasureSpec進行測量,子元素的MeasureSpec通過getChildMeasureSpec方法獲得。

之所以ViewGroup沒有像View一樣統(tǒng)一使用onMeasure方法去測量大小,是因為ViewGroup的子類有不同的布局特性,測量細節(jié)也各不相同,ViewGroup也無法做統(tǒng)一實現(xiàn)。

View的measure過程是三大過程中最復雜的一個,measure完成以后,通過getMeasuredWidth/Height方法就可以正確的獲取到View測量后的寬高。在某些極端情況下,onMeasure方法中拿到的測量值并不準確,所以最好是在onLayout方法中View的測量寬高的最終值。

現(xiàn)在我們要考慮一個我們開發(fā)過程中經(jīng)常遇到的問題:當Activity啟動時,我們想獲取某個View的尺寸,以便在達到某個條件時做一些事情,但是打印Log顯示這個View的尺寸是0。這是因為View的measure過程和Activity的生命周期應不是同步執(zhí)行的,要解決這種情況也有好幾種辦法,下面詳細來說一說。

  • onWindowFoucsChanged,這個方法的含義是View已經(jīng)初始化完畢了,寬高已經(jīng)準備好了。不過這個方法在Activity獲得焦點和失去焦點時都會被調(diào)用,也就是說如果頻繁調(diào)用onResume方法和onPause方法,那onWindowFoucsChanged方法也會被頻繁調(diào)用。

  • view.post(runnable),該方法通過post一個runnable到消息隊列尾部,然后等Looper調(diào)用此runnable,runnable中即是獲取View尺寸的操作。

  • ViewTreeObserver,這個類其實是一個對View監(jiān)聽的類,它有很多回調(diào)方法,我們使用onGlobalLayoutListener這個接口就可以完成這個功能,當View樹的狀態(tài)發(fā)生改變或者View樹下面的子View發(fā)生變化時,onGlobalLayut方法將會被回調(diào)。所以這是一個獲取View寬高的時機。

View的layout過程

layout過程主要是確定自身的位置和確定子元素的位置,當ViewGroup的位置被確定后,它會在onLayout方法中遍歷所有子元素并調(diào)用其layout方法,在layout方法中onLayout方法又會被調(diào)用。

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

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

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

layout方法首先會通過setOpticalFrame方法或者setFrame方法來給View設(shè)定四個頂點的位置,也就是初始化mLeft、mRight、mTop、mBottom這幾個值,一旦四個頂點位置確定,那么View在父容器總的位置也就確定了,接著調(diào)用onLayout方法,這個方法的作用是父容器確定子元素的位置,但是View和ViewGroup都沒有具體實現(xiàn)這個方法,和onMeasure類似。

之前我們提到過一個問題,View的測量寬高和最終寬高是不是一樣的,這個問題其實就是比較View的getMeasuredWidth方法和getWidth方法的返回值是否一樣。其實在View的默認實現(xiàn)中,測量寬高和最終寬高是相等的,只不過測量寬高是在measure過程賦值的,而最終寬高則是在layout過程賦值,日常開發(fā)中我們可以認為他們相等,但某些特定的情況會導致不一樣,比如當我們重寫了View的layout方法,改變了其寬高,這時候就不相等了。

View的draw過程

draw過程作用就是講View繪制到屏幕上了,draw的步驟有以下幾步:

  • 繪制背景:background.draw(canvas)
  • 繪制自己:onDraw
  • 繪制子元素:dispatchDraw
  • 繪制裝飾:onDrawScrollBars

draw方法源碼如下:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

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

    /*
     * Here we do the full fledged routine...
     * (this is an uncommon case where speed matters less,
     * this is why we repeat some of the tests that have been
     * done above)
     */

    boolean drawTop = false;
    boolean drawBottom = false;
    boolean drawLeft = false;
    boolean drawRight = false;

    float topFadeStrength = 0.0f;
    float bottomFadeStrength = 0.0f;
    float leftFadeStrength = 0.0f;
    float rightFadeStrength = 0.0f;

    // Step 2, save the canvas' layers
    int paddingLeft = mPaddingLeft;

    final boolean offsetRequired = isPaddingOffsetRequired();
    if (offsetRequired) {
        paddingLeft += getLeftPaddingOffset();
    }

    int left = mScrollX + paddingLeft;
    int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
    int top = mScrollY + getFadeTop(offsetRequired);
    int bottom = top + getFadeHeight(offsetRequired);

    if (offsetRequired) {
        right += getRightPaddingOffset();
        bottom += getBottomPaddingOffset();
    }

    final ScrollabilityCache scrollabilityCache = mScrollCache;
    final float fadeHeight = scrollabilityCache.fadingEdgeLength;
    int length = (int) fadeHeight;

    // clip the fade length if top and bottom fades overlap
    // overlapping fades produce odd-looking artifacts
    if (verticalEdges && (top + length > bottom - length)) {
        length = (bottom - top) / 2;
    }

    // also clip horizontal fades if necessary
    if (horizontalEdges && (left + length > right - length)) {
        length = (right - left) / 2;
    }

    if (verticalEdges) {
        topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
        drawTop = topFadeStrength * fadeHeight > 1.0f;
        bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
        drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
    }

    if (horizontalEdges) {
        leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
        drawLeft = leftFadeStrength * fadeHeight > 1.0f;
        rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
        drawRight = rightFadeStrength * fadeHeight > 1.0f;
    }

    saveCount = canvas.getSaveCount();

    int solidColor = getSolidColor();
    if (solidColor == 0) {
        final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

        if (drawTop) {
            canvas.saveLayer(left, top, right, top + length, null, flags);
        }

        if (drawBottom) {
            canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
        }

        if (drawLeft) {
            canvas.saveLayer(left, top, left + length, bottom, null, flags);
        }

        if (drawRight) {
            canvas.saveLayer(right - length, top, right, bottom, null, flags);
        }
    } else {
        scrollabilityCache.setFadeColor(solidColor);
    }

    // Step 3, draw the content
    if (!dirtyOpaque) onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    // Step 5, draw the fade effect and restore layers
    final Paint p = scrollabilityCache.paint;
    final Matrix matrix = scrollabilityCache.matrix;
    final Shader fade = scrollabilityCache.shader;

    if (drawTop) {
        matrix.setScale(1, fadeHeight * topFadeStrength);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, right, top + length, p);
    }

    if (drawBottom) {
        matrix.setScale(1, fadeHeight * bottomFadeStrength);
        matrix.postRotate(180);
        matrix.postTranslate(left, bottom);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, bottom - length, right, bottom, p);
    }

    if (drawLeft) {
        matrix.setScale(1, fadeHeight * leftFadeStrength);
        matrix.postRotate(-90);
        matrix.postTranslate(left, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(left, top, left + length, bottom, p);
    }

    if (drawRight) {
        matrix.setScale(1, fadeHeight * rightFadeStrength);
        matrix.postRotate(90);
        matrix.postTranslate(right, top);
        fade.setLocalMatrix(matrix);
        p.setShader(fade);
        canvas.drawRect(right - length, top, right, bottom, p);
    }

    canvas.restoreToCount(saveCount);

    // Overlay is part of the content and draws beneath Foreground
    if (mOverlay != null && !mOverlay.isEmpty()) {
        mOverlay.getOverlayView().dispatchDraw(canvas);
    }

    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}   

源碼中注釋已經(jīng)很清楚了,通過onDraw方法來draw the content,也就是繪制自身的內(nèi)容,通過dispatchDraw方法來繪制子元素,這個方法會遍歷調(diào)用所有子元素的draw方法,draw事件一直傳遞下去,完成了整個ViewGroup的繪制。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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