自定义标签流水型布局

对于自定义view我们平时在项目中使用的很多,但是我们自己写的却很少,因为现在开源的代码越来越多,使得我们的惰性也变大了。

前段时间朋友找我解决个需求,他们产品需要做一个类似于标签的流水型布局如下图所示:


数据:/#Python#/是一种/#入门快#/、/#功能强大#/、/#高效灵活#/的编程语言,学会之后无论是想进入/#数据分析#/、/#人工智能#/、/#网站开发#/、/#网络安全#/、/#集群运维#/这些领域,还是希望掌握第一门编程语言,都可以用 Python 来开启美好未来的无限可能!

备注:在 "/#" 和 "#/" 中为特殊字符需要进行变小字体、加圆角背景

正常情况下我们使用Textview,组件内部会给我们做到自动换行,现在对于这个需求 我采用的是自定义viewgroup,每一行使用多个TextView拼接的方式来排版,通过计算字符的方式来进行换行。(方式有很多种,希望大家多给提提宝贵意见)

一、初始化

初始化普通字体TextView、特殊字体TextView,我们需要根据他们所对应的的画笔Paint来测量绘画每个字符所需要的长度,我们需要计算每个分隔号好的字符串来做到换行处理。

    /**
     * 初始化
     */
    private void init(Context context) {
        this.context = context;
        //比较普通字体和特殊字体的大小来决定行高(layout排布的时候把小字体的居中显示)
        maxLineHeight =orTextSize >spTextSize ?orTextSize *4 :spTextSize *4;
        //特殊字体
        specialView =new TextView(this.context);
        specialView.setTextSize(spTextSize);
        specialPaint =specialView.getPaint();
        //普通字体
        ordinaryView =new TextView(this.context);
        ordinaryView.setTextSize(orTextSize);
        ordinaryPaint =ordinaryView.getPaint();
    }

二、拆分字体

我是采用多次替换的方式来进行分隔普通字体和特殊字体


    /**
     * 拆分字体
     */
    private String[] split(String data) {
        data = data.replaceAll("/#", "&");
        data = data.replaceAll("#/", "~&");
        String[] split = data.split("&");
        return split;
    }

    /**
     * creat view
     */
    private void startCalc() {
        if (null == data || data.length() == 0) return;
        if (isStart) return;
        String[] split = split(data);
        for (int i = 0; i < split.length; i++) {
            String str = split[i];
            if (null != str && str.length() > 0) {
                calc(str.contains("~"), str);
            }
        }
        invalidate();
        isStart = true;
    }

    /**
     * 计算数据
     */
    private void calc(boolean flag, String data) {
        //特殊字体
        if (flag) {
            data = data.replace("~", "");
            addSpecialText(data);
//            Log.d("TAG-main","特殊字体:"+data);
        // 普通字体
        } else {
            addOrdinaryText(data);
//            Log.d("TAG-main","普通字体:"+data);
        }
    }

三、计算每行可放置几个TextView

这里我通过viewgroup的宽度来计算每行可放置几个view,有一点要考虑的如果剩下的宽度不足以放置下一个view 我们就要根据剩余宽度来计算可放置几个字符来拆分下一个字符串,来拆分成两个或者多个(我使用的是递归的方式)

    /**
     * 添加普通字体
     *
     * @param data
     */
    private void addOrdinaryText(String data) {
        float v = ordinaryPaint.measureText(data);
        //如果字符串宽度小于剩余宽度 直接创建一个textview
        if (v <= lastWidth) {
            lastWidth = lastWidth - v;
            creatView(data);
        //如果字符串宽度大于剩余宽度 需要拆分字符串
        } else {
            String str = getOrdinaryStr(data);
            lastWidth = width;
            creatView(str);
            addOrdinaryText(data.substring(str.length(), data.length()));
        }
    }

    /**
     * 获取剩余控件特殊字体可填入的内容
     *
     * @param data
     * @return
     */
    private String getOrdinaryStr(String data) {
        String str = "";
        char[] chars = data.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            str += chars[i];
            if (ordinaryPaint.measureText(str) >= lastWidth) {
                if (i <= chars.length) {
                    str = data.substring(0, i);
                }
                break;
            }
        }
        return str;
    }

四、测量、排布

