自定义ViewGroup本质上就干一件事——layout
。bash
咱们知道ViewGroup是一个组合View,它与普通的基本View(只要不是ViewGroup,都是基本View)最大的区别在于,它能够容纳其余View,这些View既能够是基本View,也能够ViewGroup,可是在咱们的ViewGroup眼中,不论是View仍是ViewGroup,它们都抽象成了一个普通的View,ViewGroup的最最根本的职责就是,在本身内部,给它们每个人找一个合适的位置
,也就是调用它们的以下方法:app
public void layout(int left, int top, int right, int bottom)
复制代码
如图所示:ide
这个方法,既肯定了子View的位置,也肯定了子View的大小
,请注意,这个大小是由咱们的ViewGroup最后决定的分给该子View的屏幕区域大小
。post
通常状况下,ViewGroup在设定这个大小时,会考虑子View的自身要求的,也就是它们measured的大小(getMeasuredWidth , getMeasuredHeight),一般最后给每一个子View设定的大小就是它们所要求的大小,但这不是绝对的。动画
假若有一个二愣子性格的ViewGroup,它宣称:“我全部的子View的大小都必须是30*30的尺寸!”,这种SB的ViewGroup在调用每一个子View的layout方法时,经过让bottom-top=right-left=30,就把全部的子View最后占据的屏幕区域设定为30*30了,无论各个子View所要求的大小是多少,此时都没有任何用处了。ui
固然,除了有特殊需求,我相信没人愿意用这种ViewGroup的,这里咱们能够知道,咱们自定义ViewGroup,大致上有两条路可选:this
**那么使用者使用一个ViewGroup最基本的指望是什么?**我想,应该是使用者放入这个ViewGroup中的子View,layout出来的尺寸和每一个子View measured的尺寸相符。只有这样,才能确保使用者的每一个子View顺利完成本身的交互任务。spa
对于上面的图,有两点很是容易让人产生误解,须要解释一下:code
关于left、right、top、bottom。它们都是坐标值,既然是坐标值,就要明确坐标系,这个坐标系是什么?咱们知道,这些值都是ViewGroup设定的,那么,这个坐标系天然也是由ViewGroup决定的了。这个坐标系就是以ViewGroup左上角为原点,向右x,向下y构建起来的。
cdn
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
。咱们通常将这个宽高称为 availableWidth
和 availableHeight
(请记住这两个值,下面还要用到),它们表示的是咱们的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方法中,得到了parent给本身设定的尺寸大小,即 availableWidth
和 availableHeight
,这个值至关于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
。
咱们看到,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);
}
@Override
protected 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;
}
// 测量该子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,在不断变化。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;
}
//全部的努力只为了这一次layout
child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
//更新maxHeight和curLeft
if (maxHeight < curHeight) {
maxHeight = curHeight;
}
curLeft += curWidth;
}
}
}
复制代码