自定义ViewGroup本质是什么?

自定义ViewGroup本质上就干一件事——layout

layout

我们知道ViewGroup是一个组合View,它与普通的基本View(只要不是ViewGroup,都是基本View)最大的区别在于,它可以容纳其他View,这些View既可以是基本View,也可以ViewGroup,但是在我们的ViewGroup眼中,不管是View还是ViewGroup,它们都抽象成了一个普通的View,ViewGroup的最最根本的职责就是,在自己内部,给它们每一个人找一个合适的位置,也就是调用它们的如下方法:

public void layout(int left, int top, int right, int bottom)
复制代码

如图所示:

这个方法,既确定了子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顺利完成自己的交互任务。

对于上面的图,有两点非常容易让人产生误解,需要解释一下:

  • 关于left、right、top、bottom。它们都是坐标值,既然是坐标值,就要明确坐标系,这个坐标系是什么?我们知道,这些值都是ViewGroup设定的,那么,这个坐标系自然也是由ViewGroup决定的了。这个坐标系就是以ViewGroup左上角为原点,向右x,向下y构建起来的。

    ViewGroup的左上角又在哪里呢?我们知道,在ViewGroup的parent(也是ViewGroup)眼中,我们的ViewGroup就是一个普通的View,parent也会调用我们的ViewGroup的如下方法:

    //注意,这个layout方法是ViewGroup的parent在layout我们的ViewGroup,
    //不要和我们的ViewGroup layout自己的子View搞混了。
    public void layout(int left, int top, int right, int bottom)
    复制代码

    此时,我们ViewGroup的左上角,就是在parent的坐标系内的点(left,top)。好奇的你可能又问,假如我们的ViewGroup没有parent,它的左上角在屏幕上的位置又该如何确定?系统控制的Window都有一个DecorView,我们所能创建的View也好,ViewGroup也好,都是它的儿子、孙子、重孙、重重孙......,所以不用担心我们的ViewGroup没有parent,至于DecorView左上角在屏幕上的位置,是由系统帮我们决定的,我们不用操那么多心。

    由此我们看到,Google创建的这一套坐标系统非常的高效,只要确定DecorView左上角在屏幕上的位置,那么,所有的View在屏幕上的相对位置都可以精准地确定。

  • 第二点就是上图中代表ViewGroup的那个方框。

    • 那么这个方框是什么意思?
    • 是代表ViewGroup的大小吗?
    • 如果是的话,这个大小是不是ViewGroup在onMeasure方法中设定的各个子View大小的和?

    正确的答案是,这个方框是ViewGroup的parent在layout我们的ViewGroup时,给ViewGroup设定的大小,parent调用我们的ViewGroup的如下layout方法:

    /注意,这个layout方法是ViewGroup的parent在layout我们的ViewGroup,
    //不要和我们的ViewGroup layout自己的子View搞混了。
    public void layout(int left, int top, int right, int bottom)
    复制代码

    上图中,代表ViewGroup的方框的宽是上述方法中的right-left,方框的高是bottom-top。我们一般将这个宽高称为 availableWidthavailableHeight(请记住这两个值,下面还要用到),它们表示的是我们的ViewGroup总共可以获得的屏幕区域大小(请仔细体会available的含义)。

    那么问题来了,假如我们的ViewGroup的parent是二球货,给我们的ViewGroup设定的宽高小于我们的ViewGroup measured的宽高,让我们的ViewGroup怎么优雅地layout自己的子View 呢?

    答案是:我们的ViewGroup在layout自己的子View时,想怎么layout就怎么layout,可以diao,也可以不diao parent给自己设定的尺寸。

    为什么是这样呢?既然可以不diao这个尺寸,为什么我们的ViewGroup还要辛苦地在onMeasure方法中计算每一个子View的宽高,还二乎乎地将它们的尺寸加起来,告诉它的parent呢?

ViewGroup如何优雅的Layout

ViewGroup在自己的layout方法中,获得了parent给自己设定的尺寸大小,即 availableWidthavailableHeight这个值相当于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,如果你不是很理解,请查看相关资料。

