自定义View之View的工作原理

目录

 1.View的绘制流程从哪里开始

2.什么是MeasureSpec?

(1)SpecMode有三类

(2)创建MeasureSpec

(3)获取测量模式SpecMode和大小SpecSize

(4)MeasureSpec和LayoutParams的对应关系

3.View的工作流程

(1)measure过程

(2)layout过程

 (3)draw过程


 1.View的绘制流程从哪里开始

       View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来。

      如图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中会调用onMeasure方法,在onMeasure方法这则会对所有的子元素进行measure过程,这时候meaure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。同理,performLayout和performDraw的传递流程和performMeasure是类似的。

2.什么是MeasureSpec?

         MeasureSpec代表一个32位int值,高2位代表SpecMode,就是测量模式,低30位代表SpecSize,就是规格大小。

(1)SpecMode有三类
  • UNSPECIFIED       

父容器对子 View 没有施加任何限制,子 View 可以任意大小。一般用于系统内部。

  • EXACTLY             

 父容器已经为子 View 精确指定了大小,子 View 应该匹配这个大小。它对应于LayoutParams中的match_parent和具体的数值这两个模式。

  • AT_MOST             

 子 View 可以是任何大小,但不能超过父容器指SpecSize。它对应于LayoutParams中的wrap_content。

(2)创建MeasureSpec
MeasureSpec measureSpec=MeasureSpec.makeMeasureSpec(size,mode);

       MeasureSpec 是通过静态方法 MeasureSpec.makeMeasureSpec() 创建的,该方法接受两个参数:大小和测量模式。

(3)获取测量模式SpecMode和大小SpecSize
 int selfwidthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int selfwidthSpecSize=MeasureSpec.getSize(widthMeasureSpec);

         我们可以通过 MeasureSpec.getMode() 和 MeasureSpec.getSize() 方法来获取测量模式和大小,然后根据这些信息来确定 View 的最终大小。

(4)MeasureSpec和LayoutParams的对应关系

        在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec从而决定View的宽高。

MeasureSpec的转换流程

对于顶级的View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。

其中LayoutParams中的宽高的参数

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通的View,其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来共同确定。

  • 当View采用固定宽/高时,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。
  • 当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也会是精确模式并且大小是父容器的剩余空间。如果父容器的模式是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当View的宽/高是wrap_content时,View的模式总是最大化并且大小不能超过父容器的剩余空间。

3.View的工作流程

  View的工作流程主要是measure、layout、draw这三大流程,即测量、布局和绘制。

(1)measure过程

        1️⃣  View的measure过程

      View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,子类不能重写该方法,在View的measure方法中会去调用View的onMeasure方法,View的onMeasure方法如下所示:

#onMeasure

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

setMeasuredDimension方法会设置View宽高的测量值。

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

AT_MOST和EXACTLY情况下,getDefaultSize方法返回值为MeasureSpec的specSize 。

UNSPECIFIED情况下,getDefaultSize方法返回值宽/高为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()。

#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 没有设置背景,那么返回android:minWidth 这个属性所指定的值,这个值可以为 0; 如果 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽/高。

        2️⃣ViewGroup的measure过程

  对于ViewGroup来说,除了完成自己的measure过程以外,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。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);
            }
        }
    }

上面代码得出,ViewGroup在measure时,会对每一个子元素进行measure,measureChildren这个方法的实现如下所示:

    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 的思想就是取出子元素的 LayoutParams,然后再通过getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View的measure 方法来进行测。

(2)layout过程

      Layout的作用是ViewGroup用来确认子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法会被调用。

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

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

       layout方法的大致流程如下:首先会通过 setFrame方法来设定 View的四个顶点的位置即初始化 mLeft、mRight、mTop 和 mBottom 这四个值,View 的四个顶点一旦确定,那View 在父容器中的位置也就确定了; 接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。

❗❗View的getMeasureWidth和getWidth这两个方法有什么区别?

在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高在measure过程,而最终宽/高在layout过程,即两者的赋值时机不同。但是存在某些特殊情况会导致两者不一致,下面举例说。

如果重写View的layout方法,代码如下:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right+100, bottom+100);
    }

上述代码会导致在任何情况下View的最终宽/高总是比测量宽/高大100px。

 (3)draw过程

Draw过程的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

  1. 绘制背景background.draw(canvas)。
  2. 绘制自己(onDraw)。
  3. 绘制children(dispatchDraw)。
  4. 绘制装饰(onDrawScrollBars)。

上面可通过源码看出:

 public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        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)
         *      7. If necessary, draw the default focus highlight
         */

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

        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
            onDraw(canvas);

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

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

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (isShowingLayoutBounds()) {
                debugDrawFocus(canvas);
            }

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

猜你喜欢

转载自blog.csdn.net/weixin_63357306/article/details/135440160