我们这里测量的目的是要获取当前viewgroup的宽度,以及做一个自适应的高度,排布的话就是layout函数在摆放子view的位置来达到一个流水型布局的目的。(使用的指定viewgroup具体的宽度数值)

    /**
     * 测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        if (width >0 && mode == MeasureSpec.EXACTLY) {
            this.width = width;
            lastWidth = width;
            startCalc();
        }
        int count = getChildCount();
        int l =0;
        float totalHeight =lineHeight;
        int viweHeight =0;
        int maxLineHeight =0;
        for (int i =0; i < count; i++) {
            View v = getChildAt(i);
            if (v.getVisibility() != View.GONE) {
                measureChild(v, widthMeasureSpec, heightMeasureSpec);
                View childAt = getChildAt(i);
                int viewWidth = childAt.getMeasuredWidth();
                viweHeight = childAt.getMeasuredHeight();
                if (viewWidth > width - l) {
                    totalHeight += maxLineHeight +lineHeight;
                    l =0;
                    maxLineHeight =0;
                }
                if (maxLineHeight ==0) {
                    maxLineHeight = viweHeight;
                }
                if (viweHeight > maxLineHeight) {
                    maxLineHeight = viweHeight;
                }
                if (width - l >0) {
                    l += viewWidth;
                }
            }
        }
        setMeasuredDimension(width, (int) totalHeight + viweHeight);
    }

    /**
     * 排布
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed,int l,int t,int r,int b) {
        int childCount = getChildCount();
        float totalHeight =lineHeight;
        if (childCount ==0) {
            return;
        }
        l =0;
        for (int i =0; i < childCount; i++) {
            View childAt = getChildAt(i);
            int viewWidth = childAt.getMeasuredWidth();
            int viewHeight = childAt.getMeasuredHeight();
            if (viewWidth >width - l) {
                totalHeight +=maxLineHeight +lineHeight;
                l =0;
                maxLineHeight =orTextSize >spTextSize ?orTextSize *4 :spTextSize *4;
            }
            if (viewHeight >maxLineHeight) {
                maxLineHeight = viewHeight;
                t = (int) totalHeight;
                b = (int) totalHeight + viewHeight;
            }else {
                t = (int) totalHeight + ((maxLineHeight - viewHeight) /2);
                b = (int) totalHeight + viewHeight + ((maxLineHeight - viewHeight) /2);
            }
            childAt.layout(l, t, l + viewWidth, b);
            if (width - l >0) {
                l += (viewWidth) ;
            }
        }
    }
我上面大概的讲解了一下我实现这个自定义viewgroup的思路,完整的代码我放到下面了,各位看官自行理解!!!

Java代码:

/**
 * 自定义标签流水型布局
 * create by wxy on 2020/1/13
 */
public class LabelLayout extends ViewGroup {
    //特殊字体属性
    //字体大小
    private int spTextSize = 10;
    //字体颜色
    private int spTextColor = R.color.white_a_color;
    //普通字体属性
    //字体大小
    private int orTextSize = 14;
    //字体颜色
    private int orTextColor = R.color.gray_a_color;
    //行高
    private int lineHeight = 10;
    private Context context;
    private float width;
    private float lastWidth;
    //特殊字体textview
    private TextView specialView;
    //特殊字体textview paint
    private Paint specialPaint;
    //普通字体textview
    private TextView ordinaryView;
    //特殊字体textview paint
    private Paint ordinaryPaint;
    private int maxLineHeight;
    public LabelLayout(Context context) {
        super(context);
        init(context);
    }

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

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

    /**
     * 初始化
     */
    private void init(Context context) {
        this.context = context;
        maxLineHeight = orTextSize > spTextSize ? orTextSize * 4 : spTextSize * 4;
        //特殊字体
        specialView = new TextView(this.context);
        specialView.setTextSize(spTextSize);
        specialPaint = specialView.getPaint();
        //普通字体
        ordinaryView = new TextView(this.context);
        ordinaryView.setTextSize(orTextSize);
        ordinaryPaint = ordinaryView.getPaint();
    }

    /**
     * 测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        if (width > 0 && mode == MeasureSpec.EXACTLY) {
            this.width = width;
            lastWidth = width;
            startCalc();
        }
        int count = getChildCount();
        int l = 0;
        float totalHeight = lineHeight;
        int viweHeight = 0;
        int maxLineHeight = 0;
        for (int i = 0; i < count; i++) {
            View v = getChildAt(i);
            if (v.getVisibility() != View.GONE) {
                measureChild(v, widthMeasureSpec, heightMeasureSpec);
                View childAt = getChildAt(i);
                int viewWidth = childAt.getMeasuredWidth();
                viweHeight = childAt.getMeasuredHeight();
                if (viewWidth > width - l) {
                    totalHeight += maxLineHeight + lineHeight;
                    l = 0;
                    maxLineHeight = 0;
                }
                if (maxLineHeight == 0) {
                    maxLineHeight = viweHeight;
                }
                if (viweHeight > maxLineHeight) {
                    maxLineHeight = viweHeight;
                }
                if (width - l > 0) {
                    l += viewWidth;
                }
            }
        }
        setMeasuredDimension(width, (int) totalHeight + viweHeight);
    }

    /**
     * 排布
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        float totalHeight = lineHeight;
        if (childCount == 0) {
            return;
        }
        l = 0;
        for (int i = 0; i < childCount; i++) {
            View childAt = getChildAt(i);
            int viewWidth = childAt.getMeasuredWidth();
            int viewHeight = childAt.getMeasuredHeight();
            if (viewWidth > width - l) {
                totalHeight += maxLineHeight + lineHeight;
                l = 0;
                maxLineHeight = orTextSize > spTextSize ? orTextSize * 4 : spTextSize * 4;
            }
            if (viewHeight > maxLineHeight) {
                maxLineHeight = viewHeight;
                t = (int) totalHeight;
                b = (int) totalHeight + viewHeight;
            } else {
                t = (int) totalHeight + ((maxLineHeight - viewHeight) / 2);
                b = (int) totalHeight + viewHeight + ((maxLineHeight - viewHeight) / 2);
            }
            childAt.layout(l, t, l + viewWidth, b);
            if (width - l > 0) {
                l += (viewWidth);
            }
        }
    }

    private String data;
    private boolean isStart;
    public void upData(String data) {
        this.data = data;
    }

    /**
     * creat view
     */
    private void startCalc() {
        if (null == data || data.length() == 0) return;
        if (isStart) return;
        String[] split = split(data);
        for (int i = 0; i < split.length; i++) {
            String str = split[i];
            if (null != str && str.length() > 0) {
                calc(str.contains("~"), str);
            }
        }
        invalidate();
//        requestLayout();
        isStart = true;
    }