好奇的我们可能要问:“假如我是一个ViewGroup,我把一个子View的一部分layout在了parent给定的区域内,另一部分超出了该区域,这个子View是不是最多只能获得部分展示自己的机会?”不用怀疑,答案是:Yes!

你可能还要问:“那些完全被layout在parent限定的区域之外的子View怎么办呢?它们难道就该在无边黑暗中永不见天日吗?”这确实有点残酷,所以,作为一个ViewGroup,你可以有三个选择:

  • 选择一:很简单,不要将子View 放到这个区域之外,万事大吉! 如果这个ViewGroup的子View数量太多,parent给限定的区域实在放不下它们怎么办?此时ViewGroup可以让子View重叠,以便所有的子View能够在parent限定的区域内layout出来。
  • 选择二:让你的ViewGroup实现scroll功能,从而确保parent限定区域外的子View也能够有机会展示自己。
  • 选择三:将你的ViewGroup的parent换成ScrollView。这样你的ViewGroup就不用自己实现scroll功能了。但是ScrollView只能允许子View的高度超过自己,不允许子View的宽度超过自己。所以,作为ViewGroup,可以在不超过availableWidth的情况下,将子View layout 到任意的高度上。如下图所示:

看到没?作为一个优秀的ViewGroup,当你layout自己的子View时,只要保证子View在availableWidth之内,即使超过了parent要求的高度也没有关系,开发者还是愿意使用你的,因为他们可以为你指定ScrollView作为parent。

这就是我们看到许多的ViewGroup在layout子View时,宁超高度,不超宽度的原因。

至此,你应该明白,上文中我们提出的,对于parent指定的availableWidth和availableHeight,作为ViewGroup还是要尽量不超过parent限定的区域,如果一定要超过的话,那就超availableHeight,而不要超availableWidth

了解一下layout_gravity

我们看到,Android系统提供的FrameLayout、LinearLayout等都支持子View设定layout_gravity,它到底是干什么用的?我们自己自定义ViewGroup时能不能也用上它?

关于它的作用,一句话就能说明白,当ViewGroup给子View分配的空间超过子View要求的大小时,就需要gravity帮助ViewGroup为子View精确定位。可见,layout_gravity就是ViewGroup在layout阶段,协助ViewGroup给它的子View确定位置的,没错,就是协助确定子View的 left,top,bottom,right四个值。

下面,我们以FrameLayout为例来进行说明。假设FrameLayout中有一个子View,这个子View的所要求的展示尺寸(measuredWidth,measuredHeight)小于FrameLayout的尺寸,但是FrameLayout是个实心眼,它不管子View要求多大,都会把它所有的屏幕区域给子View,这样就可以保证,用户在这个区域中的交互动作,都是与子View的交互。那么问题来了,FrameLayout在layout子View时,总不能让它的left和top为0,right和bottom等于自己的宽和高吧。如果这么干,子View就要在这个尺寸下,绘制自己,就不可避免地要对它包含的drawables进行拉伸,展示效果必然受到影响,那怎么办?

FrameLayout会提取子View的 LayoutParams中的gravity,看看子View想在哪个位置,假设子View的layout_gravity的值是"top|left",那么FrameLayout就会把子View layout到自己的左上角,大小嘛就是子View所要求的大小。但是请注意,虽然此时子View绘制时是按照自己要求的大小绘制的,但是,能与它发生交互的区域却是整个FrameLayout所占的屏幕区域。

所以,要不要使用layout_gravity,就看你自定义的ViewGroup是不是给子View分配大于它们要求的空间。

下面我就举一个简单的例子来说明。

假设ViewGroup现在要layout一个子View,如下是该子View要求的尺寸大小:

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
复制代码

现在,ViewGroup要给这个子View设定位置和大小了。设定的位置和大小用如下四个参数表示:

bigLeft,bigTop,bigRight,bigBottom。
复制代码

这四个值在ViewGroup的以左上角为原点,向右x,向下y的坐标系中构成了一个矩形。如下:

Rect bigRect = new Rect( bigLeft, bigTop, bigRight, bigBottom);
复制代码

