Andorid的View繪制流程

前言

Android的view繪制流程是一個比較重要的問題,理解好view的繪制流程,對平時開發(fā)工作非常有幫助,同時也是面試中常問的問題。

本文嘗試從源碼角度,結合其他人的博客,描述一下我對view繪制的一些理解

概述


每個Activity都有一個window接口實現(xiàn)PhoneWindow,用于承載用戶界面。contentView就是R.id.content,setContentView()就是設置它的子View。

Window

窗口是圖像顯示的一個容器,獨占一個surface實例的顯示區(qū)域,surface由WindowManagerService分配,看一看成一張畫布,由canvas或者openGL在上面進行繪制后,通過SurfaceFlinger將多塊surface按一定順序排列混合好,輸出到FrameBuffer中,得以顯示。

android.view.Window包含三個核心組件:
  • WindowManager.LayoutParams:布局參數(shù)
  • callback:回調,一般在Activity中實現(xiàn)
  • ViewTree:窗口所承載的控件樹

PhoneWindow

它是Window接口唯一的具體實現(xiàn)。setContentView()方法設置Activity時,實際上完成了PhoneWindow的ViewTree的設置;而requestWindowFeature()用來定制Activity關聯(lián)的PhoneWindow的外觀,實際上也是將外觀特性參數(shù)存儲到了PhoneWindow的mFeatures中,在窗口繪制階段生成外觀模板時,根據(jù)mFeatures生成特定外觀。

setContentView()

此方法完成了Activity的contentView創(chuàng)建,但是并沒有進行繪制。自定義activity繼承自Activity和AppCompatActivity時,此方法的執(zhí)行邏輯不一樣。

  • 先說Activity:
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

getWindow()返回Activity關聯(lián)的PhoneWindow:

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        // mContentParent即為上面提到的ContentView的父容器,若為空則調用installDecor()生成
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 調用mLayoutInflater.inflate()方法來填充布局
        // 填充布局也就是把我們設置的ContentView加入到mContentParent中
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    // cb即為該Window所關聯(lián)的Activity
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        // 調用onContentChanged()回調方法通知Activity窗口內容發(fā)生了改變
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}
  • AppCompatActivity:
@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

getDelegate()返回繼承自AppCompatDelegate的代理類,根據(jù)不同api的level調用不同類。但是無論api是多少,這里的邏輯都是一樣的。

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

邏輯比較簡單,不多說了。

LayoutInflater.inflate()

setContentView()調用了LayoutInflater.inflate()來進行資源文件解析。

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
 
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

無論是Activity還是AppCompatActivity都把ContentView作為root參數(shù)傳進去,最終調用inflate(XmlPullParser, viewGroup, boolean)填充布局。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

        try {
            // Look for the root node.
            int type;
            // 一直讀取xml文件,直到遇到開始標記
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
            // 最先遇到的不是開始標記,報錯
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();
            ...
            // 單獨處理<merge>標簽
            if (TAG_MERGE.equals(name)) {
                // 若包含<merge>標簽,父容器(即root參數(shù))不可為空且attachRoot須為true,否則報錯
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }
                // 遞歸地填充布局
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                // temp為xml布局文件的根View
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    ...
                    // 獲取父容器的布局參數(shù)(LayoutParams)
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        // 若attachToRoot參數(shù)為false,則我們只會將父容器的布局參數(shù)設置給根View
                        temp.setLayoutParams(params);
                    }
                }

                ...

                // Inflate all children under temp against its context.
                // 遞歸加載根View的所有子View
                rInflateChildren(parser, temp, attrs, true);

                ...

                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                if (root != null && attachToRoot) {
                    // 若父容器不為空且attachToRoot為true,則將父容器作為根View的父View包裹上來
                    root.addView(temp, params);
                }

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                // 若root為空或是attachToRoot為false,則以根View作為返回值
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            ...
        } catch (Exception e) {
            ...
        } finally {
            ...
        }

        return result;
    }
}

單獨處理<merge>,調用的是rInflate():

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 獲取當前標記的深度,根標記的深度為0
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;
   
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
         // 不是開始標記則繼續(xù)下一次迭代
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();
        // 對一些特殊標記做單獨處理
        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            // 對<include>做處理
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 對一般標記的處理
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 遞歸地加載子View
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

