简介
简单来说,流布局就是按照指定的对齐方式,将所有子view根据加入顺序依次排列,一行放不下则转入下一行。这种排列方式常见于各种标签栏、吐槽版的设计中。
上图是本文实现的一个简单流布局,支持以下功能:
- 支持左对齐、居中对齐、右对齐三种全局对齐方式
- 子view支持居于上方、居于中间、居于下方三种位置选择
- 支持开关分隔线
- 左右、上下、子view之间、行与行之间均有间隔;
下面部分将讲解实现该布局的全部步骤,并在最后附上完整代码。
准备工作
- 创建继承自ViewGroup的新class,并命名为FlowLayout。
创建自定义属性集
- 在res/value文件夹下创建attrs.xml;
- 在resources根标签下添加子标签declare-styleable声明新属性集,并设置其name属性;
- 依次添加各个自定义属性,并设置其数据格式。
本文中的流布局实现了alignment(对齐方式)和hasSplitLines(有无分隔线)两种属性,如下:
<declare-styleable name="FlowLayout">
<attr name="alignment">
<enum name="left" value="0"/>
<enum name="center" value="1"/>
<enum name="right" value="2"/>
</attr>
<attr name="hasSplitLines" format="boolean"/>
</declare-styleable>
实现LayoutParams
- 创建继承自ViewGroup.MarginLayoutParams的静态内部类LayoutParams;
- 在attrs.xml文件夹中创建属性集FlowLayout_Layout,并加入提供给子view的自定义属性。本文中提供了layout_gravity用于设置子view在纵向的位置;
- 覆盖构造器,主要是在LayoutParams(Context c, AttributeSet attrs)这个构造器中利用TypedArray提取自定义属性;
- 实现FlowLayout的generateDefaultLayoutParams(),checkLayoutParams(),generateLayoutParams()几个方法。
属性集如下:
<declare-styleable name="FlowLayout_Layout">
<attr name="layout_gravity">
<enum name="top" value="0"/>
<enum name="center" value="1"/>
<enum name="bottom" value="2"/>
</attr>
</declare-styleable>
onMeasure()方法实现
- 根据widthMeasureSpec确定FlowLayout自身的宽度;
- 利用measureChildWithMargins()方法对所有子view的宽高进行测量,测量时考虑左右、上下的间隔;
- 根据FlowLayout的宽度以及所有子view的宽度,确定出每个子view所在的行;
- 将每行的高度确定为该行高度最大的子view的高度,并将该行中所有layout_height属性为MATCH_PARENT的子view的高度重新设定为该行高度;
- 根据heightMeasureSpec以及所有行总高度(注意上下间隔与行间隔)确定出FlowLayout的高度。
onLayout()方法实现
下面为布置某一行的方法:
1. 计算出该行中子view的总宽度(注意子view的间隔);
2. 根据整体对齐方式,计算出该行第一个子view左侧的位置坐标;
3. 使用layout方法依次布置该行每个子view(注意考虑子view的layout_gravity属性,会对y坐标产生影响);
onDraw()方法实现
- 判断是否需要分隔线,需要的话则计算出每条分隔线的位置并利用drawLine()方法画上。
以上就是所有核心方法的实现思路了,具体细节部分可以参照代码及代码注释,示例程序可见FlowLayout,GitHub。
FlowLayout完整代码
package com.example.swt369.flowlayout;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
/**
* Created by swt369 on 2017/8/25.
*/
public class FlowLayout extends ViewGroup {
/**
* left-aligned
*/
public static final int ALIGNMENT_LEFT = 0;
/**
* center-aligned
*/
public static final int ALIGNMENT_CENTER = 1;
/**
* right-aligned
*/
public static final int ALIGNMENT_RIGHT = 2;
private int mAlignment;
private int mSpaceLeftAndRight;
private int mSpaceTopAndBottom;
private int mSpaceBetweenChildren;
private int mSpaceBetweenLevels;
private int mWidth;
private int mHeight;
boolean mHasSplitLines;
Paint paintForSplitLines;
private static final int DEFAULT_SPLIT_LINE_COLOR = Color.argb(255,176,48,96);
private static final int DEFAULT_SPLIT_LINE_THICKNESS = 4;
private ArrayList<ArrayList<View>> mLevels;
private ArrayList<Integer> mLevelHeights;
public FlowLayout(Context context) {
this(context,null);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.FlowLayout);
try {
mAlignment = ta.getInt(R.styleable.FlowLayout_alignment,ALIGNMENT_LEFT);
mHasSplitLines = ta.getBoolean(R.styleable.FlowLayout_hasSplitLines,true);
}finally {
ta.recycle();
}
float density = context.getResources().getDisplayMetrics().density;
mSpaceLeftAndRight = (int)(10 * density);
mSpaceTopAndBottom = (int)(5 * density);
mSpaceBetweenChildren = (int)(5 * density);
mSpaceBetweenLevels = (int)(5 * density);
paintForSplitLines = new Paint();
paintForSplitLines.setColor(DEFAULT_SPLIT_LINE_COLOR);
paintForSplitLines.setStrokeWidth(DEFAULT_SPLIT_LINE_THICKNESS);
}
public void setAlignment(int alignment){
if(alignment == mAlignment){
return;
}
if(alignment == ALIGNMENT_LEFT || alignment == ALIGNMENT_CENTER || alignment == ALIGNMENT_RIGHT){
mAlignment = alignment;
requestLayout();
}
}
public int getAlignment(){
return mAlignment;
}
public void openSplitLines(){
if(!mHasSplitLines){
mHasSplitLines = true;
invalidate();
}
}
public void closeSplitLines(){
if(mHasSplitLines){
mHasSplitLines = false;
invalidate();
}
}
public boolean hasSplitLines(){
return mHasSplitLines;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//step 1,determine the width of this layout.
//make the width equal widthSize whatever the width mode is.
if(widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED){
mWidth = widthSize;
}
//step 2,measure all the children in order to get their width and height
int count = getChildCount();
int[] childWidths = new int[count];
for(int i = 0 ; i < count ; i++){
View child = getChildAt(i);
if(child.getVisibility() != GONE){
measureChildWithMargins(
child,
widthMeasureSpec,2 * mSpaceLeftAndRight,
heightMeasureSpec,2 * mSpaceTopAndBottom
);
// child.measure(
// getChildMeasureSpec(widthMeasureSpec,2 * mSpaceLeftAndRight,child.getLayoutParams().width),
// getChildMeasureSpec(heightMeasureSpec,2 * mSpaceTopAndBottom,child.getLayoutParams().height)
// );
int height = child.getMeasuredHeight();
childWidths[i] = child.getMeasuredWidth();
}
}
//step 3,determine which level the children will be in.
mLevels = new ArrayList<>();
mLevels.add(new ArrayList<View>());
int curLevel = 0;
int curX = mSpaceLeftAndRight;
for(int i = 0 ; i < count ; i++){
View child = getChildAt(i);
if(child.getVisibility() != GONE){
if(childWidths[i] > mWidth - 2 * mSpaceLeftAndRight){
//this view is too big to be put in even an empty level,so give it up.
continue;
}
if(curX + childWidths[i] <= mWidth - mSpaceLeftAndRight){
//current level has enough space to put this view in.
mLevels.get(curLevel).add(child);
curX += (childWidths[i] + mSpaceBetweenChildren);
}else {
//current level doesn't have enough space to add this view,
//so switch into next level.
mLevels.add(new ArrayList<View>());
curLevel++;
curX = mSpaceLeftAndRight;
mLevels.get(curLevel).add(child);
curX += (childWidths[i] + mSpaceBetweenChildren);
}
if(curX > mWidth - mSpaceLeftAndRight){
//current level doesn't have enough space to add any view,
//so switch into next level.
mLevels.add(new ArrayList<View>());
curLevel++;
curX = mSpaceLeftAndRight;
}
}
}
//step 4,adjust the height of the children according to their layout_height.
mLevelHeights = new ArrayList<>();
for(int i = 0 ; i < mLevels.size() ; i++){
//obtain the maximum height of this level
int maxHeight = 0;
for(View child : mLevels.get(i)){
if(child.getVisibility() != GONE){
maxHeight = Math.max(maxHeight,child.getMeasuredHeight());
}
}
mLevelHeights.add(maxHeight);
//if a child's layout_height equals MATCH_PARENT
//and its height doesn't equal the maximum height of this level,
//then remeasure it.
for(View child : mLevels.get(i)){
if(child.getVisibility() != GONE){
if(child.getLayoutParams().height == ViewGroup.LayoutParams.MATCH_PARENT
&& child.getMeasuredHeight() != maxHeight){
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(maxHeight,MeasureSpec.EXACTLY)
);
}
}
}
}
//step 5,determine the height of this layout.
//be the size of the parent
if(heightMode == MeasureSpec.EXACTLY){
mHeight = heightSize;
}else {
//be the size of all the levels.
mHeight = 2 * mSpaceTopAndBottom;
for(Integer integer : mLevelHeights){
mHeight += (integer + mSpaceBetweenLevels);
}
mHeight -= mSpaceBetweenLevels;
}
//step 6,set the width and height of this layout.
setMeasuredDimension(mWidth,mHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int curY = mSpaceTopAndBottom;
for(int i = 0 ; i < mLevels.size() ; i++){
//calculate the total width of the views in this level(including space between them).
int curX = 0;
int totalWidth = 0;
for(View child : mLevels.get(i)){
totalWidth += (child.getMeasuredWidth() + mSpaceBetweenChildren);
}
totalWidth -= mSpaceBetweenChildren;
//use different ways to determine the start position
//so as to realize different ways of alignment.
switch (mAlignment){
case ALIGNMENT_LEFT:
curX = mSpaceLeftAndRight;
break;
case ALIGNMENT_CENTER:
curX = (mWidth - totalWidth) / 2;
break;
case ALIGNMENT_RIGHT:
curX = mWidth - mSpaceLeftAndRight - totalWidth;
break;
}
//determine the accurate position of the views according to their layout_gravity
for(View view : mLevels.get(i)){
if(view.getVisibility() != GONE){
LayoutParams params = (FlowLayout.LayoutParams)view.getLayoutParams();
switch (params.gravity){
case LayoutParams.GRAVITY_TOP:
view.layout(
curX,
curY,
curX + view.getMeasuredWidth(),
curY + view.getMeasuredHeight()
);
break;
case LayoutParams.GRAVITY_CENTER:
int space = (mLevelHeights.get(i) - view.getMeasuredHeight()) / 2;
view.layout(
curX,
curY + space,
curX + view.getMeasuredWidth(),
curY + mLevelHeights.get(i) - space
);
break;
case LayoutParams.GRAVITY_BOTTOM:
view.layout(
curX,
curY + mLevelHeights.get(i) - view.getMeasuredHeight(),
curX + view.getMeasuredWidth(),
curY + mLevelHeights.get(i)
);
break;
}
curX += (view.getMeasuredWidth() + mSpaceBetweenChildren);
}
}
curY += (mLevelHeights.get(i) + mSpaceBetweenLevels);
}
}
@Override
protected void onDraw(Canvas canvas) {
if(mHasSplitLines){
//draw split lines
int curY = mSpaceTopAndBottom / 2;
for(int i = 0 ; i < mLevelHeights.size() ; i++){
canvas.drawLine(0,curY,mWidth,curY,paintForSplitLines);
curY += (mLevelHeights.get(i) + mSpaceBetweenLevels);
}
canvas.drawLine(0,curY,mWidth,curY,paintForSplitLines);
}
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof FlowLayout.LayoutParams;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p.width,p.height);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(),attrs);
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams{
public static final int GRAVITY_TOP = 0;
public static final int GRAVITY_CENTER = 1;
public static final int GRAVITY_BOTTOM = 2;
public int gravity = GRAVITY_BOTTOM;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray ta = c.obtainStyledAttributes(attrs,R.styleable.FlowLayout_Layout);
try {
gravity = ta.getInt(R.styleable.FlowLayout_Layout_layout_gravity,GRAVITY_CENTER);
}finally {
ta.recycle();
}
}
public LayoutParams(int width,int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}