View的measure、layout和draw,View的工作原理

View的measure、layout和draw,View的工作原理

View的工作原理

View的绘制流程从ViewRoot的performTraversals方法开始的,经过measure,layout和draw三个过程最终将一个View绘制出来,其中measure方法测量View的宽和高,layout确定View在父容器中的位置,draw则负责将View绘制在屏幕上
performTraversals流程图
perfromTraversals中依次调用performMeasure,performLayoutperformDraw三个方法,这三个方法分别完成顶级View的measure,layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有的子元素进行measure过程,子元素重复父容器的measure过程,如此反复完成整个View树的遍历,同理,performLayout和performDraw的传递流程和performMeasure是类似的

View的measure、layout和draw

1 measure

View中实际测量工作在onMeasure中

1.1 MeasureSpec

MeasureSpec.UNSPECIFIED: 父布局不对View有任何限制
MeasureSpec.EXACTLUY: 父布局确认了view的精确大小
MeasureSpec.AT_MOST: 父布局指定一个大小,view不能大于这个值

LayoutParams.MATCH_PARENT = -1//填充父布局
LayoutParams.WRAP_CONTENT = -2//包裹内容

普通View的MeasureSpec的创建规则
总结:View的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams来共同决定的,所以View会有多种MeasureSpec情况:
1.View采用固定宽/高的时候,不管父容器的模式是什么,View的模式都是精确模式(EXACTLUY)并且其大小遵循LayoutParams中的大小
2.View的宽/高是match_parent时,父布局的模式是精确模式,那么View也是精确模式并且其大小是父容器的剩余空间
3.View的宽/高是match_parent时,父布局的模式是最大模式,那么View也是最大模式并且大小是父容器的剩余空间
4.View的宽/高是wrap_content时,不管父布局的模式是什么,View的模式总是最大模式(AT_MOST)并且大小不能超过父容器的剩余空间
5.UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般不需要关注此模式

1.2 View的measure

View的onMeasure的方法

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

onMeasure中主要的用的3个方法:

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

getSuggestedMinimumWidth

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

getSuggestedMinimumHeight

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

    }

在getDefaultSize中,我们只需要看AT_MOST和EXACTLY两种情况,即最终返回的结果就是measureSpec的specSize,这里的specSize是view测量后的大小,最终大小是在layout中确定的,几乎所有情况下他们都是相等的.UNSPECIFIED情况一般用于系统内部的测量过程,这种情况,getDefault返回的结果来自于getSuggestedMinimumWidth/getSuggestedMinimumHeight,其内部逻辑是判断View是否设置了背景,如果没有设置背景就返回最小宽度,可以为0,如果设置了背景则返回最小宽度(miniWidth)和背景最小宽度两者中的最大值

让View支持wrap_content
在自定义View时,直接继承View的自定义控件需要重写onMeasure方法并设置weap_content时的自身大小,否则自定义View的wrap_content等于match_parent.因为使用wrap_content,view的specmode是AT_MOST模式,getDefaultSize中返回的父布局的剩余空间,相当于match_parent.所以自定义view时需要重写onMeasure方法支持wrap_content

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            //设置默认的内部宽高
            setMeasureDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasureDimension(mWidth, heightMeasureSpec);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasureDimension(widthMeasureSpec, mHeight);
        }
    }
1.3 ViewGroup的measure

ViewGroup除了测量自己还会遍历去调用子元素的mesure方法,它是一个抽象类,没有重写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);
            }
        }
    }

    //测量子元素
    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);
    }

measure完成后,通过getMeasureWidth/Height方法可以获取到View的测量宽/高,但在某些情况下,系统肯会多次measure才能确定最终的测量宽/高,因此在onMeasure方法中拿到的测量宽/高可能不是最终的宽高,所以一般确定View最终宽/高都放到onLayout方法中获取

在Activity中获取View的最终宽/高
因为View的measure过程和Activity的的生命周期方法不是同步执行的,因此无法保证在哪个生命周期回调方法去获取view的最终宽/高,可以通过以下几种方式获取

  • Activity/View#onWindowFocusChanged
    该方法回调的时机如同方法名称是在window的焦点发生改变时触发,所以该方法在生命周期内会被多次执行

  • view.post(runable)
    通过post将获取宽/高的runable投递到消息队列发尾部,即调用此runable时,view已经完成了measure和layout
    View的post方法执行的时机

  • ViewTreeObserver
    使用ViewTreeObserver的OnGlobalLayoutLinstaner接口,当View树的状态发生改变或者View树内部的View可见性发生改变时,onGloablLayout方法都会被回调,可以在此获取View的宽/高.onGloablLayout随着View树的状态改变也会被多次调用

        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = listView.getMeasuredWidth();
                int height = listView.getMeasuredHeight();
            }
        });
    
  • view.measure(int widthMeasureSpec, int heightMeasureSpec)
    手动对View进行measure来得到View的宽/高

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。

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

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }


    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        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;


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

    public static boolean isLayoutModeOptical(Object o) {
        return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
    }

    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

在layout中

    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

通过setFram()/setOpticalFrame()确定View自身的位置,setOpticalFrame()内部最终也是调用了setFrame(),在setFrame中设定View的四个顶点位置,初始化mLeft,mRight,mTop和mBottom,确定View在父布局中的位置,接着调用onLayout方法,在onLayout中确定子元素的布局,View和ViewGroup的onLayout没有具体实现,onLayout和onMeasure的具体布局控件实现不一样,基本就是通过上面的流程遍历整个ViewTree完成layout

View的测量宽/高和最终宽/高有什么区别
默认情况下测量宽高就是最终宽高,但是在重写layout方法后改变了l,t,r,b中的任意一个值时就会出现不相等的情况,所以获取控件的宽高放在layout才能回去到最终的尺寸

onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。

2.draw

draw的作用是将View绘制到屏幕上面

View的绘制过程遵循如下几步:
1.绘制背景background.draw(canvas)
2.绘制自己(onDraw)
3.绘制children(dispatchDraw)
4.绘制装饰(onDrawScrollBars)

论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

setWillNotDraw

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

如果一个View不需要绘制任何内容那么设置这个标记为true以后,系统会进行相应的优化,View默认没有开启这个标记,ViewGroup默认开启这个标记
开发中我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时可以开启该标记便于系统进行后续优化,当这个自定义控件是通过onDraw来绘制内容时,需要我们显式关闭这个标记

自定义View

1.自定义View的分类
  • 继承View重新onDraw方法
  • 继承ViewGroup,派生特殊的Layout
  • 继承特定的View(系统控件)
  • 继承特定的ViewGroup(系统控件)
2.自定义View须知
  • 让View支持wrap_content
  • 让View支持padding
  • 尽量不要再View中使用Handler
  • View中的线程或动画需及时停止
  • 处理好View的嵌套滑动冲突

onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法

参考资料

Android艺术开发探索
图解View测量、布局及绘制原理
简析Window、Activity、DecorView以及ViewRoot之间的错综关系

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值