天天看點

View的測量、布局和繪制過程寫在前面的話1. measure的過程2. layout過程3. draw寫在後面的話

寫在前面的話

        按照之前寫的節奏來的話,這篇改對View的整個測量、布局和繪制過程進行分析了。在之前的

Activity顯示到Window的過程

中了解到

performTraversals()

這個方法會執行

performMeasure()

去測量View的大小,

performLayout()

去将子View放到合适的位置上,

performDraw()

将View真正繪制出來。

1. measure的過程

1.1 在測量前,先看下MeasureSpec

MeasureSpec了解為測量規格,從源碼中可以知道測量規格包括了測量模式(SpecMode)和大小(SpecSize),這個規格通過一個int型來表示。其中int的高2位代表了測量模式,低30位代表了大小。我們都知道兩位可以有四種組合情況,而Android中View有三種測量模式,分别是:

  • UNSPECIFIED(0 << 30):子View可以想要任意大小
  • EXACTLY(1 << 30):父容器已經檢測出子View所需要的精确大小,View的大小即為SpecSize的大小,他對應于布局參數中的MATCH_PARENT,或者精确值
  • AT_MOST(2 << 30):父容器指定了一個大小,即SpecSize,子View的大小不能超過這個SpecSize的大小

    通過測量規格擷取測量模式和大小:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

// 獲得SpecMode
@MeasureSpecMode
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
// 獲得SpecSize
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}
           

從上面可以看到,獲得SpecMode時,需要和MODE_MASK(0x30000000)進行與運算,因為低30位全為0,高2位都為1,是以最終的結果就是高2位<<30的值,也就是我們三個測量模式中的一個。

同理,在獲得SpecSize時,我們需要将SpecMode去除,獲得低30位的值,是以這裡進行的是與上非MODE_MASK運算,即擷取低30位的值(SpecSize)。

1.2 getRootMeasureSpec方法

在執行

performMeasure()

方法前,會執行ViewRootImpl中的

getRootMeasureSpec

方法,通過這個方法來獲得跟布局的測量規格。

// mWidth和mHeight的值是通過
//if (mWidth != frame.width() || mHeight != frame.height()) {
//mWidth = frame.width();
//mHeight = frame.height();
//}
//指派,這裡等于Window視窗的寬高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    //rootDimension是decorView的params的參數,這裡為MATCH_PARENT,是以測量模式是EXACTLY
    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;
}
           

1.3 View的measure方法

在獲得了寬高的測量規格後,将會執行

performMeasure()

方法,

performMeasure()

方法會調用DecorView的

measure()

方法,DecorView和其父類并沒有重寫這個measure方法,最終會調用View的measure方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //判斷目前View的layoutMode是不是LAYOUT_MODE_OPTICAL_BOUNDS,這種情況很少
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }
    
    // 作為緩存的key
    // Suppress sign extension for the low bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    // Optimize layout by avoiding an extra EXACTLY pass when the view is
    // already measured as the correct size. In API 23 and below, this
    // extra pass is required to make LinearLayout re-distribute weight.
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    
    // 需要布局
    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();
        // 如果是強制布局的話,則需要重新去調用onMeasure方法,否則去緩存中擷取
        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("View with id " + getId() + ": "
                    + getClass().getName() + "#onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
    // 放到緩存中
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

// onMeasure中需要去設定測量的結果,View的預設實作是設定預設的大小,這個大小根據測量模式來确定
// 如果是UNSPECIFIED:未指定的話則大小為建議的最小值
// 如果是AT_MOST||EXACTLY,那麼傳回值為SpecSize
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

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;
    }
    // 真正為mMeasuredWidth和mMeasuredHeight指派
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
// 為mMeasuredWidth和mMeasuredHeight指派
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
           

從代碼中可以看到,如果我們需要進行布局的話,首先判斷是否為強制布局,如果不是的話獲得mMeasureCache中目前測量規格的位置。如果沒有這個緩存,則說明需要去進行onMeasure方法去測量真正的寬高,最後将目前寬高的測量規格儲存到緩存中。

