Android根据标签长度自动换行

我们在APP中经常看到这样的效果:



这是美团的热门搜索界面,里面罗列出了长度不等的标签,应用会根据标签的长度自动换行,比如第一行有3个标签,而第二行只有2个标签,这篇文章就来讲下如何实现这种效果,首先来看效果图,为了更好的展示不同长度和宽度时的显示效果,这里加了一个红色背景,实际使用时自己去掉即可

1. 当宽度为MATCH_PARENT,高度为WRAP_CONTENT时



2. 当宽度为WRAP_CONTENT,高度为WRAP_CONTENT时



3. 当宽度为指定值时,这里为400dp,高度为WRAP_CONTENT时



4. 当宽度和高度都为指定值时,这里宽度为500dp,高度为200dp



其它情况不一一列出,不管宽高度如何设置,MATCH_PARENT,WRAP_CONTENT或者指定值(当然指定值不能小于实际需要的长度),均可正常工作,下面来说下原理:


这里毫无疑问,用到了自定义控件,这里每个标签都是一个View,所以这个自定义控件是一个ViewGroup,用来管理这里所有的子View。自定义ViewGroup最重要的就是onMeasure和onLayout方法,前者用来测量自定义控件本身的大小,后者用来确定自定义控件中的每个子控件的位置,来看自定义ViewGroup的实现:

1. 继承ViewGroup,声明构造函数

public class TagLayout extends ViewGroup {
    private List<int[]> children;

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

        children = new ArrayList<int[]>();
    }

这里声明了一个变量children,这个变量用来存储每个child的位置,int数组中int[0]为child的left坐标,int[1]为child的top坐标,int[2]为child的right坐标,int[3]为child的bottom坐标,之所以设置这样一个变量存储每个child的位置,是因为在onMeasure中计算自定义控件的大小时,就需要根据所有子控件占据的空间来确定,这时已经算出了子控件的位置,而在onLayout中,再次计算子控件的大小就重复了,所以设置这样一个变量,在onMeasure中赋值,而在onLayout中使用


2. 实现onMeasure方法

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

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        final int count = getChildCount(); // tag的数量
        int left = 0; // 当前的左边距离
        int top = 0; // 当前的上边距离
        int totalHeight = 0; // WRAP_CONTENT时控件总高度
        int totalWidth = 0; // WRAP_CONTENT时控件总宽度

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) child.getLayoutParams();

            if (i == 0) { // 第一行的高度
                totalHeight = params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            if (left + params.leftMargin + child.getMeasuredWidth() + params.rightMargin > getMeasuredWidth()) { // 换行
                left = 0;
                top += params.topMargin + child.getMeasuredHeight() + params.bottomMargin; // 每个TextView的高度都一样,随便取一个都行
                totalHeight += params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            children.add(new int[]{left + params.leftMargin, top + params.topMargin, left + params.leftMargin + child.getMeasuredWidth(), top + params.topMargin + child.getMeasuredHeight()});

            left += params.leftMargin + child.getMeasuredWidth() + params.rightMargin;

            if (left > totalWidth) { // 当宽度为WRAP_CONTENT时,取宽度最大的一行
                totalWidth = left;
            }
        }

        int height = 0;
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
            height = MeasureSpec.getSize(heightMeasureSpec);
        } else {
            height = totalHeight;
        }

        int width = 0;
        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
            width = MeasureSpec.getSize(widthMeasureSpec);
        } else {
            width = totalWidth;
        }

        setMeasuredDimension(width, height);
    }

这个方法有点长,但原理比较简单,首先调用 measureChildren(widthMeasureSpec, heightMeasureSpec);方法,这个方法的作用在于测量每个子控件的大小和模式,因为下面我们需要获取每个子控件的宽高和margin等参数的值,所以必须首先调用该方法。

接下来声明了一些用到的变量left, top, totalHeight, totalWidth等,变量的作用注释已经说明了。

下面进入核心部分,遍历自定义控件的所有子控件,在这里,大家首先需要明白,对自定义控件的长和宽的值,有两种大的情况,一种是MATCH_PARENT或指定值,一种是WRAP_CONTENT,其中前者的值是确定的,我们可以直接通过MeasureSpec来获取,而后者的值是不定的,我们需要自己计算,上面的totalHeight和totalWidth变量,都是针对的后一种情况。

当i == 0,也就是第一个子控件时,我们首先计算totalHeight的值,因为每个子控件的高度都一样,所以就取第一个即可,它的值包括3个部分,自身的高度和上下的边距。

接下来,我们计算当前的left加上我们现在准备添加的子控件的宽度后,是否大于自定义控件的宽度,如果大于,那说明要换行了,这个时候要将left重新赋值为0,因为换行后它的左边距离为0了,并且将top和totalHeight都加上一个子控件的高度,也就是一行的高度。而如果当前的left加上准备添加的子控件的宽度小于自定义控件的宽度,则说明在这行新加一个子控件是没有问题的。

