课前准备
课堂笔记
- 创建 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>