用来设置标签的流式布局简单设计

其实现在用来设置标签的流式布局开源库和文章都挺多的,写这个是因为自己学习总结自定义View的相关知识,毕竟看的再多都不如自己实现一下来得有用。流式布局用来学习还是挺不错的,下面就一步一步来实现流式布局。

实现基本功能

首先来说明几点:

1.标签视图TagView直接TextView,这样有几个好处:不用去重写onMeasure()接口,不用自己绘制Text,对Text控制也方便;
2.标签布局TagGroup继承ViewGroup,需要重写onMeasure()和onLayout()方法来控制TagView的显示;

直接来看下TagView的实现:

public class TagView extends TextView {

    private Paint mPaint;
    // 背景色
    private int mBgColor;
    // 边框颜色
    private int mBorderColor;
    // 字体颜色
    private int mTextColor;
    // 边框大小
    private float mBorderWidth;
    // 字体大小,单位sp
    private float mTextSize;
    // 边框角半径
    private float mRadius;
    // 字体水平空隙
    private int mHorizontalPadding;
    // 字体垂直空隙
    private int mVerticalPadding;
    // 边框矩形
    private RectF mRect;


    public TagView(Context context, String text) {
        super(context);
        setText(text);
        _init(context);
    }

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

    /**
     * 初始化
     * @param context
     */
    private void _init(Context context) {
        mRect = new RectF();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgColor = Color.parseColor("#33F44336");
        mBorderColor = Color.parseColor("#88F44336");
        mTextColor = Color.parseColor("#FF666666");
        mBorderWidth = MeasureUtils.dp2px(context, 0.5f);
        mTextSize = 13.0f;
        mRadius = MeasureUtils.dp2px(context, 5f);
        mHorizontalPadding = (int) MeasureUtils.dp2px(context, 5);
        mVerticalPadding = (int) MeasureUtils.dp2px(context, 5);
        // 设置字体占中
        setGravity(Gravity.CENTER);
        setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding);
        setTextColor(mTextColor);
        // 设置字体大小,如果转化为像素单位则要使用setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize)
        setTextSize(mTextSize);
//        setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 设置矩形边框
        mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h - mBorderWidth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 绘制背景
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mBgColor);
        canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint);
        // 绘制边框
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mBorderWidth);
        mPaint.setColor(mBorderColor);
        canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint);
        super.onDraw(canvas);
    }
}

其实还是很简单的,主要通过一些属性来设置绘制的效果,包括背景、边框和文字。在代码中设置了文字占中,并在onSizeChanged()方法中设置了边框矩形,其它就没什么了看代码就好了。再看下ViewGroup的实现:

public class TagGroup extends ViewGroup {

    private Paint mPaint;
    // 背景色
    private int mBgColor;
    // 边框颜色
    private int mBorderColor;
    // 边框大小
    private float mBorderWidth;
    // 边框角半径
    private float mRadius;
    // Tag之间的垂直间隙
    private int mVerticalInterval;
    // Tag之间的水平间隙
    private int mHorizontalInterval;
    // 边框矩形
    private RectF mRect;


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

