课前准备
课堂笔记
- 创建 TextFlowLayout 继承 ViewGroup ,实现方法 onLayout,添加构造方法,并统一入口
public class TextFlowLayout extends ViewGroup {
public TextFlowLayout(Context context) {
this(context, null);
}
public TextFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TextFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
添加子View
- 暴露方法获取子 View 的数据,并添加到 TextFlowLayout
private List<String> mStringList;
public void setTextList(List<String> stringList) {
// 每次添加之前清楚所有 view
removeAllViews();
this.mStringList = stringList;
for (String text : mStringList) {
TextView item = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.flow_text_view, this, false);
item.setText(text);
item.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mListener != null) {
mListener.onFlowTextItemClick(item.getText().toString());
}
}
});
addView(item);
}
}
自定义属性
- 给 TextFlowLayout 自定义个边距属性,设置默认值
public static final float DEFAULT_SPACE = 10;
private float mItemVerticalSpace;
private float mItemHorizontalSpace;
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlowTextStyle">
<attr name="vertical_space" format="dimension" />
<attr name="horizontal_space" format="dimension" />
</declare-styleable>
</resources>
public TextFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowTextStyle);
mItemVerticalSpace = ta.getDimension(R.styleable.FlowTextStyle_vertical_space, DEFAULT_SPACE);
mItemHorizontalSpace = ta.getDimension(R.styleable.FlowTextStyle_horizontal_space, DEFAULT_SPACE);
ta.recycle();
}
测量
- 按照逻辑一步一步写,思路很清晰的,首先测量子 view 的宽度,判断是否能在当前行放下,放不下就新建一行放,然后所有 view 测量完后,再根据行数测量自己的高度,最后设置布局数据,onMeasure 方法会多次调用,测量前清空所有行数据
private int mSelfWidth;
private int mLineHeight;
private List<List<View>> lists = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 如果子 View 的数量为0 就不再继续测量
if (getChildCount() == 0) return;
// 可以放子 view 的宽度
mSelfWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
//onMeasure 方法会多次调用,测量前清空所有行数据
lists.clear();
// 测量前清空每行数据
List<View> line = null;
// 遍历所有的子 view
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View itemView = getChildAt(i);
// 如果子 View 不可见,停止这次view 测量
if (itemView.getVisibility() != VISIBLE) {
continue;
}
// 测量孩子
measureChild(itemView, widthMeasureSpec, heightMeasureSpec);
if (line == null) {
// 如果当前行是空,创建行并添加子 view
line = createLine(itemView);
} else {
// 如果当前看不为空,判断是否还能再添加子view
if (canAdd(itemView, line)) {
// 能,继续添加
line.add(itemView);
} else {
// 不能,创建新行添加
line = createLine(itemView);
}
}
}
// 测量自己
// 每行的高度
mLineHeight = getChildAt(0).getMeasuredHeight();
// 自己的高度
int selfHeight = (int) (lists.size() * (mLineHeight + mItemHorizontalSpace) + 0.5f);
setMeasuredDimension(mSelfWidth, selfHeight);
}
- 其中判断子 view 能不能在当前行放下的方法 canAdd
private boolean canAdd(View child, List<View> line) {
// 条件:child 的宽度 + line 子view 的宽度 + 间距宽度 小于 屏幕宽度
float totalWidth = child.getMeasuredWidth();
for (View view : line) {
totalWidth += view.getMeasuredWidth();
}
totalWidth += mItemHorizontalSpace * (line.size() + 1);
return totalWidth <= mSelfWidth;
}
private List<View> createLine(View child) {
List<View> line = new ArrayList<>();
line.add(child);
lists.add(line);
return line;
}
布局
- 测量时,我们已经解决了布局换行的问题,所以布局时直接把子 view 添加进去就可以了
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 顶部初始位置,每次换行都需改变
int topPoint = (int) mItemVerticalSpace;
for (List<View> list : lists) {
// 左边初始位置,每次添加view都需改变
int leftPoint = (int) mItemHorizontalSpace;
for (View view : list) {
view.layout(leftPoint, topPoint, leftPoint + view.getMeasuredWidth(),
topPoint + view.getMeasuredHeight());
leftPoint += view.getMeasuredWidth() + mItemHorizontalSpace;
}
topPoint += mLineHeight + mItemVerticalSpace;
}
}
设置点击事件
public void setOnFlowTextItemClickListener(OnFlowTextItemClickListener listener) {
this.mListener = listener;
}
public interface OnFlowTextItemClickListener {
void onFlowTextItemClick(String text);
}
public void setTextList(List<String> stringList) {
this.mStringList = stringList;
for (String text : mStringList) {
TextView item = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.flow_text_view, this, false);
item.setText(text);
item.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mListener != null) {
mListener.onFlowTextItemClick(item.getText().toString());
}
}
});
addView(item);
}
}
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/shape_text_flow_press" android:state_pressed="true" />
<item android:drawable="@drawable/shape_text_flow_normal" />
</selector>