上面的inflate()和rInflate()方法中都調用了rInflateChildren()方法:

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

View的繪制

ViewRoot

View的繪制是由ViewRootImpl負責的。
每個應用窗口window的decorView都有一個ViewRootImpl與之關聯(lián),他們之間由WindowManager(確切來說WindowManger接口的實現(xiàn)類WindowManagerImpl,繼而調用WindowManagerGlobal類)來維護關系。當ActivityThread調用handleResumeActivity()時,相當于調用了activity的onResume(),decorView與ViewRootImpl建立聯(lián)系:WindowManager.addView--WindowManagerImpl.addView--WindowManagerGlobal.addView:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
        ...
        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            ...
        }
}

View的繪制是由ViewRootImpl完成,setView內部會調用requestLayout()完成異步刷新請求。

public void setView(View view, WindowManager.LayoutParams attrs, 
          View panelParentView) {
       ...
       // Schedule the first layout -before- adding to the window
       // manager, to make sure we do the relayout before receiving
       // any other events from the system.
       requestLayout();
       ...
}


@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //View繪制入口
        scheduleTraversals();
    }
}

具體細節(jié)需要了解Activity啟動流程。

View繪制起點

scheduleTraversals()方法來調度一次完成的繪制流程,向主線程發(fā)送一個遍歷消息,最終調用ViewRootImpl的performTraversals()調用:

private void performTraversals() {
       ...
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
       ...
       performLayout(lp, mWidth, mHeight);
       ...
       performDraw();
       ...
}
  • measure: 判斷是否需要重新計算View的大小,需要的話則計算;
  • layout: 判斷是否需要重新計算View的位置,需要的話則計算;
  • draw: 判斷是否需要重新繪制View,需要的話則重繪制。

measure階段

計算每個view需要多大尺寸。
performTraversals()中通過measureHierarchy(),計算出根view的measureSpec,隨后調用performMeasure()、onMeasure()計算各層次veiw的大小。

// 傳入的desiredWindowXxx為窗口尺寸
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, 
        final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;
    ...
    boolean goodMeasure = false;
    ...
    if (!goodMeasure) {
        //獲取根的measureSpec信息,約束了decorView的寬高
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
            windowSizeMayChange = true;
        }
    }
    ...
    return windowSizeMayChange;
}

最終,獲取到viewRoot的width黑height的MeasureSpec后,傳給performMeasure()。spectMode為EXACTLY,spectSize為window尺寸。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

mView即為decorView,執(zhí)行View.measure()這個不可繼承的final方法。

