Android 自定义控件之标签控件

一、首先这是效果

二、实现原理

通过继承ViewGroup,然后在重写 onMeasure测量每个View的宽度,重新onLayout控制每个控件的位置,

并添加点击事件

三、实现

1、在onMeasure方法中得到显示方式,并得到宽高

 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);


        int parentTotalWidth = MeasureSpec.getSize(widthMeasureSpec);
        //得到真实的宽,去掉左右内边距
        int parentWidth = parentTotalWidth - getPaddingLeft() - getPaddingRight();
        //得到真实的高,去掉上下内边距
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();

2、遍历子控件,得到他们的宽高,然后和当前控件的宽度去比较,如过小于,当前控件的宽度,则继续向一行添加,如果大于则新添加一行

  clearData();
        //得到所有的子控件
        int childCount = getChildCount();

        //遍历
        for (int i = 0; i < childCount; i++) {
            //得到子控件
            View childView = getChildAt(i);
            //如果子控件为隐藏,则不计入宽高
            if (childView.getVisibility() == View.GONE) {
                continue;
            }
            //创建一个spec文件
            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, 
       widthMode == MeasureSpec.EXACTLY ?
                    MeasureSpec.AT_MOST : widthMode);
            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentHeight,         
      heightMode == MeasureSpec.EXACTLY ?
                    MeasureSpec.AT_MOST : heightMode);
            //测量view
            childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mLine == null)
                mLine = new CustomLine();

            int childWidth = childView.getMeasuredWidth();
            //累加使用的宽度
            mUsedWidth += childWidth;
            //当前行,能显示的下,就在当前显示
            if (mUsedWidth <= parentWidth) {
                mLine.addView(childView);
                mUsedWidth += mHorizontalSpace;//添加上间距
                if (mUsedWidth >= parentWidth) {
                    //如果加上间距大于当前宽度,则换行
                    if (!isAddLine()) {
                        break;
                    }
                }
            } else {
                //显示不了,则需要换行
                if (mLine.getLineViewSize() == 0) {
                    //一行一个都没有,说明标签里的数据太多,但是也得加上,不然就空了一行
                    mLine.addView(childView);
                    if (!isAddLine()) {
                        break;
                    }
                } else {
                    //该行有数据,但是还是显示 不下,所以直接换行
                    if (!isAddLine()) {
                        break;
                    }
                    //这是一个新行,所以不管有没有都必须加上去
                    mLine.addView(childView);
                    mUsedWidth += childWidth + mHorizontalSpace;
                }
            }
        }

3、在判断当前行是不是为空,因为最后一行如果没放满,则可能不能加入,则需要判断

 if (mLine != null && mLine.getLineViewSize() > 0 && !listLines.contains(mLine)) {
            //如果最后一行添加了数据,但是没超过当前宽度,则可能不会加入,所以添加判断
            listLines.add(mLine);
        }

4、得到当前控件的高,就是所有子控件的高之和,最后保存测量宽高

 int totalWidth = parentTotalWidth;
        int totalHeight = 0;
        int lineCount = listLines.size();
        for (int i = 0; i < lineCount; i++) {
            totalHeight += listLines.get(i).lineMostHeight;
        }
        totalHeight += mVerticalSpace * (lineCount - 1) + getPaddingBottom() + getPaddingTop();
        //保存测量结果
        setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec));