进一步假设这个bigRect的宽高大于子View要求的宽高(是为了更明显地说明layout_gravity的作用,实际情况可能不是这样的),如下图所示:

现在ViewGroup准备把bigRect区域全部分给子View,但是ViewGroup显然不能直接这样layout 子View:

child.layout(bigLeft,bigTop,bigRight,bigBottom);
复制代码

这样的话,child就要在bigRect区域内绘制自己,不可避免地要拉伸自己,导致展示的效果变差(想像一下1010的图片扩成100100是什么效果)。所以,我们需要在bigRect内进一步为子View定位,怎么定位?

  • 第一步就是读出子View的LayoutParams对象中的layout_gravity值。如下:
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int child_layout_gravity = lp.gravity;
复制代码

从上面代码可以看出,layout_gravity最终是以整数的形式存放于子View的LayoutParams中的。

  • 第二步就是构建一个空的Rect,准备接收为子View定位后的四个坐标值,如下:
Rect smallRect = new Rect();
复制代码
  • 第三步就是见证奇迹的时刻,如下:
Gravity.apply(child_layout_gravity, childWidth, childHeight, bigRect, smallRect);
复制代码

经过上面的调用,Gravity会在smallRect中存入依据子View的layout_gravity以及子View要求的尺寸,在bigRect中为子View精确定位后的坐标值,注意这个坐标值所在的坐标系还是ViewGroup的坐标系。所以,我们现在可以愉快地layout子View了。

child.layout(smallRect.left, smallRect.top, smallRect.right, smallRect.bottom);
复制代码

示例

自定义一个ViewGroup,名为CustomLayout,效果如下:

代码如下,注释的很清晰:

public class CustomLayout extends ViewGroup {public CustomLayout(Context context) {this(context, null);}public CustomLayout(Context context, AttributeSet attrs) {this(context, attrs, 0);}public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {this(context, attrs, defStyleAttr, 0);}@TargetApi(21)public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, 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();final int widthSize = MeasureSpec.getSize(widthMeasureSpec);// 遍历我们的子View,并测量它们,根据它们要求的尺寸// 进而计算我们的StaggerLayout需要的尺寸。for (int i = 0; i < count; i++) {final View child = getChildAt(i);//可见性为gone的子View,我们就当它不存在。if (child.getVisibility() == GONE) {continue;}// 测量该子ViewmeasureChild(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));}@Overrideprotected 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,在不断变化。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) {continue;}int curWidth, curHeight;curWidth = child.getMeasuredWidth();curHeight = child.getMeasuredHeight();//用来判断是否应当将该子View放到下一行if (curLeft + curWidth >= childRight) {/*需要移到下一行时,更新curLeft和curTop的值,使它们指向下一行的起点同时将maxHeight清零。*/curLeft = childLeft;curTop += maxHeight;maxHeight = 0;}//所有的努力只为了这一次layoutchild.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);//更新maxHeight和curLeftif (maxHeight < curHeight) {maxHeight = curHeight;}curLeft += curWidth;}}
}
复制代码

目录结构

  • Android 自定义View基础(一)
  • Android自定义View:View(二)
  • Android 自定义View:处理事件分发(四)
  • Android 自定义View:属性动画(六)
  • Android 自定义View:深入理解自定义属性(七)

参考文章

milter:教你步步为营掌握自定义ViewGroup

