android基础自定义view相关自定义view

一篇文章理解Android 视图树的测量过程

2017-03-28  本文已影响775人  楚云之南

好久没有写文章了,最近公司社招窗口重新打开了,又忙着面试,在面试过程中发现自己已经有些不知道问候选人什么问题了...大写的尴尬。特别是现在很多同学准(各)备(种)充(背)分(书),通常我刚问请你描述一下AndroidView的测量过程,候选人已经开始如长江流水滔滔不绝地背书,怎么去甄别他们是真懂还是短时间突击?短时间突击不是不可以,我们需要的人才是真正能够理解这个过程的人,知其然而且知其所以然,这样在真正项目中遇到问题的时候,你才能快速定位到问题。基于此,我只好把这块东西的源码再过一遍,其实今天的这篇文章是我14年发表在公司内部wiki上面的博文,稍微整理一下,放出来跟大家分享一下吧。

如果你能回答出来如下的问题,那么这篇文章可能对你没有太大的帮助,你可以略过了。也欢迎大家在评论中提出自己的答案,我们可以一起讨论讨论。

带着这些问题,我们进入今天的正文:

1 测量流程主线

AndroidView的测量是一个比较复杂的过程,但是在Android中所有跟视图树相关的内容,请你记住一条原则,他们都是从根布局开始,然后遍历到叶子View,抓住这根主线之后,我们来看看AndroidMeasure的过程吧。

首先,整个过程的开始都是在ViewRootImpl中开始的,至于ViewRootImpl是个什么东东,它实际上是在ActivityWindow中间的一个代理层,系统消息都是通过发送到ViewRootImpl,来触发整个视图树的响应,ok,你了解到这里就行了,如果需要详细知道系统消息具体是怎么流转到ViewRootImpl,建议你在网上搜索一下吧,很多文章都是描述这个过程,ViewRootImpl有个方法performTraversals(),它是整个视图树进行绘制的入口,我们常说的绘制三大流程都是在这里触发的。

private void performTraversals() {  
  //省略代码
  //measure入口
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);        
  //省略代码
  performLayout(lp, desiredWindowWidth, desiredWindowHeight);     
   //省略代码
  performDraw();                                                      
}

既然performMeasure是入口,那么它具体是怎么做的呢?因为ViewRootImpl会持有ActivityDecorView,所以在performMeasure中,它就会直接去调用DecorViewmeasure方法,我们知道DecorView是整个Activity的视图树的根布局,通常情况下它是一个FrameLayout(所以它自然是一个ViewGroup),所以这里就开启了从上往下的遍历measure过程。

注意,这篇文章基于的SDK版本可能是5.x,如果你是4.x或者更老的版本,ViewRootImpl里面是没有这里所说的performMeasure performLayout performDraw方法的,它是直接调用Decorviewmeasure layout draw方法,本质上没有啥区别。

View中和测量过程相关的方法有三个,measureonMeasuresetMeasuredDimension。相应的,View的测量过程有三步:

根据前面说的,我们大概已经知道了视图树的整体测量流程:


Paste_Image.png

下面我们再来看看整个测量过程中的具体的细节。

2 测量过程的细节

既然我们已经找到测量视图树的入口了,是不是就可以开始接着往下撸源代码了呢?稍等,我们先来了解一下整个测量流程中一个非常重要的类:View.MeasureSpec

2.1 View.MeasureSpec

在测量过程中,你可以看到父View和子View之间的数据传递就是普通的int类型,比如measure(int widthMeasureSpec, int heightMeasureSpec)的函数原型。在Android中,这个int类型其实包含了两部分信息:大小(specSize)和模式(mode),mode指的是父View期望子View按照某种建议去测量,specSize是具体的大小。其中高两位表示mode、低三十位表示specSize。为了避免我们自己去进行这些移位操作,Android提供了一个工具类MeasureSpec,可以方便的根据它去操作,生成一个包含modespecSize的int值。

mode有三种类型:

大家都知道,在AndroidView其实并不是一个抽象类,也就是我们可以直接new出来一些View的实例,那么View肯定也处理了measure过程,我们看看它是怎么做的吧:

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

