自定义标签换行控件WordWrapperView

    遇到多项选择的情况,我们可以使用RadioGroup,但是它只能处理单行或者单列的情况,如果标签很多就需要滑动屏幕了。考虑到显示的简洁性,我们也可以考虑自定义一个换行控件。这里就记录一下我实现的换行控件WordWrapperView。

    一、大致思路:

    WordWrapperView扩展自ViewGroup,根据内容标签测量尺寸,确定内容标签的位置。

    二、实现:

    WordWrapperView类代码:

/**
 * 自动换行的空间,只支持自动换行<br/>
 * 1.没有考虑单个标签过长的情况<br/>
 * 2.不支持滚动,即标签过多时不支持上下滑动<br/>
 * 3.默认标签的高度都是一样的<br/>
 * Created by hsji on 16/1/9.
 */
public class WordWrapperView extends ViewGroup {
    private static final String TAG = WordWrapperView.class.getSimpleName();
    private static final int DEFAULT_HORIZONTAL_SPACING = 10;
    private static final int DEFAULT_VERTICAL_SPACING = 10;
    /**
     * 水平间距
     */
    private int mHorizontalSpacing;
    /**
     * 垂直间距
     */
    private int mVerticalSpacing;


    public WordWrapperView(Context context) {
        this(context, null);
    }

    public WordWrapperView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WordWrapperView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //通过 TypedArray 获取自定义属性
        TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.WordWrapperView);
        //获取自定义属性的个数
        int N = typedArray.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.WordWrapperView_horizontal_spacing:
                    //通过自定义属性拿到水平间距
                    mHorizontalSpacing = typedArray.getDimensionPixelSize(attr, DEFAULT_HORIZONTAL_SPACING);
                    break;
                case R.styleable.WordWrapperView_vertical_spacing:
                    //通过自定义属性拿到垂直间距
                    mVerticalSpacing = typedArray.getDimensionPixelSize(attr, DEFAULT_VERTICAL_SPACING);
                    break;
            }
        }
        //使用完成之后记得回收
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //必须先测量出宽度,之后才能测量高度
        int width = measureWidth(widthMeasureSpec);
        //根据测量出的宽度,测量高度
        int height = measureHeight(heightMeasureSpec, width);
        setMeasuredDimension(width, height);
    }

    /**
     * 计算所有标签排成一行所需要的宽度,再根据mode处理
     *
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec) {
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);

        int desired = getPaddingLeft();
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int childWidth = child.getMeasuredWidth();
            if (desired == getPaddingLeft()) {
                desired += childWidth;
            } else {
                desired += mHorizontalSpacing + childWidth;
            }
        }
        desired += getPaddingRight();
        if (mode == MeasureSpec.EXACTLY) {
            return size;
        } else {
            if (mode == MeasureSpec.AT_MOST) {
                return Math.min(desired, size);
            } else {
                return desired;
            }
        }
    }

    /**
     * 先计算出需要排多少行,根据行数算出高度,再根据mode处理
     *
     * @param heightMeasureSpec
     * @param width
     * @return
     */
    private int measureHeight(int heightMeasureSpec, int width) {
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);

        int counter = 0;//rows
        int totalWidth = getPaddingLeft();
        int count = getChildCount();
        int childHeight = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int childWidth = child.getMeasuredWidth();
            //第一行第一列的标签
            if (totalWidth == getPaddingLeft()) {
                totalWidth += childWidth;
                childHeight = child.getMeasuredHeight();
            } else {
                //第一列但非第一行的标签
                if (totalWidth + mHorizontalSpacing + childWidth <= width - getPaddingRight()) {
                    totalWidth += mHorizontalSpacing + childWidth;
                } else {//非第一列的标签
                    counter++;
                    totalWidth = getPaddingLeft() + childWidth;
                }
            }
        }
        int desired = getPaddingTop() + childHeight + (mVerticalSpacing + childHeight) * counter + getPaddingBottom();
        if (mode == MeasureSpec.EXACTLY) {
            return size;
        } else {
            if (mode == MeasureSpec.AT_MOST) {
                return Math.min(desired, size);
            } else {
                return desired;
            }
        }
    }

    /**
     * 采用两个变量totalWidth和totalHeight来记录坐标,考虑的情况和measureHeight类似
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int count = getChildCount();
            int totalWidth = getPaddingLeft();
            int totalHeight = getPaddingTop();
            int childHeight = 0;
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                childHeight = child.getMeasuredHeight();
                if (totalWidth == getPaddingLeft()) {
                    child.layout(totalWidth,
                            totalHeight,
                            totalWidth + child.getMeasuredWidth(),
                            totalHeight + child.getMeasuredHeight());
                    totalWidth += child.getMeasuredWidth();
                } else {
                    if (totalWidth + mHorizontalSpacing + child.getMeasuredWidth() <= r - l - getPaddingRight()) {
                        child.layout(totalWidth + mHorizontalSpacing,
                                totalHeight,
                                totalWidth + mHorizontalSpacing + child.getMeasuredWidth(),
                                totalHeight + child.getMeasuredHeight());
                        totalWidth += mHorizontalSpacing + child.getMeasuredWidth();
                    } else {
                        totalWidth = getPaddingLeft();
                        totalHeight += mVerticalSpacing + childHeight;
                        child.layout(totalWidth, totalHeight, totalWidth + child.getMeasuredWidth(), totalHeight + child.getMeasuredHeight());
                        totalWidth += child.getMeasuredWidth();
                    }
                }
            }
        }
    }
}

 自定义属性:

<declare-styleable name="WordWrapperView">
    <attr name="horizontal_spacing" format="dimension"></attr>
    <attr name="vertical_spacing" format="dimension"></attr>
</declare-styleable>

 三、使用

<com.hsji.testapp.widget.WordWrapperView
        android:id="@+id/wrapper"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        app:horizontal_spacing="10dp"
        app:vertical_spacing="20dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签1"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签2"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签3"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签4"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签5"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签6"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签7"
            android:textColor="@android:color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_btn_round_corner"
            android:padding="8dp"
            android:text="标签8"
            android:textColor="@android:color/white" />

    </com.hsji.testapp.widget.WordWrapperView>

 以上是直接在布局文件里面设置标签,也可以通过addView的方式往WordWrapperView里面添加标签。

另外可以为内容标签设置点击事件(tv.setOnClickListener(listener)),这里就不再赘述。

最后贴一张效果图:

 

原文地址:https://www.cnblogs.com/hsji/p/5117641.html