/**
 * 調用這個方法來算出一個View應該為多大。參數(shù)為父View對其寬高的約束信息。
 * 實際的測量工作在onMeasure()方法中進行
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 判斷是否需要重新計算measureSpec
    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);
    }

    // Suppress sign extension for the low bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    // 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制重新布局
    // 比如調用View.requestLayout()會在mPrivateFlags中加入此標記
    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();
        // 先嘗試從緩從中獲取,若forceLayout為true或是緩存中不存在或是
        // 忽略緩存,則調用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
}
  • forceLayout為true,表示強制重新布局,可以通過View.requestLayout()來實現(xiàn);

  • needsLayout為true,這需要specChanged為true(表示本次傳入的MeasureSpec與上次傳入的不同),并且以下三個條件之一成立:

    • sAlwaysRemeasureExactly為true: 該變量默認為false;
    • isSpecExactly為false: 若父View對子View提出了精確的寬高約束,則該變量為true,否則為false;
    • matchesSpecSize為false: 表示父View的寬高尺寸要求與上次測量的結果不同。

對于decorView來說,實際執(zhí)行測量工作的是FrameLayout的onMeasure()方法。View.onMeasure():

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

而FrameLayout.onMeasure():

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

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

    //每一個子view最大寬度和最大高度
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            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
    // 最大寬度、高度加上父view和前景區(qū)域的padding
    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());
    }

    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

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

FrameLayout是ViewGroup子類,而ViewGroup中有一個View[]類型成員變量mChildren,代表當前viewGroup的所有子view的集合,該變量為private,但是可以通過publish的getChildAt(int position)獲取到View[position]的子view,以及getChildCount()獲取所有子view數(shù)量。

  1. 通過getChildAt()獲取到每一個子view,隨后measureChildWithMargins()對每一個子view進行測量,計算出所有子view中,最大寬度和最大高度,加上父View的padding和前景區(qū)域的padding,然后會檢查是否設置了最小寬高,并與其比較,將兩者中較大的設為最終的最大寬高。最后,若設置了前景圖像,我們還要檢查前景圖像的最小寬高。
  2. 得到了maxHeight和maxWidth的最終值,表示當前容器View用這個尺寸就能夠正常顯示其所有子View(同時考慮了padding和margin)。而后我們需要調用resolveSizeAndState()方法來結合傳來的MeasureSpec來獲取最終的測量寬高,并保存到mMeasuredWidth與mMeasuredHeight成員變量中。
  3. 我們可以看到,容器View通過measureChildWithMargins()方法對所有子View進行測量后,才能得到自身的測量結果。也就是說,對于ViewGroup及其子類來說,要先完成子View的測量,再進行自身的測量(考慮進padding等)。
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的測量時通過子view自身child.measure()完成的。子view測量需要自身的measureSpec,這個就需要父view的measureSpec和子view自身的layoutParams參數(shù)決定,子view的layoutParams代表著子view自身期待的大小。getChildMeasureSpec():

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

首先,根據(jù)父view的measureSpec算出specMode和specSize,在計算出父view留給子view的最大可用空間size。

  1. SpecMode為EXACTLY,表示父View對子View指定了確切的寬高限制,此時子View的LayoutParams:
    • childDimension為具體大?。鹤觱iew的resultSize為childDimension,即子View在LayoutParams指定的具體大小值;SpecMode為EXACTLY,即這種情況下若該子View為容器View,它也有能力給其子View指定確切的寬高限制(子View只能在這個寬高范圍內),若為普通View,它的最終測量大小就為childDimension
    • childDimension為match_parent:表示子View想和父View一樣大。SpecSize為size,即父View的剩余可用大小;SpecMode為EXACTLY
    • childDimension為wrap_content:表示子View想自己決定自己的尺寸(根據(jù)其內容的大小動態(tài)決定)。這種情況下子View的確切測量大小只能在其本身的onMeasure()方法中計算得出,父View此時無從知曉。所以暫時將子View的SpecSize設為size(父View的剩余大小);令子View的SpecMode為AT_MOST,表示了若子View為ViewGroup,它沒有能力給其子View指定確切的寬高限制,畢竟它本身的測量寬高還懸而未定。
  2. SpecMode為AT_MOST:
    • childDimension為具體大?。鹤覸iew的SpecSize為childDimension,SpecMode為EXACTLY
    • childDimension為match_parent:表示子View想和父View一樣大,故令子View的SpecSize為size,但是由于父View本身的測量寬高還無從確定,所以只是暫時令子View的測量結果為父View目前的可用大小。這時令子View的SpecMode為AT_MOST。
    • childDimension為wrap_content:表示子View想自己決定大?。ǜ鶕?jù)其內容動態(tài)確定)。然而這時父View還無法確定其自身的測量寬高,所以暫時令子View的SpecSize為size,SpecMode為AT_MOST。

結論:當子View的測量結果能夠確定時,子View的SpecMode就為EXACTLY;當子View的測量結果還不能確定(只是暫時設為某個值)時,子View的SpecMode為AT_MOST。
(可參考一張表格,自行谷歌)

子view層層遍歷,調用measure(),完成測量。當遞歸地執(zhí)行完所有子View的測量工作后,會調用resolveSizeAndState()方法來根據(jù)之前的測量結果確定最終對FrameLayout的測量結果并存儲起來。

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) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                // 父View給定的最大尺寸小于完全顯示內容所需尺寸
                // 則在測量結果上加上MEASURED_STATE_TOO_SMALL
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        // 若specMode為EXACTLY,則不考慮size,result直接賦值為specSize
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

上面介紹的onMeasure()是FrameLayout的,而普通view的onMeasure():

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

普通View(非ViewgGroup)來說,只需完成自身的測量工作即可。

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

View的getDefaultSize()方法對于AT_MOST和EXACTLY這兩種情況都返回了SpecSize作為result。所以若我們的自定義View直接繼承了View類,我們就要自己對wrap_content (對應了AT_MOST)這種情況進行處理,否則對自定義View指定wrap_content就和match_parent效果一樣了。—— 自定義View的注意事項

但是如果是View的派生類,如TextView、Button、ImageView等,它們的onMeasure方法系統(tǒng)了都做了重寫,不會這么簡單直接拿 MeasureSpec 的size來當大小,而去會先去測量字符或者圖片的高度等,然后拿到View本身content這個高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做為View的大小。

layout階段

DecorView繼承自FrameLayout,onLayout()會執(zhí)行super().onLayout()。FrameLayout的onLayout類似于onMeasure,getChildAt()獲得child,調用每個child的layout()。

View.layout()
public void layout(int l, int t, int r, int b) {
    ...
    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);
        ...
    }
    ...
}

setFrame()方法四個參數(shù)描述了View相對其父View的位置,setFrame()方法中會判斷View的位置是否發(fā)生了改變,若發(fā)生了改變,則需要對子View進行重新布局,changed。

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);
        ...
    }
    return changed;
}

子view布局通過onLayout()實現(xiàn),普通View( 非ViewGroup)不含子View,所以View類的onLayout()方法為空。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}
ViewGroup.layout()
@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

LayoutTransition是用于處理ViewGroup增加和刪除子視圖的動畫效果,也就是說如果當前ViewGroup未添加LayoutTransition動畫,或者LayoutTransition動畫此刻并未運行,那么調用super.layout(l, t, r, b),繼而調用到ViewGroup中的onLayout,否則將mLayoutSuppressed設置為true,等待動畫完成時再調用requestLayout()

ViewGroup.onLayout()

ViewGroup類的onLayout()方法是abstract,不同的布局管理器有著不同的布局方式。

decorView,即FrameLayout的onLayout():

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

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

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

            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}
  • parentLeft為子View顯示區(qū)域的左邊緣到父View的左邊緣的距離,根據(jù)父view的padding值確定。
  • 子View的可見性不為GONE才會進行布局
  • childLeft代表了最終子View的左邊緣距父View左邊緣的距離,由parentLeft,即父view的padding,和子view的margin,以及view自身寬度等因素計算而成。不過不同的verticalGravity、absoluteGravity等形式,計算法師不同
  • childTop同理。
  • 最后會調用child.layout()方法對子View的位置參數(shù)進行設置,若子View是容器View,則會遞歸地對其子View進行布局。

draw()

decorView.draw()繼承View.draw()

public void draw(Canvas canvas) {
    . . . 
    // 繪制背景,只有dirtyOpaque為false時才進行繪制,下同
    int saveCount;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    . . . 
    // 繪制自身內容
    if (!dirtyOpaque) onDraw(canvas);

    // 繪制子View
    dispatchDraw(canvas);

     . . .
    // 繪制滾動條等
    onDrawForeground(canvas);
 }

省略了實現(xiàn)滑動時漸變邊框效果相關的邏輯

  • View類的onDraw()方法為空,因為每個View繪制自身的方式都不盡相同,對于decorView來說,由于它是容器View,所以它本身并沒有什么要繪制的。

  • dispatchDraw()方法用于繪制子View,顯然普通View(非ViewGroup)并不能包含子View,所以View類中這個方法的實現(xiàn)為空。ViewGroup類的dispatchDraw()方法中會依次調用drawChild()方法來繪制子View

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }
    

這個方法調用了View.draw(Canvas, ViewGroup,long)方法來對子View進行繪制。在draw(Canvas, ViewGroup, long)方法中,首先對canvas進行了一系列變換,以變換到將要被繪制的View的坐標系下。完成對canvas的變換后,便會調用View.draw(Canvas)方法進行實際的繪制工作,此時傳入的canvas為經過變換的,在將被繪制View的坐標系下的canvas。

進入到View.draw(Canvas)方法后,會向之前介紹的一樣,執(zhí)行以下幾步:

  • 繪制背景;
  • 通過onDraw()繪制自身內容;
  • 通過dispatchDraw()繪制子View;
  • 繪制滾動條

參考:深入理解Android之View的繪制流程
???????????Android View的繪制流程

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容