我们可以看到View的默认实现很简单,直接调用了setMeasuredDimension()设置测量的结果。其中getSuggestedMinimumWidthgetSuggestedMinimumHeight都是我们通常给View设置的最小宽高,比如android:minWidth="23dp"。我们接着来看看getDefaultSize()这个函数,跟进去看看:

public static int getDefaultSize(int size, int measureSpec) {
    //size 的值就是外面传进来的最小值
    int result = size;
    //父View传给子View的模式
    int specMode = MeasureSpec.getMode(measureSpec);
    //父View传给子View的大小
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
     case MeasureSpec.UNSPECIFIED:
        // 代码1
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        // 代码2
        result = specSize;
        break;
    }
    return result;
}

我们看到View默认实现的测量还是挺简单的,代码1处,如果父View指定的ModeUNSPECIFIEDView直接返回它自己最小值。代码2处,AT_MOSTEXACTLY都是直接返回父View传递进来的值。

看到这里,我相信你已经有点蒙逼了,最大的疑惑是父View传进来的ModeSize是怎么算的?下面我就来解决这个疑惑吧,我们把代码切到ViewGroup中来。

2.2 ViewGroup的Measure流程

我们注意到ViewGroup它是一个抽象类,所以我们并不能直接new一个ViewGroup实例,那我们继承一个试试:

 public class MyView extends ViewGroup {
    public MyView(Context context, AttributeSet attrs) {
      super(context, attrs);
    }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for(int i=0;i<getChildCount();i++) {
        View child = getChildAt(i);
        child.layout(l,t,child.getMeasuredWidth(),child.getMeasuredHeight());
    }
  }
}

这里为了演示方便,我们直接把MyView中所有的子View放到了左上角(onLayout中处理),xml中这样指定:

  <com.chuyun932.learn.view.MyView
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:background="#ff0000"
      xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView android:layout_width="match_parent"
          android:layout_height="100dp"
          android:background="#00ff00"
          android:text="测试"
          android:id="@+id/test"
    />
</com.chuyun932.learn.view.MyView>

我们看一下页面run起来的结果:

Paste_Image.png

我们明明给TextView设置了match_parent100dp的高度,结果View并没有显示到界面中间来,你能解释为什么吗?因为MyView并没有重写ViewonMeasure,所以在View的默认实现中,它只会去measure自己(当前是MyView),所有MyView的子View都得不到measure的机会,所以他们的getMeasureWidth都是0,那么在Layout阶段我们依据measure的值去布局的时候,自然也就不会给它分配布局空间了。

虽然ViewGroup没有实现omMeasure的过程,但是它提供了两个工具方法:measureChildren()getChildMeasureSpec()

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

measureChildren中,ViewGroup对每一个不为GONEView调用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);
}

measureChild也很简单,我们终于找到调用Viewmeasure的入口了,参数产生的位置就在getChildMeasureSpec中,所以这就是我们今天这篇文章的核心内容啦,我们单独起一节来说它。

2.3 getChildMeasureSpec生成参数

我们在一开始就说了,在视图树从根开始进行遍历的过程中,传递的参数就是int类型的变量,它有两个含义,mode和大小,那我们下面来看看ViewGroup中提供的工具方法是如何产生给子View的参数的吧。

首先,getChildMeasureSpec()的输入就很有意思,第一个参数是外面传递给当前这个ViewGroup的参数;第二个参数是当前ViewGrouppadding值,第三个参数是子Viewlayoutparams.layout_heightlayoutparams.layout_width