5、重写onLayout进行摆放,判断是不是需要紧凑排序,如果要,则创建一个临时存放所有子View的集合,然后拿到所有的view的宽度,然后用冒泡排序从大到小排列,然后分别用最后一个最大的子view宽度和最小的去匹配,然后在和当前控件的宽去比较,然后在决定是继续添加还是重新创建一行

  int left = getPaddingLeft();
            int top = getPaddingTop();

            try {
                if (tempAllView != null && tempAllView.size() != 0) {
                    tempAllView.clear();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            for (int i = 0; i < listLines.size(); i++) {
                CustomLine tempLine = listLines.get(i);
                for (int j = 0; j < tempLine.lineViews.size(); j++) {
                    tempAllView.add(tempLine.lineViews.get(j));
                }
            }
            if (isCompact) {


                isCompact = !isCompact;
                //需要紧凑排列,打乱所有行中的数据,然后重新组装
                //把所有view从小到大排列
                View tempView = null;
                for (int i = 0; i < tempAllView.size() - 1; i++) {

                    for (int j = 0; j < tempAllView.size() - 1 - i; j++) {
                        if (tempAllView.get(j).getMeasuredWidth() >
                                tempAllView.get(j + 1).getMeasuredWidth()) {
                            tempView = tempAllView.get(j);
                            tempAllView.set(j, tempAllView.get(j + 1));
                            tempAllView.set(j + 1, tempView);
                        }
                    }
                }

                clearData();
                int viewsCount = tempAllView.size();
                int screenWidth = getMeasuredWidth() - getPaddingRight() - getPaddingLeft();

                int tempFirstWidth = 0;
                if (mLine == null)
                    mLine = new CustomLine();

                //遍历并去匹配
                for (int i = 0; i < viewsCount; i++) {

                    usedList.add(tempAllView.get(viewsCount - 1 - i).hashCode());
                    tempFirstWidth = tempAllView.get(viewsCount - 1 - i).getMeasuredWidth();
                    mLine.addView(tempAllView.get(viewsCount - 1 - i));
                    for (int j = 0; j < viewsCount; j++) {
                        if (!usedList.contains(tempAllView.get(j).hashCode()) && tempFirstWidth > 0) {
                            if ((tempFirstWidth + tempAllView.get(j).getMeasuredWidth()) <= screenWidth) {
                                tempFirstWidth += tempAllView.get(j).getMeasuredWidth();
                                usedList.add(tempAllView.get(j).hashCode());
                                mLine.addView(tempAllView.get(j));

                            } else {
                                tempFirstWidth = 0;
                                if (!isAddLine()) {
                                    continue;
                                }
                            }
                        }
                    }
                }
                if (mLine != null && mLine.getLineViewSize() > 0 && !listLines.contains(mLine)) {
                    //如果最后一行添加了数据,但是没超过当前宽度,则可能不会加入,所以添加判断
                    listLines.add(mLine);
                }


            }

如果有点击事件,则设置点击事件

 if (tagLayoutListener != null) {
                for (int i = 0; i < tempAllView.size(); i++) {
                    tempAllView.get(i).setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (tagLayoutListener != null) {
                                tagLayoutListener.tagLayoutItemClickListener(v);
                            }
                        }
                    });
                }
            }

