本篇继续ViewGroup系列来讲述自定义ViewGroup的流程。本篇要讲述的是流式布局。
在Java Swing中,有一种布局,叫流式布局,这种布局的特点是子组件按照从左到右,从上到下的顺序依次排序,如果一行放不下了,自动显示到下一行。Android中并没有提供这样的布局,本节,我们将一起来实现这种流式布局FlowViewGroup。
对于FlowViewGroup来说,有两个难点:
1. 计算子组件的排布后的宽度和高度
FlowViewGroup的宽度为wrap_content时计算逻辑如下 measureWidth:
- 当子view的宽度和 > FlowViewGroup的宽度时 —> FlowViewGroup的宽度不变。
- 当子view的宽度和 < FlowViewGroup的宽度时 —> FlowViewGroup的宽度为子组件宽度之和。
FlowViewGroup的高度为wrap_content时计算逻辑如下 measureHeight:
- 遍历子组件,当前子view与本行已经排布的子view之和 是否超过FlowViewGroup的宽度,未超过,不换行,将此子view放到本行;否则就需要换行了,更新FlowViewGroup的高度
2. 对子组件进行定位,要根据当前行的宽度、已占据的宽度来计算当前子组件是否需要换行显示 onLayout。
是否换行就会影响子组件的left和top值。此逻辑与计算FlowViewGroup的宽高思路有相近之处。
测量FlowViewGroup时,不允许子组件的宽度比容器还大。
下面贴出FlowViewGroup的这个代码,请读者关注measureWidth、measureHeight和onLayout方法,关键代码都添加了注释。
/**
* Created by cyy on 2020/7/30.
* 流式布局
*/
public class FlowViewGroup extends ViewGroup {
public FlowViewGroup(Context context) {
super(context);
}
public FlowViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
getContext();
int n = getChildCount();
int maxViewHeight = 0;//当前行子组件的最大高度
int maxLineWidth = 0;//当前行子组件的总宽度
int totalHeight = 0;//累加总高度
int width = getMeasuredWidth();//容器宽度
for(int i = 0 ; i < n ; i++){//循环遍历子view
View child = getChildAt(i);
if(maxLineWidth + getChildAt(i).getMeasuredWidth() > width - getPaddingStart() - getPaddingEnd()){
//加上此view后,超过行的总宽度,要换行
totalHeight += maxViewHeight;//要换行,totalHeight 加上当前行的最大高度
maxLineWidth = 0;//要换行,当前子view就是下一行的第一个view,并且还未layout上去,因此清空maxLineWidth 值
maxViewHeight = 0;//要换行,当前子view就是下一行的第一个view,并且还未layout上去,因此maxViewHeight 值置为0
}
//定位当前子view
layoutChild(child,maxLineWidth,totalHeight,maxLineWidth + child.getMeasuredWidth(), totalHeight + child.getMeasuredHeight());
//当前子view已经layout了,更新当前行maxViewHeight 值(无论是否换行,都是此逻辑。如果换行了,maxViewHeight 即为当前子view的高度;如果未换行,就拿当前子view的高度与之前最大的view高度相比 去最大值)
maxViewHeight = Math.max(maxViewHeight, child.getMeasuredHeight());
//更新maxLineWidth 值(无论是否换行,都是此逻辑。如果换行了,maxLineWidth 被清为0了,下一行只有当前子view,maxLineWidth 就是当前子view的宽度;如果不换行了,maxLineWidth 就是累当前子view的宽度)
maxLineWidth += child.getMeasuredWidth();
}
}
private void layoutChild(View child,int l,int t,int r,int b){
child.layout(l + getPaddingStart(),t + getPaddingTop(), r + getPaddingStart(), b + getPaddingTop());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);//先测量子view
setMeasuredDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));//在测量自身
}
private int measureWidth(int widthMeasureSpec){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
Log.e("cyy","measureWidth mode:"+mode+" size:"+size);
int width = 0;
if(mode == MeasureSpec.EXACTLY){ //FlowViewGroup是固定宽度,无需计算
width = size;
}else if(mode == MeasureSpec.AT_MOST){ //FlowViewGroup是wrap_content,需计算其宽度
int n = getChildCount();
int childTotalWidth = 0;
for(int i = 0; i < n ; i++){//循环遍历子view
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
if(childWidth > size){
throw new IllegalArgumentException("Sub view is too large");
}
childTotalWidth += childWidth;//遍历并累加所有子view的宽度和
}
if(childTotalWidth > size){
//所有子view的宽度和 > FlowViewGroup的宽度,一行显示不下,FlowViewGroup即为wrap_content下传来的最大宽度
width = size;
}else{
//所有子view的宽度和 < FlowViewGroup的宽度,一行显示得下,FlowViewGroup即为子view的宽度之和
width = childTotalWidth;
}
}
return width;
}
private int measureHeight(int heightMeasureSpec){
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if(mode == MeasureSpec.EXACTLY){
height = size;
}else if(mode == MeasureSpec.AT_MOST){
int width = getMeasuredWidth();
int n = getChildCount();
int maxViewHeight = 0;//当前行子view的最大高度
int currentLineWidth = 0;//当前行子view的总宽度
for(int i = 0; i < n ; i++){//循环遍历子view
TextView child = (TextView) getChildAt(i);
currentLineWidth += child.getMeasuredWidth();//当前行的子view的宽度和 + 待排布的子view宽度
if(currentLineWidth > width){
//currentLineWidth 超过了FlowViewGroup的宽度了,表示要换行,当前view需要在下一行显示
Log.e("cyy","换行:"+child.getText().toString());
height += maxViewHeight;//FlowVIewGroup的高度更新,加上前一行的最大高度。
maxViewHeight = child.getMeasuredHeight();//换行了,当前view是下一行的第一个view,下一行的最大高度先假定是此view的高度
currentLineWidth = child.getMeasuredWidth();//换行了,下一行只有一个view,就是此view,那么下一行的子view的总宽度就是当前view的宽度
}else{
//不换行 ,宽度已经加过了,主要处理高度
Log.e("cyy","不换行:"+child.getText().toString());
maxViewHeight = Math.max(maxViewHeight,child.getMeasuredHeight());//当前行的最大高度更新
}
if(i == (n -1)){//对最后一个子view需要特殊处理。
height += maxViewHeight;//无论最后一个子view是否换行,都需要将maxViewHeight的值加到FlowViewGroup的高度上去。
}
}
}
height += getPaddingTop() + getPaddingBottom();//经过遍历得到的FlowViewGroup的高度,再加上padding值
Log.e("cyy","height:"+height);
return height;
}
DONE
此系列后续持续更新。