我们设置layout_height的方式一共有三种,match_parentwrap_content和直接给一个值。match_parentwarp_content都是一个负值,所以我们判断第三个参数是否 > 0,就可以知道子View是否设定了一个确切的值。在ViewGroup实现的时候,这三种方式其实就会影响测量流程中的MODE

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //拿到ViewGroup的父View传递进来的mode和size,其实就是当前ViewGroup的measure参数
    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) {
      case MeasureSpec.EXACTLY:
        //如果子View设置了一个确定值
        if (childDimension >= 0) {
            //直接给它确切值,模式是EXACTLY
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //直接给它当前ViewGroup的大小,模式是EXACTLY
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // 代码1   如果子View设置的是wrap_content,那么把当前GroupView的大小给它,然后告诉它最大是这么多了
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    //如果外面传给ViewGroup的mode是给最大值
    case MeasureSpec.AT_MOST:
        //如果子View设置了一个确定值,那么还是直接给子View它期望的值
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //告诉子view你最大也就这么大
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 父View让我们自己决定你有多大
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) 
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面这个方法很长,但是我们能看出来一些规律:

1、 如果子View设置了layout_width(height),那么一般情况下ViewGroup会直接按照它需要的宽高设置spec大小,同时modeEXACTLY,也就是说对于一个行为良好的ViewGroup,它不应该去改变这个约定。但是。。。不应该!=不能。
2、如果子View设置了我们设置了layout_width(height)="wrap_content",那么子传递给子View的就是当前ViewGroup的大小,同时指定modeAT_MOST,告诉子View你自己去measure你自己,但是不能超过我的大小。
3、什么时候用UNSPECIFIED
要说清这个事情,我先卖个关子,我们先来说明另外一个东东。坚持看到这里的你有没有一个疑问,最顶层的DecorView measure()方法的参数是谁传递给它的?是什么?

2.4 DecorView 测量入口参数

根据前面的分析,你要找这个入口上哪里看代码?没错,就是ViewRootImpl中去,我们看到执行视图树的measure过程的函数其实也是接收两个int参数:

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);   //直接调用DecorView的measure方法!!入口

baseSizedesiredWindowHeight参数其实是当前Window的大小,lp在这里是Window.LayoutParams,是Activity设置的WindowLayoutParams,当然一般情况下都是match_parent。那我们看看getRootMeasureSpec做了什么:

private 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: 
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 
        break; 
    } 
  return measureSpec; 
}

一般Activity都是设置的Window的属性都是match_parent,那么这里传递给DecorView的参数就是 window的大小+ EXACTLYmode

这个时候,我们再来看看UNSPECIFIED的话题,你会发现在整个视图树测量中,正常情况下我们完全走不到UNSPECIFIED这个分支,为什么?因为顶层传入的mode就是EXACTLYViewGroup默认实现在传递给子View的时候,只有外面传给自己是UNSPECIFIED的时候,它才会传递UNSPECIFIED给子View。那为什么存在UNSPECIFIED这个模式呢?

从ViewGroup的角度来看,如果一个子View设置了match_parentwrap_content,前者我直接吧自己的大小传递给子View,并指定modeEXACTLY;后者我还是把自己的大小传给子View,并告诉它你最大不能超过我这个值。除了这两种场景,你想想还有别使用场景吗?

比如在ScrollView中,ScrollView能包含一个LinearLayout的子View,这个时候其实LinearLayoutmeasure自己的时候,其实就不需要参考父View的大小,所以ScrollView会给它的子Viewmode设置成UNSPECIFIED

2.5 View的Measure过程

我们前面说了这么多,主要解析了ViewGroup传递参数给子View,那么子View拿到这个参数之后,就会去走自己的onMeasure,所以父View和子View的测量其实是协商的过程,父View给你建议了,子View怎么实现?当然最好是按照父View的建议来测量呗,我们来举个反例吧:

public class MyView extends View {
  public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(100,100);
  }
}

这是个傲娇的View,它在自己的onMeasure方法中直接设置了自己的measure结果,直接忽略了父View给的建议,这样的后果是什么?你在xml中给MyView设置的layout_width layout_height属性都完全失效,比如:你设置了Layout_width="50px",父View调用MyView测量的时候,它看到设置了layout_width="50px",那么父View传递给MyViewmeasure参数肯定是:modeEXACTLYspecSize50,但是MyView在自己的onMeasure里面压根就不考虑父View的建议,所以所有给它设置的Layout_widthheight都是无效的。

总结

你可以看到Android中将一个View展示到页面上是一件多么复杂的过程,measure只是万里长征第一步。其实在Andriod中,视图树的很多通知和操作都是基于父View和子View协商完成的,测量过程也是如此,后面有时间我会整理一下LayoutDraw过程,敬请期待。

上一篇下一篇

猜你喜欢

热点阅读