在調用View的onMeasure方法時,我們需要調用

setMeasuredDimension

方法來設定具體的寬高,當調用了這個方法後,會通過

setMeasuredDimensionRaw

方法來給mMeasuredWidth和mMeasuredHeight指派,這樣我們通過getMeasuredWidthAndState()擷取mMeasuredWidth值或者通過getMeasuredWidth()來擷取

mMeasuredWidth & MEASURED_SIZE_MASK(0x00ffffff)

值。

上面寫到的都是View裡面關于測量的方法,從這裡我們就看出來了,View的測量确定了View的四個點的位置以及測量的寬高。ViewGroup作為View的子類,其并沒有重寫onMeasure方法,作為ViewGroup的子類基本上都會重寫onMeasure方法,通過onMeasure方法來測量子View的大小,通過子View的大小最終來确定自己的大小。

下面是FrameLayout的測量過程:

FrameLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // 如果目前的測量模式不是EXACTLY,則需要統計擁有MATCH_PARENT屬性的子View
    // 在設定完成目前layout的寬高後,需要重新測量擁有MATCH_PARENT屬性的子View
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 如果子View不是隐藏狀态,則需要測量
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 測量子View
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            // 設定最大寬度,每個子View和目前的最大寬度進行比較
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
    // 為目前layout設定寬高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // 如果有MATCH_PARENT屬性的子View大于1的話,則需要重新去測量這些子View
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }

            final int childHeightMeasureSpec;
            if (lp.height == LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}
ViewGroup.java
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去測量自身大小
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup.java
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) {
    // EXACTLY如果是精确大小的話,則根據child的大小來計算具體大小
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            // 如果設定有具體值,則結果設定具體值,模式為EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            // MATCH_PARENT則設定父View的大小,模式為EXACTLY
            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.
            // 結果設定成父View大小,并且測量模式設定成AT_MOST
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    
    // AT_MOST模式
    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            // 設定具體大小,并且測量模式是EXACTLY
            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.
            // 設定成父View大小,并且模式是AT_MOST
            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.
            // 設定成父View大小,并且模式是AT_MOST
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // UNSPECIFIED模式
    // 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
            // 設定具體大小,并且測量模式是EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            // sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M
            // 通過targetSdkVersion來判斷size設定為0還是父View的size,并且測量模式設定UNSPECIFIED
            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
            // 通過targetSdkVersion來判斷size設定為0還是父View的size,并且測量模式設定UNSPECIFIED
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
View.java
// 這個我的了解是解析size并使這個size擁有一個狀态
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        // 如果是AT_MOST模式,則需要判斷需要的size和測量規格的SpecSize的大小
        // 如果需要的size>SpecSize,那麼使用SpecSize,并且設定标志位為MEASURED_STATE_TOO_SMALL = 0x01000000
        // 這個标志為代表了目前測量規格的大小小于所需大小
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        // 如果測量模式是EXACTLY,那麼這個結果就是specSize
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        // 其他情況都是所需的size
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    //MEASURED_STATE_MASK = 0xff000000,這裡的結果都帶有一個狀态,這個狀态的用處不詳。。。
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
           

從上面FrameLayout的測量過程我們可以看到,整個流程如下:

View的測量、布局和繪制過程寫在前面的話1. measure的過程2. layout過程3. draw寫在後面的話

流程圖

測量過程:

  1. FrameLayout調用onMeasure方法,開始測量
  2. FrameLayout的onMeasure方法中調用了其父類的measureChildWithMargins方法去測量子View的大小
  3. measureChildWithMargins通過getChildMeasureSpec方法獲得子View的測量規格後調用子View的measure方法
  4. 子View通過調用onMeasure方法最終通過setMeasuredDimension設定具體的測量後的寬高
  5. FrameLayout在獲得所有子View的結果後,擷取其中的最大值,并且如果有背景圖的話,擷取子View和背景圖的最大值,同樣通過setMeasuredDimension設定FrameLayout的大小

2. layout過程

        measure之後就會進行layout過程。layout其實就是對View的left、top、right、bottom這四個點位置的确定的過程。從源碼可以看到,View中實作了layout方法,ViewGroup對其進行了Override,但是ViewGroup會調用super.layout(l, t, r, b),是以最終還是進入View的layout方法。

        在ViewGroup中onLayout是一個抽象方法,這就意味着所有的子類需要實作這個抽象方法。一般來說,每個不同的layout都有不同的實作,這樣就構成了我們Android各種布局。當然了,自定義控件中關于onLayout的實作也是很重要的。下面還是關于FrameLayout的layout的實作:

View.java
public void layout(int l, int t, int r, int b) {
    // mPrivateFlags3的指派是在measure方法中,多次測量是在不是強制layout并且有緩存的情況下進行指派的
    // 這種情況需要重新調用onMeasure方法,對View重新設定大小
    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;
    // 是否使用視覺邊界布局效果,這個并不影響這個流程,因為setOpticalFrame也會調用setFrame方法
    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;
        // 調用OnLayoutChangeListener的onLayoutChange方法
        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;
}
View.java
// 傳回值是boolean,代表是否位置改變了,如果和以前的位置不同,則說明改變了,需要重新布局
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;

        // 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);
        // 使我們舊的位置無效
        // Invalidate our old position
        invalidate(sizeChanged);

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

        mPrivateFlags |= PFLAG_HAS_BOUNDS;

        // 如果尺寸改變了,調用sizeChange方法,這裡面會調用onSizeChanged方法
        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        
        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            // If we are visible, force the DRAWN bit to on so that
            // this invalidate will go through (at least to our parent).
            // This is because someone may have invalidated this view
            // before this call to setFrame came in, thereby clearing
            // the DRAWN bit.
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            // parent display list may need to be recreated based on a change in the bounds
            // of any child
            invalidateParentCaches();
        }

        // Reset drawn bit to original value (invalidate turns it off)
        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