接着,将子控件的位置存储在children中,这里就是每个子控件在自定义控件中的left,top,right,bottom,根据当前已有的left,top以及子控件本身的宽高和margin,很容易计算出来,存储在children中后,就可以在onLayout的时候直接用了。

完后,将left的值加上当前子控件占据的控件,也就是放上新子控件后,left的新位置,同时还要记得比较left和totalWidth的值,当自定义控件的宽度为WRAP_CONTENT时,totalWidth的值为最宽一行的宽度。

下面,当前自定义控件的模式,也就是最开始说的两种大的情况,当模式为EXACTLY时,说明宽高是精确的,直接通过MeasureSpec.getSize取出即可,而否则,就是宽高不固定的情况,这时就要用我们上面定义的totalWidth和totalHeight的值了。

最后,调用setMeasuredDimension方法来确定自定义控件的宽高。

3. 实现onLayout方法

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            int[] position = children.get(i);
            child.layout(position[0], position[1], position[2], position[3]);
        }
    }

在onMeasure方法中,我们已经得到了每个子控件的left, top, right,bottom,所以这里就很简单了,直接调用layout方法确定每个子控件的位置即可。


完整的TagLayout.java方法如下:

public class TagLayout extends ViewGroup {
    private List<int[]> children;

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

        children = new ArrayList<int[]>();
    }

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

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        final int count = getChildCount(); // tag的数量
        int left = 0; // 当前的左边距离
        int top = 0; // 当前的上边距离
        int totalHeight = 0; // WRAP_CONTENT时控件总高度
        int totalWidth = 0; // WRAP_CONTENT时控件总宽度

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) child.getLayoutParams();

            if (i == 0) { // 第一行的高度
                totalHeight = params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            if (left + params.leftMargin + child.getMeasuredWidth() + params.rightMargin > getMeasuredWidth()) { // 换行
                left = 0;
                top += params.topMargin + child.getMeasuredHeight() + params.bottomMargin; // 每个TextView的高度都一样,随便取一个都行
                totalHeight += params.topMargin + child.getMeasuredHeight() + params.bottomMargin;
            }

            children.add(new int[]{left + params.leftMargin, top + params.topMargin, left + params.leftMargin + child.getMeasuredWidth(), top + params.topMargin + child.getMeasuredHeight()});

            left += params.leftMargin + child.getMeasuredWidth() + params.rightMargin;

            if (left > totalWidth) { // 当宽度为WRAP_CONTENT时,取宽度最大的一行
                totalWidth = left;
            }
        }

        int height = 0;
        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
            height = MeasureSpec.getSize(heightMeasureSpec);
        } else {
            height = totalHeight;
        }

        int width = 0;
        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
            width = MeasureSpec.getSize(widthMeasureSpec);
        } else {
            width = totalWidth;
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            int[] position = children.get(i);
            child.layout(position[0], position[1], position[2], position[3]);
        }
    }
}

其它的工作就比较简单了,界面布局activity_main.xml如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/root"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
        >

    <com.my.flowlayout.TagLayout
            android:id="@+id/tags"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#F00">
    </com.my.flowlayout.TagLayout>
</LinearLayout>

注意这里 com.my.flowlayout要改成你自己的包名, android:background="#F00"是我为了清楚看到不同宽高的背景,实际使用中请去掉

主类MainActivity.java如下:

public class MainActivity extends Activity {
    TagLayout mFlowLayout;
    String[] tags = new String[] {"别人家孩子作业做到转钟", "别人家孩子周末都在家学习", "成天就知道玩游戏", "别人上清华了", "比你优秀的人还比你勤奋", "我怎么教出你这么个不争气的败家子", "因为你是小明?"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mFlowLayout = (TagLayout) findViewById(R.id.tags);
        for (int i = 0; i < tags.length; i++) {
            TextView tv = new TextView(this);
            tv.setText(tags[i]);
            tv.setTextColor(Color.BLACK);

            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
            params.setMargins(10, 10, 10, 10);
            tv.setLayoutParams(params);

            tv.setBackgroundResource(R.drawable.text_background);
            mFlowLayout.addView(tv);
        }
    }
}

这里使用了我们自定义的ViewGroup类 TagLayout,完后动态添加TextView,也就是标签, params.setMargins(10, 10, 10, 10);是每个标签的margin的值,可以自行根据实际需要修改,这里还用到了一个背景 text_background.xml,如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="#CCC"/>

    <corners android:radius="5dp"/>

    <padding
            android:bottom="5dp"
            android:left="10dp"
            android:right="10dp"
            android:top="5dp"/>
</shape>

这就是每个标签的背景,没什么好说的,至此就完成了所有的工作。


如果不想复制粘贴,也可以直接下载项目源码下载

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值