    public TagGroup(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

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

    private void _init(Context context) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgColor = Color.parseColor("#11FF0000");
        mBorderColor = Color.parseColor("#22FF0000");
        mBorderWidth = MeasureUtils.dp2px(context, 1f);
        mRadius = MeasureUtils.dp2px(context, 5f);
        int defaultInterval = (int) MeasureUtils.dp2px(context, 5f);
        mHorizontalInterval = defaultInterval;
        mVerticalInterval = defaultInterval;
        mRect = new RectF();
        // 如果想要自己绘制内容,则必须设置这个标志位为false,否则onDraw()方法不会调用
        setWillNotDraw(false);
        setPadding(defaultInterval, defaultInterval, defaultInterval, defaultInterval);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        // 计算可用宽度,为测量宽度减去左右padding值
        int availableWidth = widthSpecSize - getPaddingLeft() - getPaddingRight();
        // 测量子视图
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        int tmpWidth = 0;
        int measureHeight = 0;
        int maxLineHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 记录该行的最大高度
            if (maxLineHeight == 0) {
                maxLineHeight = child.getMeasuredHeight();
            } else {
                maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight());
            }
            // 统计该行TagView的总宽度
            tmpWidth += child.getMeasuredWidth() + mHorizontalInterval;
            // 如果超过可用宽度则换行
            if (tmpWidth - mHorizontalInterval > availableWidth) {
                // 统计TagGroup的测量高度,要加上垂直间隙
                measureHeight += maxLineHeight + mVerticalInterval;
                // 重新赋值
                tmpWidth = child.getMeasuredWidth() + mHorizontalInterval;
                maxLineHeight = child.getMeasuredHeight();
            }
        }
        // 统计TagGroup的测量高度,加上最后一行
        measureHeight += maxLineHeight;

        // 设置测量宽高,记得算上padding
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (heightSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize, measureHeight + getPaddingTop() + getPaddingBottom());
        } else {
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        if (childCount <= 0) {
            return;
        }

        int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        // 当前布局使用的top坐标
        int curTop = getPaddingTop();
        // 当前布局使用的left坐标
        int curLeft = getPaddingLeft();
        int maxHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);

            if (maxHeight == 0) {
                maxHeight = child.getMeasuredHeight();
            } else {
                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
            }

            int width = child.getMeasuredWidth();
            int height = child.getMeasuredHeight();
            // 超过一行做换行操作
            if (width + curLeft > availableWidth) {
                curLeft = getPaddingLeft();
                // 计算top坐标,要加上垂直间隙
                curTop += maxHeight + mVerticalInterval;
                maxHeight = child.getMeasuredHeight();
            }
            // 设置子视图布局
            child.layout(curLeft, curTop, curLeft + width, curTop + height);
            // 计算left坐标,要加上水平间隙
            curLeft += width + mHorizontalInterval;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h - mBorderWidth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制背景
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mBgColor);
        canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint);
        // 绘制边框
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mBorderWidth);
        mPaint.setColor(mBorderColor);
        canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint);
    }


    /******************************************************************/

    /**
     * 添加Tag
     * @param text tag内容
     */
    public void addTag(String text) {
        addView(new TagView(getContext(), text));
    }

    public void addTags(String... textList) {
        for (String text : textList) {
            addTag(text);
        }
    }

    public void cleanTags() {
        removeAllViews();
        postInvalidate();
    }

    public void setTags(String... textList) {
        cleanTags();
        addTags(textList);
    }
}
其实代码主要看onMeasure()和onLayout()两个方法。

onMeasure()我们要对布局进行测量,遍历所有子视图来计算布局的最终宽高,需要注意的是要把布局的padding属性计算上去,所以布局可用宽度为测量宽度减去左右两边的padding值,除了padding需要计算外,还要计算上TagView之间的间隙值。具体的测量过程代码注释的挺清楚,看下就懂了。

然后再看onLayout(),这个和onMeasure()其实挺像的,同样要计算上padding和间隙值,然后就是一个一个算出每个TagView的上下左右坐标,再调用TagView的layout()方法来设置到布局中的相应位置。

这只是初步的代码,还是很简单的,先看下效果:


大体效果是有了,但可以看到在显示长字符串的时候是有问题的,TagView一个只能显示一行,所以太长的字符串显示就要做裁剪。功能都是一点一点加的,后面就是一个个去添加功能了。

裁剪过长的字符串

下面来实现字符串的裁剪,裁剪的方式可能很多样,来简单说下思路:

首先太长的字符串截取前面的部分,并在后面补上3个“.”,就类似省略号;

既然要裁剪就要知道最大可用的布局宽度,这个要从父布局中获取,需要TagGroup提供接口;

最后计算的时候也要算上TagView的padding值,然后一个字符一个字符测量到符合要求;

还是看下代码实际些:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    _adjustText();
}

/**
 * 调整内容,如果超出可显示的范围则做裁剪
 */
private void _adjustText() {
    if (mIsAdjusted) {
        return;
    }
    mIsAdjusted = true;
    // 获取可用宽度
    int availableWidth = ((TagGroup) getParent()).getAvailableWidth();
    mPaint.setTextSize(getTextSize());
    // 计算字符串长度
    float textWidth = mPaint.measureText(String.valueOf(mTagText));
    // 如果可用宽度不够用,则做裁剪处理,末尾不3个.
    if (textWidth + mHorizontalPadding * 2 > availableWidth) {
        float pointWidth = mPaint.measureText(".");
        // 计算能显示的字体长度
        float maxTextWidth = availableWidth - mHorizontalPadding * 2 - pointWidth * 3;
        float tmpWidth = 0;
        StringBuilder strBuilder = new StringBuilder();
        for (int i = 0; i < mTagText.length(); i++) {
            char c = mTagText.charAt(i);
            float cWidth = mPaint.measureText(String.valueOf(c));
            // 计算每个字符的宽度之和,如果超过能显示的长度则退出
            if (tmpWidth + cWidth > maxTextWidth) {
                break;
            }
            strBuilder.append(c);
            tmpWidth += cWidth;
        }
        // 末尾添加3个.并设置为显示字符
        strBuilder.append("...");
        setText(strBuilder.toString());
    }
}
结合上面的思路看应该没什么问题,就是遍历测量每个字符,最后加"..."结尾,再设置给TagView作为显示的字符。需要注意的时,后面要获取完整的TagView字符串需要返回mTagText而不能直接通过getText()方法。还有就是我把_adjustText()放在onMeasure()里调用,如果直接初始化调用会找不到父类。

