前言
其实对于流式布局控件,很多人并不陌生,项目中或多或少都会用到的.但是有多少人会写一个流式布局的控件这就不知道了,所以博主这里对流式布局进行一个讲解,并且封装一个比较完善的控件
效果图
看到的这个整个就是一个流式布局,里面是很多个TextView,博主使用了一个圆角的背景为了显示的好看一点,当然了,流式布局控件并不关心里面的控件是什么控件,任何控件在流式布局内部都是可以显示的
效果图上了,接下来就是教大家如何自定义了
分析
1.这是一个什么类型的控件?
答:这是一个容器控件,那么就需要继承ViewGroup
2.流式布局是什么意思?
答:就是每一个控件从左往右排列,如果一行放不下啦就换一行显示
3.流式布局支持哪些属性?
答:支持行间距,支持控件左右间的间距,整个流式布局支持内边距,支持做多显示几行(这个属性有时候很好用的哦,至少博主项目中就用到了)
4.流式布局有什么特点?
答:这个特点是博主这个流式布局的特点,并非所有的,博主实现的就是每一个流式布局内部控件推荐的高度都是一样的,但是如果出现大小不一的情况,高度小的会显示在一行的中间
代码实现
1.支持的属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="XFlowLayout">
<!--横纵向的间距-->
<attr name="h_space" format="dimension" />
<attr name="v_space" format="dimension" />
<attr name="maxlines" format="integer" />
</declare-styleable>
</resources>
2.继承ViewGroup并且创建相应的构造函数
public class XFlowLayout extends ViewGroup {
public XFlowLayout(Context context) {
this(context, null);
}
public XFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.XFlowLayout);
//获取横纵向的间距
mHSpace = a.getDimensionPixelSize(R.styleable.XFlowLayout_h_space, dpToPx(10));
mVSpace = a.getDimensionPixelSize(R.styleable.XFlowLayout_v_space, dpToPx(10));
mMaxLines = a.getInt(R.styleable.XFlowLayout_maxlines, -1);
a.recycle();
}
}
控件内的属性和支持的属性对应
/**
* -1表示不限制,最多显示几行
*/
private int mMaxLines = -1;
/**
* 孩子中最高的一个
*/
private int mChildMaxHeight;
/**
* 每一个孩子的左右的间距
* 20是默认值,单位是px
*/
private int mHSpace = 20;
/**
* 每一行的上下的间距
* 20是默认值,单位是px
*/
private int mVSpace = 20;
测量方法
之前我们说过,博主这个流式布局控件每一个内部的控件的高度都是一眼的,所以这里测量孩子高度的时候,首先得让所有孩子都测量一遍,然后找到高度最高的那个值,然后再让孩子测量一遍(为什么要再测量一遍是因为第一次测量只是为了知道每一个孩子的高度,而第二次测量是告诉所有孩子高度现在确定啦,这样子每一个孩子内部才会根据这个值做一些计算,如果没有再次测量这一步,比如TextView显示文本的时候,你设置了居中显示,可能就会失效了!)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 拿到父容器推荐的宽和高以及计算模式
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
//测量孩子的大小
measureChildren(widthMeasureSpec, heightMeasureSpec);
//寻找孩子中最高的一个孩子,找到的值会放在mChildMaxHeight变量中
findMaxChildMaxHeight();
//初始化值
int left = getPaddingLeft(), top = getPaddingTop();
// 几行
int lines = 1;
//循环所有的孩子
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (left != getPaddingLeft()) { //是否是一行的开头
if ((left + view.getMeasuredWidth()) > sizeWidth - getPaddingRight()) { //需要换行了,因为放不下啦
// 如果到了最大的行数,就跳出,top就是当前的
if (mMaxLines != -1 && mMaxLines <= lines) {
break;
}
//计算出下一行的top
top += mChildMaxHeight + mVSpace;
left = getPaddingLeft();
lines++;
}
}
left += view.getMeasuredWidth() + mHSpace;
}
if (modeHeight == MeasureSpec.EXACTLY) {
//直接使用父类推荐的宽和高
setMeasuredDimension(sizeWidth, sizeHeight);
} else if (modeHeight == MeasureSpec.AT_MOST) {
setMeasuredDimension(sizeWidth, (top + mChildMaxHeight + getPaddingBottom()) > sizeHeight ? sizeHeight : top + mChildMaxHeight + getPaddingBottom());
} else {
setMeasuredDimension(sizeWidth, top + mChildMaxHeight + getPaddingBottom());
}
}
/**
* 寻找孩子中最高的一个孩子
*/
private void findMaxChildMaxHeight() {
mChildMaxHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (view.getMeasuredHeight() > mChildMaxHeight) {
mChildMaxHeight = view.getMeasuredHeight();
}
}
}
这里的代码分几步:
1.测量所有孩子,找到高度最高的值
2.然后我们模拟排列每一个孩子(View),计算出自身需要的高度
3.然后根据父容器推荐的高度的计算模式,来决定,自身高度是直接采用父容器给的高度还是采用自身的高度
4.最后设置自己的宽和高,把内边距考虑进去
5.这里有一个需要注意的是,如果设置了最高显示的行数,那么如果模拟排列的时候行数超过了最大的行数,那么高度就以最高行数的高度为准
安排孩子的位置
protected void onLayout(boolean changed, int l, int t, int r, int b) {
findMaxChildMaxHeight();
//开始安排孩子的位置
//初始化值
int left = getPaddingLeft(), top = getPaddingTop();
// 几行
int lines = 1;
//循环所有的孩子
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (left != getPaddingLeft()) { //是否是一行的开头
if ((left + view.getMeasuredWidth()) > getWidth() - getPaddingRight()) { //需要换行了,因为放不下啦
// 如果到了最大的行数,就跳出,top就是当前的
if (mMaxLines != -1 && mMaxLines <= lines) {
break;
}
//计算出下一行的top
top += mChildMaxHeight + mVSpace;
left = getPaddingLeft();
lines++;
}
}
//安排孩子的位置
view.layout(left, top, left + view.getMeasuredWidth(), top + mChildMaxHeight);
left += view.getMeasuredWidth() + mHSpace;
}
}
这次的安排和测量onMeasure方法中的模拟排列的逻辑是一样的.
不再赘述
到这里整个自定义就完工了,其实很简单,并没有什么难的地方,代码量也很少