最后摆放当前控件

 public void LayoutView(int l, int t) {
            int left = l;
            int top = t;

            int count = getLineViewSize();
            //得到当前行的总宽度
            int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            int right = totalWidth;
            //得到当前行,使用后剩余的宽度
            int surplusWidth = totalWidth - lineTotalWidth - mHorizontalSpace * (count - 1);
            if (surplusWidth >= 0) {
                //还有剩余
                int averageWidth = 0;
                try {
                    averageWidth = (int) (surplusWidth / count + 0.5);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                for (int i = 0; i < count; i++) {
                    View view = lineViews.get(i);

                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();
                    int topOffset = (int) ((lineMostHeight - childHeight) / 2 + 0.5);
                    if (topOffset < 0) {
                        topOffset = 0;
                    }
                    if (gravity == 2) {
                        //居中显示,平均剩余宽度到每个view上
                        childWidth = childWidth + averageWidth;
                        view.getLayoutParams().width = childWidth;
                    }

                    if (averageWidth > 0) {
                        //子view改变,需重新测量
                        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
                        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
                        view.measure(widthMeasureSpec, heightMeasureSpec);
                    }
                    // 摆放
                    if (gravity == 1 || gravity == 2) {

                        view.layout(left, top + topOffset, left + childWidth, lineMostHeight + topOffset + top);
                        left += childWidth + mHorizontalSpace;//为下一个view的left赋值
                    } else if (gravity == 3) {
                        view.layout(right - childWidth, top + topOffset, right, lineMostHeight + topOffset + top);
                        right -= childWidth + mHorizontalSpace;//为下一个view的left赋值
                    }


                }

            } else {
                //没有剩余
                if (count == 1) {
                    //只有一个
                    View view = lineViews.get(0);
                    view.layout(left, top, view.getMeasuredWidth(), top + view.getMeasuredHeight());
                } else {
                    System.err.print("this is error");
                }
            }

        }

当然,这里还设置了别的属性,比如从左边开始,或者右边开始,或者居中,是否需要紧凑排列

注意,如果可能出现在子线程刷新UI的情况,可以把requestLayout();放入主线程中

最后是所有源代码

package com.xiaofan.customcontrol;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import org.w3c.dom.Text;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TagLayout extends ViewGroup {

    public interface TagLayoutItemClickListener {
        void tagLayoutItemClickListener(View view);
    }

    private TagLayoutItemClickListener tagLayoutListener;

    public void setTagLayoutItemClickListener(TagLayoutItemClickListener listener) {
        this.tagLayoutListener = listener;
    }

    public TagLayout(Context context) {
        super(context);
    }

    public TagLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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


    //所有的行数
    private List<CustomLine> listLines = new ArrayList<>();
    private int gravity = 1;
    private CustomLine mLine = null;
    //最大行数
    private int mMaxLines = Integer.MAX_VALUE;
    //水平的间距
    private int mHorizontalSpace = 0;
    //竖直的间距
    private int mVerticalSpace = 0;
    private boolean isCompact = false;

    /**
     * @param mgravity 1,left,2,center,3,right
     */
    public void setGravity(int mgravity) {
        if (this.gravity != mgravity) {
            this.gravity = mgravity;
            requestLayout();
        }

    }

    /**
     * 设置水平的间距
     *
     * @param space
     */
    public void setHorizontalSpace(int space) {
        if (this.mHorizontalSpace != space) {
            this.mHorizontalSpace = space;
            requestLayout();
        }

    }

    /**
     * 设置竖直的间距
     *
     * @param space
     */
    public void setVerticalSpace(int space) {
        if (this.mVerticalSpace != space) {
            this.mVerticalSpace = space;
            requestLayout();
        }
    }

    /**
     * @param isC 是否要让当前的东西紧凑显示
     *            这个很费性能,建议慎用
     */
    public void setIsNeedCompact(boolean isC) {

        if (this.isCompact != isC) {
            this.isCompact = isC;
            requestLayout();
        }
    }

    /**
     * 设置最大行数
     *
     * @param maxLines
     */
    public void setMaxLines(int maxLines) {
        if (this.mMaxLines != maxLines) {
            this.mMaxLines = maxLines;
            requestLayout();
        }
    }

    //计算已经使用的宽度
    private int mUsedWidth = 0;

    private List<Integer> usedList = new ArrayList<Integer>();
    private List<View> tempAllView = new ArrayList<>();

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);


        int parentTotalWidth = MeasureSpec.getSize(widthMeasureSpec);
        //得到真实的宽,去掉左右内边距
        int parentWidth = parentTotalWidth - getPaddingLeft() - getPaddingRight();
        //得到真实的高,去掉上下内边距
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();

        clearData();
        //得到所有的子控件
        int childCount = getChildCount();

        //遍历
        for (int i = 0; i < childCount; i++) {
            //得到子控件
            View childView = getChildAt(i);
            //如果子控件为隐藏,则不计入宽高
            if (childView.getVisibility() == View.GONE) {
                continue;
            }
            //创建一个spec文件
            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidth, widthMode == MeasureSpec.EXACTLY ?
                    MeasureSpec.AT_MOST : widthMode);
            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentHeight, heightMode == MeasureSpec.EXACTLY ?
                    MeasureSpec.AT_MOST : heightMode);
            //测量view
            childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mLine == null)
                mLine = new CustomLine();

            int childWidth = childView.getMeasuredWidth();
            //累加使用的宽度
            mUsedWidth += childWidth;
            //当前行,能显示的下,就在当前显示
            if (mUsedWidth <= parentWidth) {
                mLine.addView(childView);
                mUsedWidth += mHorizontalSpace;//添加上间距
                if (mUsedWidth >= parentWidth) {
                    //如果加上间距大于当前宽度,则换行
                    if (!isAddLine()) {
                        break;
                    }
                }
            } else {
                //显示不了,则需要换行
                if (mLine.getLineViewSize() == 0) {
                    //一行一个都没有,说明标签里的数据太多,但是也得加上,不然就空了一行
                    mLine.addView(childView);
                    if (!isAddLine()) {
                        break;
                    }
                } else {
                    //该行有数据,但是还是显示 不下,所以直接换行
                    if (!isAddLine()) {
                        break;
                    }
                    //这是一个新行,所以不管有没有都必须加上去
                    mLine.addView(childView);
                    mUsedWidth += childWidth + mHorizontalSpace;
                }
            }
        }

        if (mLine != null && mLine.getLineViewSize() > 0 && !listLines.contains(mLine)) {
            //如果最后一行添加了数据,但是没超过当前宽度,则可能不会加入,所以添加判断
            listLines.add(mLine);
        }
        int totalWidth = parentTotalWidth;
        int totalHeight = 0;
        int lineCount = listLines.size();
        for (int i = 0; i < lineCount; i++) {
            totalHeight += listLines.get(i).lineMostHeight;
        }
        totalHeight += mVerticalSpace * (lineCount - 1) + getPaddingBottom() + getPaddingTop();
        //保存测量结果
        setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec));
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //摆放
        if (changed) {
            int left = getPaddingLeft();
            int top = getPaddingTop();

            try {
                if (tempAllView != null && tempAllView.size() != 0) {
                    tempAllView.clear();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            for (int i = 0; i < listLines.size(); i++) {
                CustomLine tempLine = listLines.get(i);
                for (int j = 0; j < tempLine.lineViews.size(); j++) {
                    tempAllView.add(tempLine.lineViews.get(j));
                }
            }
            if (isCompact) {


                isCompact = !isCompact;
                //需要紧凑排列,打乱所有行中的数据,然后重新组装
                //把所有view从小到大排列
                View tempView = null;
                for (int i = 0; i < tempAllView.size() - 1; i++) {

                    for (int j = 0; j < tempAllView.size() - 1 - i; j++) {
                        if (tempAllView.get(j).getMeasuredWidth() >
                                tempAllView.get(j + 1).getMeasuredWidth()) {
                            tempView = tempAllView.get(j);
                            tempAllView.set(j, tempAllView.get(j + 1));
                            tempAllView.set(j + 1, tempView);
                        }
                    }
                }

                clearData();
                int viewsCount = tempAllView.size();
                int screenWidth = getMeasuredWidth() - getPaddingRight() - getPaddingLeft();

                int tempFirstWidth = 0;
                if (mLine == null)
                    mLine = new CustomLine();

                //遍历并去匹配
                for (int i = 0; i < viewsCount; i++) {

                    usedList.add(tempAllView.get(viewsCount - 1 - i).hashCode());
                    tempFirstWidth = tempAllView.get(viewsCount - 1 - i).getMeasuredWidth();
                    mLine.addView(tempAllView.get(viewsCount - 1 - i));
                    for (int j = 0; j < viewsCount; j++) {
                        if (!usedList.contains(tempAllView.get(j).hashCode()) && tempFirstWidth > 0) {
                            if ((tempFirstWidth + tempAllView.get(j).getMeasuredWidth()) <= screenWidth) {
                                tempFirstWidth += tempAllView.get(j).getMeasuredWidth();
                                usedList.add(tempAllView.get(j).hashCode());
                                mLine.addView(tempAllView.get(j));

                            } else {
                                tempFirstWidth = 0;
                                if (!isAddLine()) {
                                    continue;
                                }
                            }
                        }
                    }
                }
                if (mLine != null && mLine.getLineViewSize() > 0 && !listLines.contains(mLine)) {
                    //如果最后一行添加了数据,但是没超过当前宽度,则可能不会加入,所以添加判断
                    listLines.add(mLine);
                }


            }
            //给每个view设置点击事件
            if (tagLayoutListener != null) {
                for (int i = 0; i < tempAllView.size(); i++) {
                    tempAllView.get(i).setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (tagLayoutListener != null) {
                                tagLayoutListener.tagLayoutItemClickListener(v);
                            }
                        }
                    });
                }
            }
            //摆放
            for (int i = 0; i < listLines.size(); i++) {
                CustomLine line = listLines.get(i);
                line.LayoutView(left, top);
                top += line.lineMostHeight + mVerticalSpace;
            }


        }

    }


    private void clearData() {
        try {
            listLines.clear();
            mLine = new CustomLine();
            mUsedWidth = 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 是不是新添加一行
     *
     * @return
     */
    private boolean isAddLine() {
        if (mLine != null)
            listLines.add(mLine);
        //如果当前的行数小于最大行数则添加一行
        if (listLines.size() < mMaxLines) {
            mLine = new CustomLine();
            mUsedWidth = 0;
            return true;
        }
        return false;

    }


    //一行的对象
    private class CustomLine {
        int lineTotalWidth = 0;//得到该行的所有子View累加宽度
        int lineMostHeight = 0;//得到该行子View中最高的
        private List<View> lineViews = new ArrayList<>();//存放一行的所有View

        public int getLineViewSize() {
            return lineViews.size();
        }

        public void addView(View view) {
            lineViews.add(view);
            lineTotalWidth += view.getMeasuredWidth();
            int childHeight = view.getMeasuredHeight();
            lineMostHeight = Math.max(childHeight, lineMostHeight);

        }

        public void LayoutView(int l, int t) {
            int left = l;
            int top = t;

            int count = getLineViewSize();
            //得到当前行的总宽度
            int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
            int right = totalWidth;
            //得到当前行,使用后剩余的宽度
            int surplusWidth = totalWidth - lineTotalWidth - mHorizontalSpace * (count - 1);
            if (surplusWidth >= 0) {
                //还有剩余
                int averageWidth = 0;
                try {
                    averageWidth = (int) (surplusWidth / count + 0.5);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                for (int i = 0; i < count; i++) {
                    View view = lineViews.get(i);

                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();
                    int topOffset = (int) ((lineMostHeight - childHeight) / 2 + 0.5);
                    if (topOffset < 0) {
                        topOffset = 0;
                    }
                    if (gravity == 2) {
                        //居中显示,平均剩余宽度到每个view上
                        childWidth = childWidth + averageWidth;
                        view.getLayoutParams().width = childWidth;
                    }

                    if (averageWidth > 0) {
                        //子view改变,需重新测量
                        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
                        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
                        view.measure(widthMeasureSpec, heightMeasureSpec);
                    }
                    // 摆放
                    if (gravity == 1 || gravity == 2) {

                        view.layout(left, top + topOffset, left + childWidth, lineMostHeight + topOffset + top);
                        left += childWidth + mHorizontalSpace;//为下一个view的left赋值
                    } else if (gravity == 3) {
                        view.layout(right - childWidth, top + topOffset, right, lineMostHeight + topOffset + top);
                        right -= childWidth + mHorizontalSpace;//为下一个view的left赋值
                    }


                }

            } else {
                //没有剩余
                if (count == 1) {
                    //只有一个
                    View view = lineViews.get(0);
                    view.layout(left, top, view.getMeasuredWidth(), top + view.getMeasuredHeight());
                } else {
                    System.err.print("this is error");
                }
            }

        }
    }

}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值