转载请注明出处:http://blog.csdn.net/crazy1235/article/details/74907150


标签瀑布流布局!

实现方式有很多种。

  • 继承LinearLayout

  • 继承ViewGroup

  • 继承别的布局…


继承LinearLayout

继承LinearLayout相对来说,实现比较简单!不需要自己处理onMeasure() 和 onLayout() 函数!

整个布局设置成 LinearLayout.VERTICAL 排列模式!

然后每一行作为一个子LinearLayout,通过 addView(LinearLayout)进去!子LinearLayout是 LinearLayout.HORIZONTAL 排列模式!

当一行addView(tag)的时候超过了最外层的宽度,则需要另起一行!


继承ViewGroup

继承ViewGroup的情况,比较麻烦。需要自己处理onMeasure()和onLayout() !

自定义一套TagGroup 需要注意的地方:

  • 标签的margin值

  • 标签的padding值

  • TagGroup的padding值

  • 标签的行间距

  • 标签的列间距

  • 字体大小

  • 字体颜色

  • 标签按下的状态

  • 选中标签

  • ……


SuperTagGroup

首先是,属性定义:


    <declare-styleable name="SuperTagGroup">

        <attr name="horizontal_spacing" format="dimension" />
        <attr name="vertical_spacing" format="dimension" />
        <attr name="tag_horizontal_padding" format="dimension" />
        <attr name="tag_vertical_padding" format="dimension" />
        <attr name="tag_text_size" format="dimension" />
        <attr name="tag_text_color" format="color" />
        <attr name="tag_corner_radius" format="dimension" />
        <attr name="tag_border_width" format="dimension" />
        <attr name="tag_border_color" format="color" />
        <attr name="tag_border_checked_color" format="color" />
        <attr name="tag_bg_color" format="color" />
        <attr name="tag_bg_checked_color" format="color" />
        <attr name="tag_bg_drawable" format="reference" />
        <attr name="max_selected_count" format="integer" />

    </declare-styleable>

然后在构造函数中,读取出这些属性

public SuperTagGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SuperTagGroup, defStyleAttr, R.style.SuperTagGroup);

        horizontalSpace = ta.getDimension(R.styleable.SuperTagGroup_horizontal_spacing, 0);
        verticalSpace = ta.getDimension(R.styleable.SuperTagGroup_vertical_spacing, 0);

        horizontalPadding = (int) ta.getDimension(R.styleable.SuperTagGroup_tag_horizontal_padding, 0);
        verticalPadding = (int) ta.getDimension(R.styleable.SuperTagGroup_tag_vertical_padding, 0);

        textSize = ta.getDimension(R.styleable.SuperTagGroup_tag_text_size, 0);
        textColor = ta.getColor(R.styleable.SuperTagGroup_tag_text_color, 0);

        cornerRadius = ta.getDimension(R.styleable.SuperTagGroup_tag_corner_radius, 0);

        borderWidth = ta.getDimension(R.styleable.SuperTagGroup_tag_border_width, 0);
        borderColor = ta.getColor(R.styleable.SuperTagGroup_tag_border_color, 0);
        tagBorderCheckedColor = ta.getColor(R.styleable.SuperTagGroup_tag_border_checked_color, 0);

        tagBgColor = ta.getColor(R.styleable.SuperTagGroup_tag_bg_color, 0);
        tagBgCheckedColor = ta.getColor(R.styleable.SuperTagGroup_tag_bg_checked_color, 0);

        tagBgDrawable = ta.getResourceId(R.styleable.SuperTagGroup_tag_bg_drawable, 0);

        maxSelectedNum = ta.getInt(R.styleable.SuperTagGroup_max_s    ta.recycle();

        init();
    }

接着就是重要的onMeasure() 函数

用来对自身布局和子view进行测量,得到测量宽高来进行设置

主要就是针对每个子view进行测量,注意过程中要加上我们设置的padding值,和spacing值!

针对各个子view测量完毕之后,再加上布局的padding值,即可得到布局的宽高!

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("SuperTagGroup", "onMeasure");
        // width size & mode
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        // height size & mode
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // padding
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        // the width & height final result
        int resultWidth = 0;
        int resultHeight = 0;

        int lineWidth = 0;
        int lineHeight = 0;

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            if (!(child instanceof SuperTagView)) {
                throw new IllegalStateException("SuperTagGroup can only has SuperTagView child");
            }

            // measure child
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            // 这里要记得加上子view的margin值
//            MarginLayoutParams childLayoutParams = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            lineHeight = Math.max(childHeight, lineHeight);

            if (lineWidth + childWidth > widthSize - paddingLeft - paddingRight) { // 需要换一行
                resultWidth = Math.max(resultWidth, lineWidth); // 每一行都进行比较,最终得到最宽的值
                resultHeight += verticalSpace + lineHeight;

                lineWidth = (int) (childWidth + horizontalSpace); // 新的一行的宽度
                lineHeight = childHeight; // 新的一行的高度
            } else {
                // 当前行的宽度
                lineWidth += childWidth + horizontalSpace;
                // 当前行最大的高度
                lineHeight = Math.max(lineHeight, childHeight);
            }

            // 最后一个, 需要再次比较宽
            if (i == getChildCount() - 1) {
                resultWidth = Math.max(resultWidth, lineWidth);
            }
        }

        resultWidth += paddingRight + paddingLeft;
        // 布局最终的高度
        resultHeight += lineHeight + paddingBottom + paddingTop;

        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : resultWidth, heightMode == MeasureSpec.EXACTLY ? heightSize : resultHeight);
    }

