View的绘制流程

    xiaoxiao2021-12-15  41

    View的绘制是从ViewRootperformTraversals()开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘(draw),其核心也就是通过判断来选择顺序执行这三个方法中的哪个。performTraversals()这个方法完成了对顶级View的measure,layout,draw三个过程,其中又会分别对子View进行遍历,实现整个View的绘制。

     

    private void performTraversals() { ...... //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); ...... mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); ...... mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); ...... mView.draw(canvas); ...... }

    在第5~6行获取了根视图的MeasureSpec,size等于屏幕的大小,mode是At_Most。接着就调用measure()方法进行测量,所以首先看看measure的过程。

    View的measure过程

    measure()方法

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ...... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ...... }

    其中传入的两个参数信息是宽高的参数信息,用于计算实际宽高,它真实的测量是在onMeasure(int, int)中完成的,并且可以看到measure()方法是一个final方法,所以不能重写,所以我们只需要重写onMeasure()方法。至于widthMeasureSpec和heightMeasureSpec从哪里来的,下面会分析到。

    onMeasure()方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }这个方法里面很简单,就调用了setMeasuredDimension()方法,如果我们重写onMeasure()方法必须去调用这个setMeasuredDimension()方法,它去存储了view的宽高的测量信息。这里传递进来的宽高信息都是用MeasureSpec存储的,先看看这个MeasureSpec

    MeasureSpec

    它是一个32位的int值,高两位代表模式,低30位代表大小,模式有以下三种:

    UNSPECIFIED:父容器对这个view没有任何限制,可以得到任意它想要的大小

    EXACTLY:父容器已经决定了View的精确大小,也就是父容器的SpecSize的大小,相当于我们的match_parent

    AT_MOST:父容器指定了一个大小SpecSize,View不能超过这个大小,具体根据实际情况,相当于wrap_content

    一个View的MeasureSpec确定了,再通过onMeasure()方法,那么它的大小也就确定了,一个View的MeasureSpec的确定是由父容器的MeasureSpec和它自身的LayoutParams共同决定的。

    getDefaultSize()方法

    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; }这个方法是根据传入的size和父类的MeasureSpec来返回一个默认的大小。上面可以看到这里的size是通过getSuggestedMinimumWidth或者getSuggestedMinimumHeight获取的,再通过MeasureSpec计算出它的大小,这里当mode为At_Most和Exactly,返回的都是specSize。

    第3~4行,根据父类的MeasureSpec来计算出其中的mode和size。

    第6~14行,用一个switch语句根据父类不同的mode来返回不同的大小。

    protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }这个方法的意思就是如果view没有设置背景,那么宽度就是mMinWidth,也就是对应的android:minWidth这个属性,如果设置了背景自然就返回背景的宽度。

    setMeasuredDimension()方法

    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; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }这是一个必须在onMeasure中调用的方法,它会去存储测量的宽高。看看这里面调用的setMeasuredDimensionRaw()方法

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; }可以看到测量的宽高都被存储到mMeasuredWidth和mMeasuredHeight中了。

    至此一个简单view的测量就完毕了。可以知道基本上view都是嵌套的,并不会一个view这么简单,所以下面看看ViewGroup的测量。

    ViewGroup的measure()

    对于ViewGrouo来说,除了完成自身的measure之外,还需要去遍历所以的子元素,去调用它们的measure()方法,然后各个子View再递归去执行这个过程。ViewGroup是一个抽象类,并没有去重写View的onMeasure()方法,并没有定义具体的测量过程,所以需要它的子类去实现onMeasure()方法,这是因为各个ViewGroup的子类的特效不同导致的测量方法不同,但是ViewGroup中提供了measureChildren, measureChild, measureChildWithMargins方法来进行测量。

    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); } } }这里是对ViewGroup进行遍历,只要其中的子view不是GONE,那么就调用measureChild()方法

    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); }这个方法里调用了getChildMeasureSpec()方法获取到子view的MeasureSpec。然后获得child的MeasureSpec后继续调用measure方法,这里也就进入了之前分析的view的测量。在这里可以看到之前我们measure()方法传入的参数就是这里通过getChildMeasureSpec()方法得到的,下面看看这个子view的MeasureSpec是怎么计算出来的。

    getChildMeasureSpec()

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //获取父类MeasureSpec的size和mode int specMode = View.MeasureSpec.getMode(spec); int specSize = View.MeasureSpec.getSize(spec); //得到父类的除去padding的剩余大小 int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; //根据父类的mode做switch语句 switch (specMode) { // Parent has imposed an exact size on us case View.MeasureSpec.EXACTLY: if (childDimension >= 0) { //如果layout_width(layout_height)属性设置了具体的值,并且大于0 //那么child的size就等于这个具体的值,mode为Exactly resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 如果layout_width(layout_height)属性设置为match_parent //意味着child想要尽可能大,最大为多少,当然是父类剩余的大小,这个大小是个确定的值所以mode为Exactly resultSize = size; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // 如果layout_width(layout_height)属性设置为wrap_content //意味着自身想要多大就给多大,但是不能超出父类的限制 resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } break; //下面的情况就和上面的类似了 // Parent has imposed a maximum size on us case View.MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = View.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 = View.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 = View.MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case View.MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = View.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 = View.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 = View.MeasureSpec.UNSPECIFIED; } break; } return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode); }

    这个方法主要就是通过自身的LayoutParams和父类的MeasureSpec来获得child的MeasureSpec,根据上面的情况可以得到下面的对照表:

    记得之前在view的getDefaultSize()方法中,无论mode是At_Most或者Exactly都是返回specSize,根据这个表对照,可以知道当我们自定义一个View,对它的onMeasure不做任何处理的话,宽高设置成match_parent或者wrap_content,返回的大小都是parentSize,也就是充满父容器,当然设置具体的值返回的就是childSize了。所以那么如果我们要自定义view设置wrap_content时,不要充满父容器的效果该怎么实现,看看下面的处理方式。

    @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //通过getChildMeasureSpec()方法获取到的MeasureSpec,获取到它们的mode和size int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //mWidth、mHeight是固定的值 if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { //宽高都设置为wrap_content setMeasuredDimension(mWidth, mHeight); } else if (widthMode == MeasureSpec.AT_MOST) { //宽度设置为wrap_content setMeasuredDimension(mWidth, heightSize); } else if (heightMode == MeasureSpec.AT_MOST) { //高度设置为wrap_content setMeasuredDimension(widthSize, mHeight); } }这样写当你自定义的View设置为wrap_content时就不会填满父容器,而是你指定的mWidth、mHeight。

    measure总结

    从最开始遍历ViewGroup的子View,如果这个子View是ViewGroup那么再重复上面的过程,如果是View,那么就调用它的measure()方法,在measure()方法中会调用onMeasure()方法,在onMeasure()又会最终调用setMeasuredDimension(),把测量值存储起来,这样递归调用就完成了View的测量过程。

    measure()方法是final的,所以不能重写,只能重写onMeasure()方法实现自己的逻辑,但是在onMeasure()方法中必须去调用setMeasuredDimension(),因为这个方法才最终完成测量,所以要么自己去调用要么写supper.onMeasure()

    一个View的大小是由MeasureSpec决定的,一个MeasureSpec是由它父类的MeasureSpec和自身的LayoutParams决定的。

    可以调用getMeasuredWidth()和getMeasuredHeight()来获得测量大小,但是必须在测量后调用。

    layout过程

    layout是用来确定视图的位置,是View绘制的第二阶段,parent会调用每一个子view的layout方法去放置它。首先看看View的layout()方法

    public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; 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); 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,这里会调用setFrame方法来设定View四个顶点的位置,也就是初始化mLeft、mRight、mTop和mBottom;如果位置发生了变化就会调用onLayout方法,这个onLayout方法是空的,没什么好看的

    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; } } /** * {@inheritDoc} */ @Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);可以看到ViewGroup的layout方法是final类型的,并且onLayout方法是个抽象方法,所以ViewGroup的子类就必须实现这个onLayout方法,在这个方法中去确定子View的位置。既然如此,那么就来看看LinearLayout的onLayout方法是怎么实现的。

    LinearLayout的onLayout

    @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } } void layoutVertical(int left, int top, int right, int bottom) { final int paddingLeft = mPaddingLeft; int childTop; int childLeft; //得到宽度 final int width = right - left; int childRight = width - mPaddingRight; //出去padding的宽度 int childSpace = width - paddingLeft - mPaddingRight; final int count = getVirtualChildCount(); //根据Gravity计算childTop final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; switch (majorGravity) { case Gravity.BOTTOM: // mTotalLength contains the padding already childTop = mPaddingTop + bottom - top - mTotalLength; break; // mTotalLength contains the padding already case Gravity.CENTER_VERTICAL: childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; break; case Gravity.TOP: default: childTop = mPaddingTop; break; } //开始遍历 for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { //获取child测量后的宽高 final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); int gravity = lp.gravity; if (gravity < 0) { gravity = minorGravity; } //计算childLeft final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; case Gravity.LEFT: default: childLeft = paddingLeft + lp.leftMargin; break; } //加上分割线的高度 if (hasDividerBeforeChildAt(i)) { childTop += mDividerHeight; } //加上子View的marginTop childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); //childTop加上这个放置了的child的高度和marginBottom childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }这段代码可以看到会遍历所以子View并调用setChildFrame方法指定子View的位置,在其中childTop的值会不断的增加,也就是依次向下放置这些子View。

    所以在parent调用自己的layout()方法后,就会调用onLayout方法,在这个方法中又会去调用子View的layout方法,就这样一层层的传递下去就完成了整个View的layout。

    private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); }

    之前我们知道了getMeasuredWidth方法可以得到测量后的宽度,那么这个方法和getWidth又有什么不同呢?看看这个方法

    public final int getWidth() { return mRight - mLeft; } 这个mRight和mLeft是在layout过程中给他赋值的,也就是说这个方法必须在layout完成后调用。getWidth和getMeasuredWidth得到的值理论上来说都是一样的,只不过它们调用的时机不一样,getMeasuredWidth赋值要早一些,所以某些特殊情况它们的值可能不同。

    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; }draw过程的传递是通过dispatchDraw()方法实现的,这个方法也是一个空方法,如果是一个ViewGroup就需要去实现,类似的在这里面调用子View的draw方法。

    转载请注明原文地址: https://ju.6miu.com/read-1000354.html

    最新回复(0)