Android自定义View:ViewGroup(三)相关推荐

  1. Android自定义View(三)自定义属性AttributeSet

    自定义View的时候通常需要提供一些自定义属性,自定义属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,然后在该文件中定义相应的属性,并在自定义View ...

  2. android画布设置最外层,Android自定义View高级(三)-Canvas之画布操作

    一.Canvas简介 Canvas我们可以称之为画布,能够在上面绘制各种东西,是Android平台2D图形绘制的基础. 二.Canvas的常用操作 操作类型 相关API 备注 绘制颜色 drawCol ...

  3. android自定义view(三)绘制表格和坐标系

    真的非常简单 运行效果截图: 说说几个常用的方法吧 画点 canvas.drawPoint(200, 200, mPaint);     //在坐标(200,200)位置绘制一个点 canvas.dr ...

  4. Android 自定义View

    [Android 自定义View] Android 自定义View 自定义View基础 自定义TextView 继承View重写onDraw方法 View的构造方法 自定义属性 创建attrsxml文 ...

  5. Android 自定义View(四)实现股票自选列表滑动效果

    一.前言 Android 开发过程中自定义 View 真的是无处不在,随随便便一个 UI 效果,都会用到自定义 View.前面三篇文章已经讲过自定义 View 的一些案例效果,相关类和 API,还有事 ...

  6. Android自定义View精品(LimitScrollerView-仿天猫广告栏上下滚动效果)

    版权声明:本文为openXu原创文章[openXu的博客],未经博主允许不得以任何形式转载 文章目录 1.分析 2.定义组合控件布局 3.继承最外层控件 4.自定义属性 5.重写onMeasure 6 ...

  7. android自定义起止时间的时间刻度尺,Android 自定义View篇(六)实现时钟表盘效果...

    前言 Android 自定义 View 是高级进阶不可或缺的内容,日常工作中,经常会遇到产品.UI 设计出花里胡哨的界面.当系统自带的控件不能满足开发需求时,就只能自己动手撸一个效果. 本文就带自定义 ...

  8. android自定义View之(六)------高仿华为荣耀3C的圆形刻度比例图(ShowPercentView)

    为什么写这篇文章: 显示当前的容量所占的比例,表现当前计划的进度,一般都会采用百分比的方式,而图形显示,以其一目了然的直观性和赏心悦目的美观形成为了我们的当然的首选. 在图形表示百分比的方法中,我们有 ...

  9. Android自定义View(一) - View与ViewGroup

    目录 1.View和ViewGroup关系 2.坐标系 2.1.Android坐标系 2.2.View坐标系 3.下一节介绍View的滑动 Android体统提供了很多控件用于展示以及和用户交互,比如 ...

最新文章

  1. win10安装JDK cmd中可以运行java,但不能用javac,解决方案
  2. 阅读【现代网络技术 SDN/NFV/QOE 物联网和云计算】 第一章
  3. jmeter中timeout值怎么设置_jMeter解决failed to respond Connection reset
  4. Module System of Swift (简析 Swift 的模块系统)
  5. 和一个刚毕业不久的朋友聊天
  6. cupload怎么保存图片_原生js的图片上传插件cupload
  7. IBM并购网络视频会议商WebDialogs 加入Lotus Sametime
  8. python通信模块_基于Python的电路故障诊断系统通信模块的实现
  9. 格雷码与二进制转换电路设计
  10. 笔记本查看WIFI密码
  11. 欢迎使用CSDN-markdown编辑器萨达所大所大所大所
  12. 人生最好的作息时间表
  13. 抗饱和积分器 matlab,抗积分饱和
  14. Django新手入门教程(2)测试服务器是否可用
  15. 百家号如何提高推荐量和阅读量,百家号提高推荐量和阅读量的方法
  16. 吴恩达深度学习课程值不值得学?四晚学完的高手给你建议
  17. EF系列(一)——深入框架底层
  18. (向量空间)概念和法则的人为定义 I
  19. 嵌入式linux之Uboot和系统移植--基础
  20. 7-7 六度空间 (30分) 【最短路径(Floyd)】

热门文章

  1. python 单元测试setup执行了多次_python单元测试setUp与tearDown
  2. android 自动打开第三方应用程序,Android如何做到应用程序图标隐藏,由第三方程序显示启动...
  3. java exchange发邮件_java发送exchange邮件问题
  4. Unity3d创建注册登录页面(1)
  5. [VSCode]中英文界面切换
  6. LeetCode 198. House Robber--动态规划--C++,Java,Python解法
  7. Thread类中yield方法
  8. php 操作mssql,php操作mssql
  9. 网件r4500刷第三方固件_网件R6800刷PandoraBox固件,激发潜在的160MHz频宽
  10. MySQL中exists与in的使用