onLayout() 是用来确定各个子view的四个点的位置

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // padding
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();

        int lineHeight = 0;

        // 子view的左侧和顶部位置
        int childLeft = paddingLeft;
        int childTop = paddingTop;

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                continue;
            }

            // 找到最后一个append tag
            if (((SuperTagView) child).isAppendTag()) {
                if (appendTagIndex != -1 && latestAppendTagView != null) {
                    latestAppendTagView.setAppendTag(false);
                }
                appendTagIndex = i;
                ((SuperTagView) child).setAppendTag(true);
                latestAppendTagView = (SuperTagView) child;
            }

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            lineHeight = Math.max(lineHeight, childHeight);

            if (childLeft + childWidth + paddingRight > getWidth()) { // 需要换行
                childLeft = paddingLeft;
                childTop += lineHeight + verticalSpace;

                lineHeight = childHeight;
            }

            // 布局
            child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);

            // 计算下一个子view X的位置
            childLeft += childWidth + horizontalSpace;
        }
    }

SuperTagView

针对SuperTagGroup设计了这样一个自定义子view。可以直接在xml布局中添加tag,当然也可以动态的添加删除tag!

还是首先来看 属性定义。

<declare-styleable name="SuperTagView">

        <attr name="is_append_tag" format="boolean" />
        <attr name="horizontal_padding" format="dimension" />
        <attr name="vertical_padding" format="dimension" />
        <attr name="corner_radius" format="dimension" />
        <attr name="border_width" format="dimension" />
        <attr name="border_color" format="color" />
        <attr name="border_checked_color" format="color" />
        <attr name="bg_color" format="color" />
        <attr name="bg_checked_color" format="color" />

    </declare-styleable>

然后在构造函数中读取属性

public SuperTagView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SuperTagView, defStyleAttr, R.style.SuperTagGroup_TagView);

        isAppendTag = ta.getBoolean(R.styleable.SuperTagView_is_append_tag, false);

        horizontalPadding = (int) ta.getDimension(R.styleable.SuperTagView_horizontal_padding, SuperTagUtil.dp2px(context, SuperTagUtil.DEFAULT_HORIZONTAL_PADDING));
        verticalPadding = (int) ta.getDimension(R.styleable.SuperTagView_vertical_padding, SuperTagUtil.dp2px(context, SuperTagUtil.DEFAULT_VERTICAL_PADDING));

        cornerRadius = ta.getDimension(R.styleable.SuperTagView_corner_radius, 0);

        borderWidth = ta.getDimension(R.styleable.SuperTagView_border_width, 0);

        borderColor = ta.getColor(R.styleable.SuperTagView_border_color, SuperTagUtil.DEFAULT_TAG_BORDER_COLOR);
        bgColor = ta.getColor(R.styleable.SuperTagView_bg_color, SuperTagUtil.DEFAULT_TAG_BG_COLOR);

        borderCheckedColor = ta.getColor(R.styleable.SuperTagView_border_checked_color, 0);
        bgCheckedColor = ta.getColor(R.styleable.SuperTagView_bg_checked_color, 0);

        ta.recycle();

        init();
    }

这里需要注意,不仅要绘制背景色还要绘制边框。还要考虑到选中标签的效果!

所以使用了 StateListDrawable

根据背景色,边框色,选中的颜色来创建StateListDrawable作为背景

private Drawable generateBackgroundDrawable() {
        StateListDrawable stateListDrawable = new StateListDrawable();
        stateListDrawable.addState(CHECK_STATE, new TagBgDrawable(bgCheckedColor, bgRectF, borderCheckedColor, borderRectF, borderWidth, cornerRadius));
        stateListDrawable.addState(new int[]{}, new TagBgDrawable(bgColor, bgRectF, borderColor, borderRectF, borderWidth, cornerRadius));
        return stateListDrawable;
    }
@Override
    protected int[] onCreateDrawableState(int extraSpace) {
        int[] states = super.onCreateDrawableState(extraSpace + CHECK_STATE.length);
        if (isChecked()) {
            mergeDrawableStates(states, CHECK_STATE);
        }
        return states;
    }

    @Override
    public void setChecked(boolean checked) {
        if (this.isChecked != checked) {
            this.isChecked = checked;
            refreshDrawableState();
        }
    }

    @Override
    public boolean isChecked() {
        return isChecked;
    }

    @Override
    public void toggle() {
        setChecked(!isChecked);
    }

效果图


  • 动态添加删除tag

  • 最多选中一个tag

  • 最多选中5个tag

  • 不限选中个数

  • 嵌套ScrollView的使用情况


引用开源库

