ViewGroup的简略流式布局
前言
前面几篇咱们简略的温习了一下自界说 View 的丈量与制作,而且回忆了常见的一些事情的处理办法。
那么假如咱们想自界说 ViewGroup 的话,它和自界说View又有什么区别呢?其实咱们把 ViewGroup 当做 View 来用的话也不是不能够。可是已然咱们用到了容器 ViewGroup 其时是想用它的一些特殊的特性了。
比方 ViewGroup 的丈量,ViewGroup的布局,ViewGroup的制作。
- ViewGroup的丈量:与 View 的丈量不同,ViewGroup 的丈量会遍历子 View ,获取子 View 的巨细,从而决议自己的巨细。当然咱们也能够经过指定的形式来指定自身的巨细。
- ViewGroup的布局:这个是 ViewGroup 中心与常用的功能。找到对于的子View 布局到指定的方位。
- ViewGroup的制作:一般咱们不会重写这个办法,由于一般来说它本身不需求制作,而且当咱们没有设置ViewGroup的背景的时分,onDraw()办法都不会被调用,一般来说 ViewGroup 仅仅会运用 dispatchDraw()办法来制作其子View,其过程同样是经过遍历一切子View,并调用子View的制作办法来完结制作作业。
下面咱们一同温习一下ViewGroup的丈量布局办法。咱们以入门级的 FlowLayout 为例,看看流式布局是如何丈量与布局的。
话不多说,Let’s go
一、基本的丈量与布局
咱们先回忆一下ViewGroup的
一个经典的ViewGroup丈量是怎样完结?一般来说,最简略的丈量如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for(int i = 0; i < getChildCount(); i++){
View childView = getChildAt(i);
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
或许咱们直接运用封装之后的默许办法
measureChildren(widthMeasureSpec,heightMeasureSpec);
其内部也是遍历子View来完结的。当然假如有自界说的一些宽高丈量规矩,就不能运用这个办法,就需求自己遍历找到View自界说完结了。
需求留意的是,这儿咱们丈量子布局传递的 widthMeasureSpec 和 heightMeasureSpec 是父布局的丈量形式。
当父布局设置为固定宽度的时分,子View是不能超过这个宽度的,比方父控件设置为match_parent,自界说View无论是match_parent 仍是 wrap_content 都是相同的,充满整个父控件。
相当于父布局调用子控件的onMeasure办法的时分告知子控件,我就这么大,你看着办,不能超过它。
而父布局传递的是自适应AT_MOST形式,那么便是由子View来决议父布局的宽高。
相当于父布局调用子控件的onMeasure办法的时分问子控件,我也不知道我多大,你需求多大的方位?我又需求多大的当地才能包容你?
其实也很好了解。那么一个经典的ViewGroup布局又是怎样完结?重写 onLayout 而且遍历拿到每一个View,进行Layout操作。
比方如下的代码,咱们每一个View的高度设置为固定高度,而且笔直排列,相似一个ListView 的布局:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
//设置子View的高度
MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
params.height = mFixedHeight * childCount;
setLayoutParams(params);
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
child.layout(l, i * mFixedHeight, r, (i + 1) * mFixedHeight);
}
}
}
留意咱们 onLayout() 的参数
展现的作用便是这样:
二、流式的布局的layout
首要咱们先不管丈量,咱们先指定ViewGroup的宽高为固定宽高,指定为match_parent。咱们先做布局的操作:
咱们自界说 ViewGroup 中重写丈量与布局的办法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec,heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* @param changed 当时ViewGroup的尺度或许方位是否发生了改动
* @param l,t,r,b 当时ViewGroup相对于父控件的坐标方位,
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewGroupWidth = getMeasuredWidth(); //当时ViewGroup的总宽度
int layoutChildViewCurX = l; //当时制作View的X坐标
int layoutChildViewCurY = t; //当时制作View的Y坐标
int childCount = getChildCount(); //子控件的数量
//遍历一切子控件,并在其方位上制作子控件
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//子控件的宽和高
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
//假如剩下控件不行,则移到下一行开端方位
if (layoutChildViewCurX + width > mViewGroupWidth) {
layoutChildViewCurX = l;
//假如换行,则需求修正当时制作的高度方位
layoutChildViewCurY += height;
}
//执行childView的布局与制作(右和下的方位加上自身的宽高即可)
childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + height);
//布局完结之后,下一次制作的X坐标需求加上宽度
layoutChildViewCurX += width;
}
}
终究咱们就能得到对应的换行作用,如下:
经过上面咱们的基础学习,咱们应该能了解这样的布局办法,跟上面的基础布局办法比较,便是多了一个 layoutChildViewCurX 和 layoutChildViewCurY 。关于其它的逻辑这儿已经注释的非常清楚了。
可是这样的作用好丑,咱们加上距离 margin 试试?
并没有作用,其实是内部 View 的 LayoutParams 就不支撑 margin,咱们需求界说一个内部类承继 ViewGroup.MarginLayoutParams,并重写generateLayoutParams() 办法。
//要使子控件的margin特点有效必须承继此LayoutParams,内部还能够定制一些其他特点
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams layoutParams) {
super(layoutParams);
}
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ViewGroup2.LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
然后修正一下代码,在 layout 子布局的时分咱们手动的把 margin 加上。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewGroupWidth = getMeasuredWidth(); //当时ViewGroup的总宽度
int layoutChildViewCurX = l; //当时制作View的X坐标
int layoutChildViewCurY = t; //当时制作View的Y坐标
int childCount = getChildCount(); //子控件的数量
//遍历一切子控件,并在其方位上制作子控件
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//子控件的宽和高
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
//假如剩下控件不行,则移到下一行开端方位
if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > mViewGroupWidth) {
layoutChildViewCurX = l;
//假如换行,则需求修正当时制作的高度方位
layoutChildViewCurY += height + lp.topMargin + lp.bottomMargin;
}
//执行childView的布局与制作(右和下的方位加上自身的宽高即可)
childView.layout(
layoutChildViewCurX + lp.leftMargin,
layoutChildViewCurY + lp.topMargin,
layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin,
layoutChildViewCurY + height + lp.topMargin + lp.bottomMargin);
//布局完结之后,下一次制作的X坐标需求加上宽度
layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
}
}
此刻的作用就能收效了:
三、流式的布局的Measure
前面的设置咱们都是运用的宽高 match_parent。那咱们修正 ViewGroup 的高度为 wrap_content ,能完结高度自适应吗?
这…并不是咱们想要的作用。并没有自适应高度。由于咱们没有写丈量的逻辑。
咱们想一下,假如咱们的宽度是固定的,想要高度自适应,那么咱们就需求丈量每一个子View的高度,计算出对应的高度,当换行之后咱们再加上行的高度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft();
final int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
final int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - this.getPaddingTop() - this.getPaddingBottom();
final int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.EXACTLY) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} else if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.AT_MOST) {
int layoutChildViewCurX = this.getPaddingLeft();
int totalControlHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
final View childView = this.getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
childView.measure(
getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (totalControlHeight == 0) {
totalControlHeight = height + lp.topMargin + lp.bottomMargin;
}
//假如剩下控件不行,则移到下一行开端方位
if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
layoutChildViewCurX = this.getPaddingLeft();
totalControlHeight += height + lp.topMargin + lp.bottomMargin;
}
layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
}
//终究确认整个布局的高度和宽度
int cachedTotalWith = resolveSize(sizeWidth, widthMeasureSpec);
int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);
this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);
}
宽度固定和高度自适应的情况下,咱们是这么处理的。计算出子View的总高度,然后设置 setMeasuredDimension 为ViewGroup的丈量宽度和子View的总高度。即为终究 ViewGroup 的宽高。
这样咱们就能完结高度的自适应了。那么宽度能不能自适应呢?
当然能够,咱们只需求记载每一行的宽度,然后终究 setMeasuredDimension 的时分传入一切行中的最大宽度,便是 ViewGroup 的终究宽度,而高度的计算是和上面的办法相同的。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
else if (modeWidth == MeasureSpec.AT_MOST && modeHeight == MeasureSpec.AT_MOST) {
//假如宽高都是Wrap-Content
int layoutChildViewCurX = this.getPaddingLeft();
//总宽度和总高度
int totalControlWidth = 0;
int totalControlHeight = 0;
//由于宽度对错固定的,所以用一个List接收每一行的最大宽度
List<Integer> lineLenghts = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
final View childView = this.getChildAt(i);
if (childView.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) childView.getLayoutParams();
childView.measure(
getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width),
getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height)
);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (totalControlHeight == 0) {
totalControlHeight = height + lp.topMargin + lp.bottomMargin;
}
//假如剩下控件不行,则移到下一行开端方位
if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) {
lineLenghts.add(layoutChildViewCurX);
layoutChildViewCurX = this.getPaddingLeft();
totalControlHeight += height + lp.topMargin + lp.bottomMargin;
}
layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin;
}
//计算每一行的宽度,选出最大值
YYLogUtils.w("每一行的宽度 :" + lineLenghts.toString());
totalControlWidth = Collections.max(lineLenghts);
YYLogUtils.w("选出最大宽度 :" + totalControlWidth);
//终究确认整个布局的高度和宽度
int cachedTotalWith = resolveSize(totalControlWidth, widthMeasureSpec);
int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec);
this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight);
}
}
为了作用,咱们把榜首行的终究一个View宽度多一点,便利查看作用。
这样就能够得到ViewGroup自适应的宽度和高度了。并不杂乱对不对!
后记
这样是不是就能完结一个简略的流式布局了呢?当然这些仅仅为便利学习和了解,真实的实战中并不推荐直接这样运用,由于内部还有一些兼容的逻辑没处理,一些逻辑没有封装,特点没有抽取。甚至连每一个View的高度,和每一行的最大高度也没有处理,其实这样健壮性并不好。
假如我们想要在项目中运用流式布局,那么我仍是推荐运用鸿洋的流式布局【传送门】。
或许运用谷歌官方的流式布局 FlexboxLayout 【传送门】
丈量与布局是 ViewGroup 的基本功了,把握了流式布局之后,咱们对其他的一些 Viewgroup 布局就能快速下手了。
关于本文的内容假如想查看源码能够点击这儿 【传送门】。你也能够重视我的这个Kotlin项目,我有时间都会持续更新。
惯例,我如有解说不到位或错漏的当地,希望同学们能够指出沟通。
假如感觉本文对你有一点点的启发,还望你能点赞
支撑一下,你的支撑是我最大的动力。
Ok,这一期就此完结。
本文正在参与「金石方案 . 分割6万现金大奖」