转载请注明出处: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. 吴超老师课程--HBASE的集群安装

    1.hbase的机群搭建过程(在原来的hadoop上的hbase伪分布基础上进行搭建)1.1 集群结构,主节点(hmaster)是hadoop,从节点(region server)是hadoop1和h ...

  2. python16_day01【介绍、基本语法、流程控制】

    一.day01 1.二进制运算 60 & 13 =12 60 | 13 =61 60 ^ 13 =49 60<<2 =240 60>>2 =15 2.逻辑运算符 and ...

  3. 菩提树下的杨过.Net 的《hadoop 2.6全分布安装》补充版

    对菩提树下的杨过.Net的这篇博客<hadoop 2.6全分布安装>,我真是佩服的五体投地,我第一次见过教程能写的这么言简意赅,但是又能比较准确表述每一步做法的,这篇博客主要就是在他的基础 ...

  4. 在 Mac OS 上编译 OBS

    本文转自:在 Mac OS 上编译 OBS | www.samirchen.com 安装环境 第一步,做准备工作,安装编译 OBS 所需要的环境,流程如下: // 给当前用户添加 /usr/local ...

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

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

  6. 【hihocoder】三十九周:二分.归并排序之逆序对

    就是用归并排序求数组中得逆序对.假设数组为a:[2 4 5],和b:[1 3],那么在这一次归并的时候逆序对这样求,belement表示当前result数组中b数组对应的元素个数,total表示逆序对 ...

  7. 如何选择合适的MySQL数据类型

    一.MySQL数据类型选择原则 更小的通常更好:一般情况下选择可以正确存储数据的最小数据类型.越小的数据类型通常更快,占用磁盘,内存和CPU缓存更小. 简单就好:简单的数据类型的操作通常需要更少的CP ...

  8. 20145201 《Java程序设计》第三周学习总结

    20145201 <Java程序设计>第三周学习总结 教材学习内容总结 本周学习了课本第四.五章内容,即认识对象和对象封装. 第四章 认识对象 4.1类与对象 定义类 要产生对象必须先定义 ...

  9. 20145109 《Java程序设计》第八周学习总结

    Chapter 15 API java.util.logging package The constructor of Logger class is protected. If Logger ins ...

  10. tomcat启动时出现There are no resources that can be added or removed from the server

    对要部署的项目右键---Properties---Myeclipse---选中Dynamic Web Module 和 Java 应该是项目自己的setting文件夹下的描述信息和.project文件 ...