FrameLayout.java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
FrameLayout.java
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    // 獲得目前View的四個可布局的點
    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }
            // 獲得布局的方向,如果沒有設定flag PFLAG2_LAYOUT_DIRECTION_RESOLVED_RTL,則為從左到右布局
            // 右到左布局在有些國家會出現這種情況
            final int layoutDirection = getLayoutDirection();
            // 獲得目前布局的絕對顯示位置,這裡會根據布局方向來設定具體是從左邊開始還是右邊開始
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            // 垂直方法的顯示位置
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
            // 水準方向顯示位置的設定
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                // 水準居中
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                // 從右邊開始
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                // 左邊和預設的情況都是左邊開始布局
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            // 垂直方向的布局
            switch (verticalGravity) {
                // 頂部開始
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                // 垂直居中
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                // 底部開始
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                // 預設頂部開始
                default:
                    childTop = parentTop + lp.topMargin;
            }
            // 子View布局
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}
           
View的測量、布局和繪制過程寫在前面的話1. measure的過程2. layout過程3. draw寫在後面的話

layout流程圖

layout

的過程就是确定目前View的left、top、right、bottom這四個點位置,通過這四個點可以确定這個View的位置,進而在繪制的時候正确繪制。layout的過程和measure的過程不同,layout的過程是先确定自己的位置在确定其子View的位置。

3. draw

draw

過程在之前的

中有寫到過,在整個過程中,會調用View的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);
    }
    // 通常來算是跳過2和5部分。這裡隻看下跳過2和5部分時,整個流程
    // 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
        // 調用onDraw方法,去繪制内容
        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;
    }

    ......
}
           

從代碼中我們可以了解到,整個繪制過程一共有6步,但是通常來說第2步和第5步不會調用:

  1. 繪制背景 drawBackground
  2. 儲存畫布圖層
  3. 繪制内容 onDraw
  4. 分發繪制事件(繪制子View) dispatchDraw
  5. 繪制并恢複圖層
  6. 繪制裝飾 onDrawForeground

3.1 drawBackground

private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    // 背景為空,直接傳回
    if (background == null) {
        return;
    }
    // 設定背景的邊界值
    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null
            && mAttachInfo.mHardwareRenderer != null) {
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }
    
    // 滾動的x和y值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    // 沒有滾動,直接繪制
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        // 将canvas移動後再繪制
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