来看下效果:


好了,字符串太长的问题就解决了,当然了这只是其中的一种方式,也有别的好方法大家自己想。现在功能还很简陋,还有很多东西可以添加,我们先来添加必备的点击监听功能。

点击监听

先在TagView中实现监听器接口OnTagClickListener,并对外提供方法来设置监听器,其实和大部分设置监听器一个样。然后给TagView设置OnClickListener和OnLongClickListener,并来执行OnTagClickListener回调方法。如下:

public OnTagClickListener getTagClickListener() {
    return mTagClickListener;
}

public void setTagClickListener(OnTagClickListener tagClickListener) {
    mTagClickListener = tagClickListener;
}

/**
 * 点击监听器
 */
public interface OnTagClickListener{
    void onTagClick(String text);
    void onTagLongClick(String text);
}

/**
 * 初始化
 * @param context
 */
private void _init(Context context) {
    // 略......
    setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mTagClickListener != null) {
                mTagClickListener.onTagClick(String.valueOf(mTagText));
            }
        }
    });
    setOnLongClickListener(new OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            if (mTagClickListener != null) {
                mTagClickListener.onTagLongClick(String.valueOf(mTagText));
            }
            return true;
        }
    });
}
现在要做的就是通过TagGroup来对外提供OnTagClickListener的设置接口,但是有一点要注意的是,如果你先添加Tags再设置监听器就可能出现前面设置的Tags没办法响应点击,所以你需要在设置监听器的地方为前面设置的Tags都重新添加上监听器,当然了你需要在之前保存好设置过的TagView。代码没什么特别的,简单看下,具体Tags在哪里保存其实想下也知道是在设置的时候保存- -,保存代码就不贴了:

public void setOnTagClickListener(TagView.OnTagClickListener onTagClickListener) {
    mOnTagClickListener = onTagClickListener;
    // 避免先调用设置TagView,后设置监听器导致前面设置的TagView不能响应点击
    for (TagView tagView : mTagViews) {
        tagView.setTagClickListener(mOnTagClickListener);
    }
}
现在点击功能也OK了,再来添加别的功能,比如我们看到的Tag有圆角矩形,也有两边半圆的,下面来给它添加个切换的功能。

Tag模式切换
我们定义3种模式:圆角矩形、两边半圆和直角矩形。来看下关键代码:

// 3种模式:圆角矩形、圆弧、直角矩形
public final static int MODE_ROUND_RECT = 1;
public final static int MODE_ARC = 2;
public final static int MODE_RECT = 3;

public void setTagMode(@TagMode int tagMode) {
    mTagMode = tagMode;
}

@IntDef({MODE_ROUND_RECT, MODE_ARC, MODE_RECT})
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PARAMETER)
public @interface TagMode {}
// ......
@Override
protected void onDraw(Canvas canvas) {
    // 绘制背景
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mBgColor);
    float radius = mRadius;
    if (mTagMode == MODE_ARC) {
        radius = mRect.height() / 2;
    } else if (mTagMode == MODE_RECT) {
        radius = 0;
    }
    canvas.drawRoundRect(mRect, radius, radius, mPaint);
    // 绘制边框
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(mBorderWidth);
    mPaint.setColor(mBorderColor);
    canvas.drawRoundRect(mRect, radius, radius, mPaint);

    super.onDraw(canvas);
}
这里定义了个注解,其实作用相当于枚举,不过比枚举更轻量级,关于这个注解的详细介绍看这里: JAVA ENUM AND ANDROID INTDEF

在onDraw()方法里根据不同的模式来设置半径进行绘制,很简单,剩下的就是TagGroup对外提供接口来设置模式就行了,代码不贴了,来看下效果:


到这基本的东西就说的差不多了,其实还有东西没加上,比如自定义属性,这个想加的话自己加上很容易的。还有很多功能可以添加,比如可以加个编辑模式、多选模式、随机颜色、TagView可拖拽,如果你爱折腾也可以加上各种动画。这些喜欢自己去加,我就不弄了,这里介绍个可拖拽的TagView开源库,我觉得效果挺酷的:AndroidTagView

最后是源代码:TagLayout

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值