compile 'com.jacksen:supertaggroup:1.0' 

gradle 4.0 版本之后,使用下面依赖方式

implementation 'com.jacksen:supertaggroup:1.0'

源码

https://github.com/crazy1235/SuperTagGroup

欢迎star


参考

https://github.com/zhuanghongji/FlowLayout

https://github.com/PepernotenX/FlowLayout

自定义控件之TagGroup的更多相关文章

  1. android自定义控件一站式入门

    自定义控件 Android系统提供了一系列UI相关的类来帮助我们构造app的界面,以及完成交互的处理. 一般的,所有可以在窗口中被展示的UI对象类型,最终都是继承自View的类,这包括展示最终内容的非 ...

  2. ASP.NET MVC学习之母版页和自定义控件的使用

    一.母板页_Layout.cshtml类似于传统WebForm中的.master文件,起到页面整体框架重用的目地1.母板页代码预览 <!DOCTYPE html> <html> ...

  3. C# 自定义控件VS用户控件

    1 自定义控件与用户控件区别 WinForm中, 用户控件(User Control):继承自 UserControl,主要用于开发 Container 控件,Container控件可以添加其他Con ...

  4. 自定义控件之 圆形 / 圆角 ImageView

    一.问题在哪里? 问题来源于app开发中一个很常见的场景——用户头像要展示成圆的:       二.怎么搞? 机智的我,第一想法就是,切一张中间圆形透明.四周与底色相同.尺寸与头像相同的蒙板图片,盖在 ...

  5. 如何开发FineReport的自定义控件?

    FineReport作为插件化开发的报表软件,有些特殊需求的功能需要自己开发,开发的插件包帆软官方有提提供,可以去帆软论坛上找,本文将主要介绍如何开发一个自定义控件,这里讲讲方法论. 第一步:实例化一 ...

  6. WPF自定义控件第二 - 转盘按钮控件

    继之前那个控件,又做了一个原理差不多的控件.这个控件主要模仿百度贴吧WP版帖子浏览界面左下角那个弹出的按钮盘.希望对大家有帮助. 这个控件和之前的也差不多,为了不让大家白看,文章最后发干货. 由于这个 ...

  7. 【Win 10应用开发】AdaptiveTrigger在自定义控件中是可以触发的

    前些天,看到有网友给我留言,说AdaptiveTrigger在自定义控件(模板化控件)中不能触发.因为当时我正在写其他的代码,就没有去做实验来验证,于是我就给这位网友提了使用GotoVisualSta ...

  8. WPF自定义控件与样式(3)-TextBox & RichTextBox & PasswordBox样式、水印、Label标签、功能扩展

    一.前言.预览 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要是对文本 ...

  9. Android自定义控件之自定义ViewGroup实现标签云

    前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...

随机推荐

  1. Delphi 正则表达式语法(1): 关于大小写与中文

    Delphi 正则表达式语法(1): 关于大小写与中文 //替换一般字符串 var   reg: TPerlRegEx; begin   reg := TPerlRegEx.Create(nil); ...

  2. Kotlin学习记录3

    参考我的博客:http://www.isedwardtang.com/2017/09/04/kotlin-primer-3/

  3. wpf利用线程制作初始界面和关闭窗体特效

    1.首先定义初始窗体,和主窗体. 初始窗体(StartWindow) 主窗体(MainWindow): 2.在主窗体界面中,加载初始窗体.注意在线程中操作UI元素需要使用BeginInvoke或者In ...

  4. xaml可扩展应用程序标记语言

    xaml 类似于 html,但不是html,它是基于xml语言的:’html可以呈现在浏览器中而xaml 可以现实 3d动画等特效. xaml  是强类型语言,  是解释性语言,虽然他可以被编译.

  5. Tomcat 优化相关知识

    ---------(Tomcat Listener)----------- Tomcat 性能的因素是内存泄露.Server标签中可以配置多个Listener,其中 JreMemoryLeakPrev ...

  6. zabbix监控php-fpm的性能

    zabbix监控php-fpm主要是通过nginx配置php-fpm的状态输出页面,在正则取值 要nginx能输出php-fpm的状态必须要先修改php-fpm的配置,这个配置没有开启nginx 就没 ...

  7. Cnblog页面美化小记

    Cnblog页面美化小记 这两天我在网上翻找了许许多多的资料,打开了不计其数的博客,对着\(js\).\(html\).\(css\)等文件删删改改,在浏览器和\(vscode\)间辗转腾挪...总算 ...

  8. Bellman-Ford算法优化

    2017-07-27 16:02:48 writer:pprp 在BEllman-Ford算法中,其最外层的循环的迭代次数为n-1,如果不存在负权回路,需要迭代的次数是远远小于n-1; 如果在某一次迭 ...

  9. 完全理解Android中的RemoteViews

    一.什么是RemoteViews RemoteViews翻译过来就是远程视图.顾名思义,RemoteViews不是当前进程的View,是属于SystemServer进程.应用程序与RemoteView ...

  10. 你知道uwsgi???

    第一步,打开https://www.google.com.hk 第二步,输入how to restart uwsgi