本节内容
3.三种计算父容器与子控件的情况
2.预备知识
3.关于布局的小demo(父容器尺寸确定,计算子控件尺寸)
一、三种需要计算的情况
1.父容器尺寸确定,需要根据父容器的尺寸确定子控件的尺寸
-
onMeasure中获取父容器尺寸
-
计算子控件的尺寸
-
构建MeasureSpec对象用于测量子控件时限制子控件
-
使用measure方法测量子控件
-
onLayout中调用视图的layout方法布局子控件
2.子控件尺寸确定,需要确定父容器的尺寸
-
onMeasure中先测量父容器获取限制measurespec
-
测量子控件,获取尺寸
-
按照规则计算父容器的宽高
-
设置父容器的宽高尺寸
-
onLayout中对子控件进行布局
3.子控件和父容器尺寸都不确定,先测量子控件,再确定父容器尺寸
-
onMeasure中先测量父容器获取限制measurespec
-
自定义view中通过onMeasure测量子view的尺寸
-
按照规则计算父容器的宽高
-
设置父容器的宽高尺寸
-
onLayout中调用视图的layout方法布局子控件、
二、预备知识
1.为什么要自定义ViewGroup
-
a.将多个子控件组合起来 形成一个完整体
-
b.系统已有的布局方式满足不了需求,所以需要自定义ViewGroup。FrameLayout、 LinearLayout、 RelativeLayout、 ConstraintLayout以上都是继承于ViewGroup的
2.如何自定义ViewGroup :View
-
a.在已有的容器上添加自己的功能(最简单),MyViewGroup :ConstraintLayout ->ViewGroup
-
b.如果自己想定义规则(复杂 灵活)MyViewGroup:ViewGroup
3.ViewGroup里面有一个重要的参数,MeasureSpec,它有几个值
-
EXACTLY:精确的
-
AT_MOST : 最多不能超过某个值 match_parent -> 填充父容器的剩余控件
-
UNSPECIFIED:无限,不做任何限制(很少用)
4.在这里面还有几个重要的方法
-
getMode:记录的是上面三个值中的一个。
-
getSize:记录的是具体的尺寸。
三、父容器尺寸确定
1.当我们把其他的View或ViewGroup加到ViewGroup里面之前,ViewGroup需要
-
①先粗略地估计一下自身大小,也就是拿到容器本身的限制。
-
②之后再测量每个子控件的尺寸。
-
③定规则。
-
④计算当前容器的最终尺寸。
-
⑤摆放(布局)控件。
2.首先ViewGroup通过onMeasure方法测量自身,得到了自己的MeasureSpec,它里面就有mode和size。子View也通过onMeasure方法计算自己的尺寸,然后就可以确定自己的MeasureHeight和MeasureWidth。把这个数据返回给父容器,父容器就可以计算自己的尺寸并保存这个尺寸。通过setMeasureDimension方法来保存。
3.确定好了尺寸之后,就要布局子控件,通过onLayout方法来布局。它告诉子控件自己的左上右下在哪里,子控件就可以确定自己的位置。
4.所以在ViewGroup里面有两个方法很重要,就是onMeasure和onLayout。但是对于子View,只有onMeasure方法。
5.对于父容器来说,如果只有一个子view,那么这个子view的width = pw(parentwidth)-2sapce,height = ph(parentheight)-2space。
-
如果有两个子view,那么width = (pw-3space)/2,height = ph - 2space
-
如果有三个子view,那么width =(pw-3space)/2,height =(ph -3space)/2,四个的话也是一样的。
四、demo简介
1.前面展示的图片就是我们要今天要做的demo,我们可以随意的添加任意个子view,然后父容器会根据相应的规则来对这些子view进行布局。也就是父容器尺寸确定,需要计算子控件尺寸。
五、demo实现
1.先新建一个view,继承自viewGroup,并实现相应的构造方法,还有必须实现的onLayout方法和onMeasure方法。
-
在onLayout里面主要是按照规则对自己的子控件进行布局。
-
onMeasure:主要是用于测量子view,确定自己的最终尺寸。
class MyViewGroup:ViewGroup {
constructor(context:Context,attrs:AttributeSet?):super(context, attrs){}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {}
}
2.在activity.xml中,如果把view添加到我们定义的类里面,相当于就是它的子控件了。
3.通过getChildAt(i)方法可以获取i对应的控件,通过getChildCount能够获取容器所有子控件的个数。在onLayout方法里面可以获取一个子控件,并对其进行布局。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val child = getChildAt(0)
child.layout(50,50,300,400)
}
4.在onMeasure方法里面,先与测量一下自己的限制尺寸,再获取子控件的尺寸其中measureChild方法里面的后两个参数表示最大的限制。(这里假设只有一个控件)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//先预测量一下自己的限制尺寸 size mode
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//获取预测量之后自己的宽和高
val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
val parentHeight =MeasureSpec.getSize(heightMeasureSpec)
//计算子控件的尺寸
val childWidth = parentWidth-2*space
val childHeight = parentHeight - 2*space
//将尺寸设置给子控件
val child = getChildAt(0)
//先确定限制条件 MeasureSpec
val wspec = MeasureSpec.makeMeasureSpec(childWidth,MeasureSpec.EXACTLY)
val hspec = MeasureSpec.makeMeasureSpec(childHeight,MeasureSpec.EXACTLY)
child.measure(wspec,hspec)
}
}
}
5.设定一下每个控件之间的间距,定义在最外部
private val space=30 //间距
6.在onLayout方法里面重新布局一下,最后效果图如下图所示
val child = getChildAt(0)
var left = space
var top= space
var right= space + child.measureWidth
var bottom= space + child.measureHeight
child.layout(left,top, right, bottom)
六、确定多个子控件的尺寸和位置
1.因为确定孩子的宽高尺寸有多种情况,所以不能直接写死,我们把childWidth和childHeight设为变量。
var childWidth = 0
var childHeight = 0
2.当只有一个孩子的时候,宽度和高度都好算。当有多个孩子的时候,宽度就为parentWidth - 2*space,但是高度与行有关,所以我们要先计算行数。然后找到规律,得到计算高度的公式。
-
只有一行时,高度 = parentHeight - 2*space
-
有两行时,高度 = (parentHeight - 3*space)/2
-
有三行时,高度= (parentHeight - 4*space)/3
-
这样我们就可以得到结论:高度 = (parentHeight-(row+1)*space)/row
if (childCount==1){
childWidth = parentWidth - 2*space
childHeight = parentHeight - 2*space
}else{
childWidth = (parentWidth - 3*space)/2
//计算有多少行
val row = (childCount+1)/2
childHeight = (parentHeight-(row+1)*space)/row
}
3.有多个控件的话,那么绘制孩子的尺寸就不能用getChild(0)了,这个时候要用for循环。因为确定限制条件那个都一样,所以放到循环外面来。
//将尺寸设置给子控件
//先确定限制条件 MeasureSpec
val wspec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY)
val hspec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(wspec, hspec)
}
4.测量完毕之后开始布局,也不能像之前一样写死,所以先把左上右下都赋值为0。
var left = 0
var top=0
var right= 0
var bottom= 0
5.然后在循环里面,确定每一个孩子的左上右下。
-
如果只有一个孩子,left为space,如果有两个孩子,left为2space+childWidth,第三个孩子又为space,第四个又为2space+childWidth。
-
只要确定孩子在第几行第几列,就很好计算这四个参数,所以我们先确定孩子的行和列。
for(i in 0 until childCount){
val child = getChildAt(i)
val row = i/2
val column = i%2
left = space+column*(child.measuredWidth+space)
top = space+row*(child.measuredHeight+space)
right= left+child.measuredWidth
bottom = top+child.measuredHeight
//告诉子容器放在哪个位置,对子控件进行布局
child.layout(left,top, right, bottom)
}
这样,我们在activity_xml随意添加多少个view,都可以按照我们想要的样子来布局了,比如下图的五个孩子。
MyViewGroup类的完整代码如下图所示:
class MyViewGroup:ViewGroup {
private val space=30 //间距
constructor(context:Context,attrs:AttributeSet?):super(context, attrs){}
//测量子View 确定自己的最终尺寸
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//先预测量一下自己的限制尺寸 size mode
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//获取预测量之后自己的宽和高
val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
val parentHeight =MeasureSpec.getSize(heightMeasureSpec)
//计算子控件的宽和高
var childWidth = 0
var childHeight = 0
if (childCount==1){
childWidth = parentWidth - 2*space
childHeight = parentHeight - 2*space
}else{
childWidth = (parentWidth - 3*space)/2
//计算有多少行
val row = (childCount+1)/2
childHeight = (parentHeight-(row+1)*space)/row
}
//将尺寸设置给子控件
//先确定限制条件 MeasureSpec
val wspec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY)
val hspec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(wspec, hspec)
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//获取某一个子控件 因为目前只加了一个控件,所以0就是代表我们的控件
//对子控件进行布局
var left = 0
var top=0
var right= 0
var bottom= 0
for(i in 0 until childCount){
val child = getChildAt(i)
val row = i/2
val column = i%2
left = space+column*(child.measuredWidth+space)
top = space+row*(child.measuredHeight+space)
right= left+child.measuredWidth
bottom = top+child.measuredHeight
//告诉子容器放在哪个位置,对子控件进行布局
child.layout(left,top, right, bottom)
}
}
}