教你步步为营掌握Android中的自定义ViewGroup

ybsoft 8年前
   <p>本篇是《教你步步为营掌握自定义View》一文的姊妹篇。自定义ViewGroup的文章很多,但都有一个缺点,没有回应用户关切,比如我在读那些文章时,就很想知道,自定义的ViewGroup如何使用layout_gravity?在onMeasure中,自定义的ViewGroup会将所有子View的尺寸加起来设置成自己的尺寸,如果超过了自定义ViewGroup的parent限定的尺寸怎么办?而且onMeasure中,ViewGroup给 <strong>每一个</strong> 子View设置的MeasureSpec中的宽高都是它的parent给它的宽高,为什么不是每measure一个子View,就把它的尺寸减去后再去measure下一个子View呢?如果一个ViewGroup把一个子View layout到了自己掌控的屏幕区域之外,这个View还怎么发挥作用呢?如果你也有类似的疑问,这篇文章也许能让你有茅塞顿开之感。</p>    <h2>一、自定义ViewGroup必须清楚的基本原理</h2>    <p>在学习一个技术点的时候,我会先搞清楚它的基本原理,然后再动手编码,因为我希望对自己写的每一行代码最终将会怎样执行有精准的把握。否则,我写代码时就没有底气,就像在棉花堆上走路,每一步都会心里发虚。学习自定义ViewGroup当然也不例外。下面,我们就一起看看自定义ViewGroup的原理吧。</p>    <h2>1、自定义ViewGroup本质上就干一件事-layout</h2>    <p>通过上篇文章,我们知道ViewGroup是一个组合View,它与普通的基本View(只要不是ViewGroup,都是基本View)最大的区别在于,它可以容纳其他View,这些View既可以是基本View,也可以ViewGroup,但是在我们的ViewGroup眼中,不管是View还是ViewGroup,它们都抽象成了一个普通的View,ViewGroup的 <strong>最最根本的职责</strong> 就是,在自己内部,给它们每一个人找一个合适的位置,也就是调用它们的如下方法:</p>    <pre>  <code class="language-java">public void layout(int left, int top, int right, int bottom)</code></pre>    <p>如下图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/70e6b728bbf0c9c787a7b9a58b2e190b.png"></p>    <p style="text-align:center">ViewGroup-demo.png</p>    <p>这个方法,可谓是一箭双雕,既确定了子View的位置,也确定了子View的大小,请注意,这个大小是由我们的ViewGroup最后决定的分给该子View的屏幕区域大小,一般情况下,作为老大哥,我们的ViewGroup在设定这个大小时,会考虑子View的自身要求的,也就是它们measured的大小(getMeasuredWidth , getMeasuredHeight),通常最后给每个子View设定的大小就是它们所要求的大小,但这不是绝对的。假如有一个二愣子性格的ViewGroup,它宣称:“我所有的子View的大小都必须是30*30的尺寸!”,这种SB的ViewGroup在调用每个子View的layout方法时,通过让bottom-top=right-left=30,就把所有的子View最后占据的屏幕区域设定为30*30了,不管各个子View所要求的大小是多少,此时都没有任何用处了。当然,除了有特殊需求,我相信没人愿意用这种ViewGroup的,这里我们可以知道,我们自定义ViewGroup,大体上有两条路可选,一条就是让这个ViewGroup满足我们开发中的特定需求,这个时候,你可以随心所欲地去定义ViewGroup,反正我也只是自己用,不打算给别人用的。另一条就是自定义一个ViewGroup,提供给更多的人使用,这个时候,你就要遵守一些基本的规矩,让你的ViewGroup符合使用者的使用习惯和期望,这样大家才能愿意用你的ViewGroup。那么使用者使用一个ViewGroup最基本的期望是什么?我想,应该是使用者放入这个ViewGroup中的子View layout出来的尺寸和每个子View measured的尺寸相符。只有这样,才能确保使用者的每个子View顺利完成自己的交互任务。</p>    <p>对于上面的图,有两点非常容易让人产生误解,需要解释一下:</p>    <ul>     <li>关于left、right、top、bottom。它们都是坐标值,既然是坐标值,就要明确坐标系,这个坐标系是什么?我们知道,这些值都是ViewGroup设定的,那么,这个坐标系自然也是由ViewGroup决定的了。没错,这个坐标系就是以ViewGroup左上角为原点,向右x,向下y构建起来的。进一步我们又想问,ViewGroup的左上角又在哪里呢?我们知道,在ViewGroup的parent(也是ViewGroup)眼中,我们的ViewGroup就是一个普通的View,parent也会调用我们的ViewGroup的如下方法: <pre>  <code class="language-java">//注意,这个layout方法是ViewGroup的parent在layout我们的ViewGroup,  //不要和我们的ViewGroup layout自己的子View搞混了。  public void layout(int left, int top, int right, int bottom)</code></pre> 此时,我们的ViewGroup的左上角,就是在parent的坐标系内的点(left,top)。好奇的你可能又问,假如我们的ViewGroup没有parent,它的左上角在屏幕上的位置又该如何确定?上篇文章中,我们提到,系统控制的Window都有一个DecorView,我们所能创建的View也好,ViewGroup也好,都是它的儿子、孙子、重孙、重重孙......,所以不用担心我们的ViewGroup没有parent,至于DecorView左上角在屏幕上的位置,是由系统帮我们决定的,我们不用操那么多心。<br> 由此我们看到,Google创建的这一套坐标系统非常的高效,只要确定DecorView左上角在屏幕上的位置,那么,所有的View在屏幕上的相对位置都可以精准地确定。</li>     <li>第二点就是上图中代表ViewGroup的那个方框。这个方框是什么意思,是代表ViewGroup的大小吗?如果是的话,这个大小是不是ViewGroup在onMeasure方法中设定的各个子View大小的和?正确的答案是,这个方框是ViewGroup的parent在layout我们的ViewGroup时,给ViewGroup设定的大小,parent调用我们的ViewGroup的如下layout方法: <pre>  <code class="language-java">//注意,这个layout方法是ViewGroup的parent在layout我们的ViewGroup,  //不要和我们的ViewGroup layout自己的子View搞混了。  public void layout(int left, int top, int right, int bottom)</code></pre> 上图中,代表ViewGroup的方框的宽是上述方法中的right-left,方框的高是bottom-top。我们一般将这个宽高称为 <strong>availableWidth和availableHeight</strong> (请记住这两个值,下面还要用到),它们表示的是我们的ViewGroup总共可以获得的屏幕区域大小(请仔细体会available的含义)。那么问题来了,假如我们的ViewGroup的parent是二球货,给我们的ViewGroup设定的宽高小于我们的ViewGroup measured的宽高,让我们的ViewGroup怎么优雅地layout自己的子View 呢?</li>    </ul>    <p>答案是:我们的ViewGroup在layout自己的子View时,想怎么layout就怎么layout,可以diao,也可以不diao parent给自己设定的尺寸。</p>    <p>为什么是这样呢?既然可以不diao这个尺寸,为什么我们的ViewGroup还要辛苦地在onMeasure方法中计算每一个子View的宽高,还二乎乎地将它们的尺寸加起来,告诉它的parent呢?我头有点晕,让我歇一会儿。好吧,看张美图提提神!</p>    <p><img src="https://simg.open-open.com/show/3263a4be7a23130e4c1f34f28d84af97.png"></p>    <p style="text-align:center">图片来源网络-如侵删</p>    <h2>2、为了优雅地layout,必须先把尺寸的问题搞明白</h2>    <p>上文中,ViewGroup在自己的layout方法中,获得了parent给自己设定的尺寸大小,即 <strong>availableWidth和availableHeight</strong> ,这个值相当于parent告诉ViewGroup:“请以你的左上角为圆点,向右为x,向下为y的坐标系,给你的每一个子View确定位置和大小。我可以向你保证,这个坐标系中的点P1(0,0)、点P2(availableWidth,0)、点P3(0,availableHeight)、点P4(availableWidth,availableHeight)组成的方框区域内的子View都可以获得在手机屏幕(这里指硬件意义上的屏幕)上展示自己的机会。这个方框之外的子View,能不能在手机屏幕上展示自己,我就管不了了。”从这里我们看到,parent给我们的ViewGroup设定的尺寸,并不一定就完全对应着手机屏幕上的一块相同大小的区域,在有些情况下,parent给我们的ViewGroup设定的这个尺寸可能比整个手机屏幕还大。但是,parent仍然向我们保证,在该区域内layout的子View,都能获得在手机屏幕上展示自己的机会,parent是如何做到这一点的呢?答案是:通过parent的scroll功能。这里我们不详细叙述scroll,如果你不是很理解,请查看相关资料。</p>    <p>好奇的我们可能要问:“假如我是一个ViewGroup,我把一个子View的一部分layout在了parent给定的区域内,另一部分超出了该区域,这个子View是不是最多只能获得部分展示自己的机会?”不用怀疑,答案是:Yes!</p>    <p>你可能还要问:“那些在完全被layout在parent限定的区域之外的子View应该怎么办呢?它们难道就该在无边黑暗中永不见天日吗?”这确实有点残酷,所以,作为一个ViewGroup,你可以有三个选择:</p>    <p>选择一:很简单,不要将子View 放到这个区域之外,万事大吉!</p>    <p>如果这个ViewGroup的子View数量太多,parent给限定的区域实在放不下它们怎么办?此时ViewGroup可以让子View重叠,以便所有的子View能够在parent限定的区域内layout出来。像下面这样:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/576510fbc418f36b53e46732710664ee.jpg"></p>    <p style="text-align:center">dieluohan.jpg</p>    <p>选择二:让你的ViewGroup实现scroll功能,从而确保parent限定区域外的子View也能够有机会展示自己。</p>    <p>选择三:将你的ViewGroup的parent换成ScrollView。这样你的ViewGroup就不用自己实现scroll功能了。但是ScrollView只能允许子View的高度超过自己,不允许子View的宽度超过自己。所以,作为ViewGroup,可以在不超过availableWidth的情况下,将子View layout 到任意的高度上。如下图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fc697aff8467339ad4d651ca9734504c.png"></p>    <p style="text-align:center">ViewGroup-demo1.png</p>    <p>看到没?作为一个优秀的ViewGroup,当你layout自己的子View时,只要保证子View在availableWidth之内,即使超过了parent要求的高度也没有关系,开发者还是愿意使用你的,因为他们可以为你指定ScrollView作为parent。这就是我们看到许多的ViewGroup在layout 子View时, <strong>宁超高度,不超宽度</strong> 的原因。</p>    <p>关于ScrollView怎样实现的scroll功能,讲起来比较复杂,我们暂时放下不表。</p>    <p>至此,你应该明白,上文中我们提出的,对于parent指定的availableWidth和availableHeight,作为ViewGroup还是要尽量不超过parent限定的区域,如果一定要超过的话,那就超availableHeight,而不要超availableWidth。</p>    <h2>3、让我们了解一下layout_gravity</h2>    <p>我们看到,Android系统提供的FrameLayout、LinearLayout等都支持子View设定layout_gravity,它到底是干什么用的?我们自己自定义ViewGroup时能不能也用上它?</p>    <p>关于它的作用,一句话就能说明白,当ViewGroup给子View分配的空间超过子View要求的大小时,就需要gravity帮助ViewGroup为子View精确定位。可见,layout_gravity就是ViewGroup在layout阶段,协助ViewGroup给它的子View确定位置的,没错,就是协助确定子View的 left,top,bottom,right四个值。</p>    <p>下面,我们以FrameLayout为例来进行说明。假设FrameLayout中有一个子View,这个子View的所要求的展示尺寸(measuredWidth,measuredHeight)小于FrameLayout的尺寸,但是FrameLayout是个实心眼,它不管子View要求多大,都会把它所有的屏幕区域给子View,这样就可以保证,用户在这个区域中的交互动作,都是与子View的交互。那么问题来了,FrameLayout在layout子View时,总不能让它的left和top为0,right和bottom等于自己的宽和高吧。如果这么干,子View就要在这个尺寸下,绘制自己,就不可避免地要对它包含的drawables进行拉伸,展示效果必然受到影响,那怎么办?</p>    <p>FrameLayout会提取子View的 LayoutParams中的gravity,看看子View想在哪个位置,假设子View的layout_gravity的值是"top|left",那么FrameLayout就会把子View layout到自己的左上角,大小嘛就是子View所要求的大小。但是请注意,虽然此时子View绘制时是按照自己要求的大小绘制的,但是,能与它发生交互的区域却是整个FrameLayout所占的屏幕区域。</p>    <p>所以,要不要使用layout_gravity,就看你自定义的ViewGroup是不是给子View分配大于它们要求的空间。</p>    <h2>二、实战自定义ViewGroup</h2>    <p>好了,下面我们就通过实战来检验下我们刚学到的自定义ViewGroup的知识。我们将定义的ViewGroup,名为StaggerLayout。它展示的效果是这样的:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/685efb49ae22f6bd54d12459f40e6884.png"></p>    <p style="text-align:center">StaggerLayout-demo.png</p>    <p>代码如下:</p>    <pre>  <code class="language-java">package com.milter.www.customviewgroupforblog;    import android.content.Context;  import android.util.AttributeSet;  import android.util.Log;  import android.view.View;  import android.view.ViewGroup;    /**   * Created by Administrator on 2016/8/14.   */  public class StaggerLayout extends ViewGroup {      public static final String TAG = "StaggerLayout" ;        /*      首先,定义好我们的四个构造方法,注意,ViewGroup的构造方法与上篇中的自定义View AnalogClock遵循相同的最佳实践。       */        //第一个构造方法      public StaggerLayout(Context context) {          this(context, null);      }      //第二个构造方法      public StaggerLayout(Context context, AttributeSet attrs) {          this(context, attrs, 0);      }      //第三个构造方法      public StaggerLayout(Context context, AttributeSet attrs, int defStyleAttr) {          this(context, attrs, defStyleAttr, 0);        }      //第四个构造方法      public StaggerLayout(Context context, AttributeSet attrs,                           int defStyleAttr, int defStyleRes) {            super(context, attrs, defStyleAttr, defStyleRes);        }          @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {              /*          maxHeight和maxWidth就是我们最后计算汇总后的ViewGroup需要的宽和高。用来报告给ViewGroup的parent。            在计算maxWidth时,我们首先简单地把所有子View的宽度加起来,          如果该ViewGroup所有的子View的宽度加起来都没有          超过parent的宽度限制,那么我们把该ViewGroup的measured宽度设为maxWidth,          如果最后的结果超过了parent的宽度限制,我们就设置measured宽度为parent的限制宽度,          这是通过对maxWidth进行resolveSizeAndState处理得到的。            对于maxHeight,在每一行中找出最高的一个子View,然后把所有行中最高的子View加起来。          这里我们在报告maxHeight时,也进行一次resolveSizeAndState处理。               */          int maxHeight = 0;          int maxWidth = 0;            /*              mLeftHeight表示当前行已有子View中最高的那个的高度。当需要换行时,把它的值加到maxHeight上,              然后将新行中第一个子View的高度设置给它。              mLeftWidth表示当前行中所有子View已经占有的宽度,  当新加入一个子View导致该宽度超过parent的宽度限制时,  增加maxHeight的值,同时将新行中第一个子View的宽度设置给它。             */            int mLeftHeight = 0;          int mLeftWidth = 0;            final int count = getChildCount();          Log.d(TAG,"Child count is " + count);          final int widthSize =  MeasureSpec.getSize(widthMeasureSpec);            Log.d(TAG,"widthSize in Measure is :"+ widthSize);              // 遍历我们的子View,并测量它们,根据它们要求的尺寸进而计算我们的StaggerLayout需要的尺寸。          for (int i = 0; i < count; i++) {              final View child = getChildAt(i);                //可见性为gone的子View,我们就当它不存在。              if (child.getVisibility() == GONE)                  continue;                // 测量该子View              measureChild(child, widthMeasureSpec, heightMeasureSpec);                //简单地把所有子View的测量宽度相加。              maxWidth += child.getMeasuredWidth();              mLeftWidth += child.getMeasuredWidth();                //这里判断是否需将index 为i的子View放入下一行,如果需要,就要更新我们的maxHeight,mLeftHeight和mLeftWidth。              if (mLeftWidth > widthSize) {                  maxHeight += mLeftHeight;                  mLeftWidth = child.getMeasuredWidth();                  mLeftHeight = child.getMeasuredHeight();                } else {                    mLeftHeight = Math.max(mLeftHeight,   child.getMeasuredHeight());              }            }            //这里把最后一行的高度加上,注意不要遗漏。          maxHeight += mLeftHeight;            //这里将宽度和高度与Google为我们设定的建议最低宽高对比,确保我们要求的尺寸不低于建议的最低宽高。          maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());          maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());            //报告我们最终计算出的宽高。          setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),                  resolveSizeAndState(maxHeight, heightMeasureSpec, 0));      }        @Override      protected void onLayout(boolean changed, int l, int t, int r, int b) {                final int count = getChildCount();                //childLeft和childTop代表在StaggerLayout的坐标系中,能够用来layout子View的区域的              //左上角的顶点的坐标。              final int childLeft = getPaddingLeft();              final int childTop = getPaddingTop();                //childRight代表在StaggerLayout的坐标系中,能够用来layout子View的区域的              //右边那条边的坐标。              final int childRight = r -  l - getPaddingRight();              /*            curLeft和curTop代表StaggerLayout准备用来layout子View的起点坐标,这个点的坐标随着            子View一个一个地被layout,在不断变化,有点像数据库中的Cursor,指向下一个可用区域。            maxHeight代表当前行中最高的子View的高度,当需要换行时,curTop要加上该值,以确保新行中            的子View不会与上一行中的子View发生重叠。           */             int curLeft, curTop, maxHeight;                maxHeight = 0;              curLeft = childLeft;              curTop = childTop;                for (int i = 0; i < count; i++) {                  View child = getChildAt(i);                    if (child.getVisibility() == GONE)                      return;                    int curWidth, curHeight;                  curWidth = child.getMeasuredWidth();                  curHeight = child.getMeasuredHeight();                  //用来判断是否应当将该子View放到下一行                  if (curLeft + curWidth >= childRight) {                      /*                      需要移到下一行时,更新curLeft和curTop的值,使它们指向下一行的起点                      同时将maxHeight清零。                       */                      curLeft = childLeft;                      curTop += maxHeight;                      maxHeight = 0;                  }                  //所有的努力只为了这一次layout                  child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);                  //更新maxHeight和curLeft                  if (maxHeight < curHeight)                      maxHeight = curHeight;                  curLeft += curWidth;              }      }      }</code></pre>    <p>好了,这样我们就基本掌握了自定义ViewGroup了。实际上,自定义ViewGroup是一个可难可简的事,关键是要满足自己的需求。如果要定义出一个能够满足大多数开发者使用需求的自定义ViewGroup,就像LinearLayout和RelativeLayout那样的,还是很有难度的,如果你不信,你可以看看它们的源码。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/5e61b6af4e4c</p>    <p> </p>