    /**
     * 计算数据
     */
    private void calc(boolean flag, String data) {
        //特殊字体
        if (flag) {
            data = data.replace("~", "");
            addSpecialText(data);
        //普通字体
        } else {
            addOrdinaryText(data);
        }
    }

    /**
     * 拆分字体
     */
    private String[] split(String data) {
        data = data.replaceAll("/#", "&");
        data = data.replaceAll("#/", "~&");
        String[] split = data.split("&");
        return split;
    }

    /**
     * 添加普通字体
     *
     * @param data
     */
    private void addOrdinaryText(String data) {
        float v = ordinaryPaint.measureText(data);
        //如果字符串宽度小于剩余宽度 直接创建一个textview
        if (v <= lastWidth) {
            lastWidth = lastWidth - v;
            creatView(data);
        //如果字符串宽度大于剩余宽度 需要拆分字符串
        } else {
            String str = getOrdinaryStr(data);
            lastWidth = width;
            creatView(str);
            addOrdinaryText(data.substring(str.length(), data.length()));
        }
    }

    /**
     * 添加特殊字体
     *
     * @param data
     */
    private void addSpecialText(String data) {
        float v = specialPaint.measureText(data) + 20;
        //如果字符串宽度小于剩余宽度 直接创建一个textview
        if (v <= lastWidth) {
            lastWidth = lastWidth - v;
            creatSpView(data);
        //如果字符串宽度大于剩余宽度 需要拆分字符串
        } else {
            String str = getSpecialStr(data);
            lastWidth = width;
            creatSpView(str);
            addSpecialText(data.substring(str.length(), data.length()));
        }
    }

    /**
     * 获取剩余控件特殊字体可填入的内容
     *
     * @param data
     * @return
     */
    private String getSpecialStr(String data) {
        String str = "";
        char[] chars = data.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            str += chars[i];
            if (specialPaint.measureText(str) >= lastWidth) {
                if (i <= chars.length) {
                    str = data.substring(0, i);
                }
                break;
            }
        }
        return str;
    }

    /**
     * 获取剩余控件特殊字体可填入的内容
     *
     * @param data
     * @return
     */
    private String getOrdinaryStr(String data) {
        String str = "";
        char[] chars = data.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            str += chars[i];
            if (ordinaryPaint.measureText(str) >= lastWidth) {
                if (i <= chars.length) {
                    str = data.substring(0, i);
                }
                break;
            }
        }
        return str;
    }

    /**
     * 创建普通字体textview
     *
     * @param data
     */
    private void creatView(String data) {
        if (null == data || data.length() == 0) return;
        int width = (int) ordinaryPaint.measureText(data);
        ordinaryView = new TextView(this.context);
        ordinaryView.setTextSize(orTextSize);
        ordinaryView.setGravity(Gravity.CENTER);
        ordinaryView.setTextColor(getResources().getColor(orTextColor));
        ordinaryView.setWidth(width);
        ordinaryView.setText(data);
        addView(ordinaryView);
    }

    /**
     * 创建特殊字体textview
     *
     * @param data
     */
    private void creatSpView(String data) {
        if (null == data || data.length() == 0) return;
        int width = (int) specialPaint.measureText(data) + 18;
        specialView = new TextView(this.context);
        specialView.setTextSize(spTextSize);
        specialView.setGravity(Gravity.CENTER);
        specialView.setTextColor(getResources().getColor(spTextColor));
        specialView.setBackgroundColor(getResources().getColor(R.color.black_d_color));
        specialView.setWidth(width);
        specialView.setHeight(maxLineHeight);
        specialView.setText(data);
        addView(specialView);
    }
}

XML代码:

<com.hyperx.wlworktools.test.LabelLayout
    android:id="@+id/group_view"
    android:layout_width="240dp"
    android:layout_height="match_parent"
</com.hyperx.wlworktools.test.LabelLayout>

Activity代码:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_cus_group);
        final LabelLayout group_view = findViewById(R.id.group_view);
        String data ="/#Python#/是一种/#入门快#/、/#功能强大#/、/#高效灵活#/的编程语言,学会之后无论是想进入/#数据分析#/、/#人工智能#/、/#网站开发#/、/#网络安全#/、/#集群运维#/这些领域,还是希望掌握第一门编程语言,都可以用 Python 来开启美好未来的无限可能!";
        group_view.upData(data);
    }

感觉有用的同学,动动小手指给个赞,码字不易。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容