void setBackgroundBounds() {
    // 如果背景尺寸改變并且背景不為空,這設定其邊界為0,0,width,height
    if (mBackgroundSizeChanged && mBackground != null) {
        mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
}
           

        drawBackground方法比較簡單,總體來說就是繪制View的背景,當然根據背景是否存在,是否頁面滾動了來繪制背景。

3.2 onDraw

         onDraw是沒有具體實作的内容,一般來說在自定義View的時候,很多時候會重寫onDraw方法來繪制真正要實作的内容。

3.3 dispatchDraw

        從注釋來看,dispatchDraw作為繪制子View的開始,其在View中是空實作。在ViewGroup中有dispatchDraw方法的具體實作:

ViewGroup.java
// 我們隻關注如何繪制childView,内容省略了一大部分
// 這裡面可以看到會調用drawChild去繪制其子View
@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    // 我們隻關注如何繪制childView
    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
        ......

        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            // 繪制其子View
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
}
ViewGroup.java
// 子View會調用其傳回值為boolean的draw方法去繪制
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    // 關于硬體加速模式
    ......
    // 動畫相關
    ......
    
    // 硬體加速相關,通過updateDisplayListIfDirty擷取顯示清單的renderNode最後下面繪制
    if (drawingWithRenderNode) {
        // Delay getting the display list until animation-driven alpha values are
        // set up and possibly passed on to the view
        renderNode = updateDisplayListIfDirty();
        if (!renderNode.isValid()) {
            // Uncommon, but possible. If a view is removed from the hierarchy during the call
            // to getDisplayList(), the display list will be marked invalid and we should not
            // try to use it again.
            renderNode = null;
            drawingWithRenderNode = false;
        }
    }
    ......
    // 如果使用緩存去繪制,則通過cache繪制,否則還是會調用View的draw(canvas)方法繪制
    if (!drawingWithDrawingCache) {
        // 硬體加速的話通過drawRenderNode去繪制,在之前講過了最後會調用View的draw(canvas)方法繪制
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            // 無背景的快速繪制
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                // 
                dispatchDraw(canvas);
            } else {
                // 調用View的draw(canvas)方法繪制
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }

    ......
    return more;
}
           

        繪制子View的過程要相對繁瑣一些,通過View的另一個draw方法來繪制子View,并且這個方法包括了動畫相關,硬體加速相關。上面的代碼有一部分省略了,其中比較重要的是兩個地方:

  1. 硬體加速相關過程:在之前的 中有寫到過,主要是通過

    updateDisplayListIfDirty

    方法擷取顯示清單的renderNode最後通過硬體加速繪制。
  2. 軟體繪制過程:需要判斷是否使用緩存,如果使用緩存的話,直接繪制緩存,否則的話還需要按照上面的繪制流程一步步進行。

3.4 onDrawForeground

        看這個名稱可以認為是繪制前景,其中包括了滾動條、滾動訓示器等。當然了,可以通過重寫這個方法去繪制任何想要的前景。

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

3.5 繪制順序

這裡還是使用

扔物線

大神的一張圖來表示繪制順序吧。

View的測量、布局和繪制過程寫在前面的話1. measure的過程2. layout過程3. draw寫在後面的話

繪制順序.jpg

3.6 draw的總結

        整個繪制過程是一個自上向下的過程,在這個過程中先繪制自身的背景(drawBackground)、内容(onDraw),接着繪制子View(dispatchDraw)。子View的繪制過程又和上面的過程一樣,當所有的子View繪制完成後,會執行裝飾的繪制(onDrawForeground)

寫在後面的話

        還是按照計劃來的,整個流程已經寫到了測量、布局和繪制的過程。總體來說感覺網上有些資料還是不夠靠譜,如果自己不去看一遍的話,可能會有許多坑等着你來填。

View的測量、布局和繪制過程寫在前面的話1. measure的過程2. layout過程3. draw寫在後面的話

Go

繼續閱讀