详解View的工作原理

View的工作原理

View的流程主要包括测量流程(measure)布局流程(layout)绘制流程(draw)

当然除了View的这三大流程外,我们也应该了解一些View的基础概念以及View是怎么一步步绘制出来的。

我们先看下Android在UI方面的层级关系。
在这里插入图片描述

ViewRoot和DecorView

ViewRoot是ViewRootImpl类的实现,它是连接WindowManagerDecorView的纽带,View的三大流程均是通过ViewRoot来完成的。

当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建一个 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立连接,源码如下:

root = new ViewRootImpl(view.getContext(),display);
root.setView(view, params, panelParentView);

DecorView是整个Window界面的最顶层View,它的内部只有一个竖直方向的LinearLayout。这个LinearLayout里一般会分为两部分,标题栏和内容栏(根据Android版本和主题的不同,具体情况不同)。DecorView 实际是一个FrameLayout, View层的事件都先经过DecorView才传递给我们的子View。

View 的绘制流程是从 ViewRootperformTraversals 方法开始的,它经过 measurelayoutdraw 三个过程才能最终将一个 View 绘制出来。

  • measure方法用于测量View的宽高
  • layout方法用于确定View在父容器中的位置
  • draw方法负责把View绘制在屏幕上

在这里插入图片描述

如上图,performTraversals 会依次调用 performMeasureperformLayoutperformDraw 三个方法,这三个方法分别完成顶级 View 的 measurelayoutdraw 这三大流程,其中在 performMeasure中会调用 measure 方法,在 measure方法中又会调用onMeasure方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。

同理,performLayoutperformDraw 的传递流程和 performMeasure 是类似的,唯一不同的是,performDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

MeasureSpec

MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说很大程度上是因为这个过程还受父容器的影响。在测量过程中,系统会将View的LayoutParams根据父容器所施加的转换规则转换为对应的MeasureSpec,然后再根据这个MeasureSprc来测量出View的宽/高。

1.什么是MeasureSpec?

MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小)。SpecMode和SpecSize都是int值,一组SpecMode和SpecSize可以打包成一个MeasureSpec,一个MeasureSpec也可以通过解包得到其对应的SpecMode和SpecSize:

//MeasureSpec源码——部分重点代码
		private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
      	
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
       
        public static final int EXACTLY     = 1 << MODE_SHIFT;
		
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //将 size和mode打包成一个MeasureSpec
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
   
            if (sUseBrokenMakeMeasureSpec) {
   
                return size + mode;
            } else {
   
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
       //MeasureSpec解包出mode
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
   
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        //MeasureSpec解包出size
        public static int getSize(int measureSpec) {
   
            return (measureSpec & ~MODE_MASK);
        }       

由上述代码可以看出,SpecMode有三类:

  • UNSPECIFIED,父容器不对View有任何限制,要多大给到大,这种情况一般用于系统内部,表示一种测量出状态。
  • EXACTLY,父容器已经检测出View的精确的大小,这时候View的最终大小就是SpecSize确定的值,它对应LayoutParams中的match_parent和具体数值这两种模式。
  • AT_MOST,父容器指定一个可用大小即SpecSize,View的大小不能大于这个值,它对应LayoutParent中的wrap_content。

2.MeasureSpec与LayoutParams的关系

在View测量的时候,系统会将LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽\高。

MeasureSpec不是唯一有LayoutParams决定的,而是需要LayoutParams和父容器一起决定View的MeasureSpec,进而决定View的宽/高。

对于顶级View(DecorView)普通View来说,MeaSureSpec的转换过程略有不同。

对于顶级View

对于DecorView,其MeasureSpec由窗口的尺寸和自身的LayoutParams来共同决定。

ViewRootImpl类的measureHierarchy方法中的一段代码展示了DecorView的MeasureSpec创建过程。其中desiredWindowWidth,desiredWindowHeight是屏幕尺寸,lp.widthlp.height是顶级View对应LayoutParams中设置的值:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

接着我们看看getRootMeasureSpec的实现:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
   
        int measureSpec;
        switch (rootDimension) {
   
        case ViewGroup.LayoutParams.MATCH_PARENT: //精确模式,大小就是窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT: //最大模式,大小不定,但不能超酷窗口大小
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default: //精确模式,大小为LayoutParams中指定的大小
           measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看出顶级View中的MeasureSpec就是根据自身的LayoutParams中的宽/高参数来决定的

对于普通View

对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,其MeasureSpec一旦确定,onMeasure中就可以确定其测量的宽/高。

已知View的measure过程是由ViewGroup传递过来的,所以我们一起看看ViewGroup中的measureChildWithMargins方法:

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

有上述代码可以看出子元素MeasureSpec的确定不止和父元素的MeasureSpec和自身的LayoutParams有关,还和View的margin和padding有关。

我们在看看具体是如何创建一个MeasureSpec的,这句需要看看ViewGroup中的getChildMeasureSpec方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
   
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec); //父容器的specSize
		/*
		padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的
		尺寸减去padding
		*/
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
   	//父容器的specMode
       //可以看出父容器的specMode不同,结果不同
        case MeasureSpec.EXACTLY:	
            if (childDimension >= 0) {
   		//childLayoutParams
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值