Android FlowLayout实现热门标签功能

来源:互联网 发布:如何看待奚梦瑶 知乎 编辑:程序博客网 时间:2024/06/05 05:16

FlowLayout实现热门标签的功能想必大家都见过,有的为搜索的历史记录,有的则是一些推荐等等。总之热门标签在很多应用里面都有使用,先看一下实现的效果图
这里写图片描述
下面的一张是截取的淘宝搜索的效果
这里写图片描述
那么我们如何实现上面的效果呢?我实现的效果是充满屏宽状态的,而淘宝的则是没有充满屏宽的。如何实现充满屏宽其实也不是很难。
下面我们就来探讨一下如何实现:
首页我们需要自定义一个控件也就是我们说的FlowLayout流式布局。实现这样的布局我们要注意以下几点:
1.添加子控件的时候是否超出边界,如果超出边界需要换行。
这里写图片描述
2.就是要考虑我们间距和屏幕的边界值,如果我们最后一个标签的宽度+前面的标签的宽度+前面标签之间的间距=屏幕的宽度,此时是没有最后一个标签与屏幕右边的边距的也就是下面的情况
这里写图片描述
这种情况怎么办呢?就是需要最后一个标签移到下一行显示,剩余的空间有剩余的几个标签平分。
3.如果单独标签的长度过长已经超出屏幕,那么这个标签的宽度就要压缩到跟屏幕的宽度相同。
这里写图片描述
为了防止上面的三种情况的发生我们需要在onMeasure(int widthMeasureSpec, int heightMeasureSpec)这个方法里面获取屏幕的宽度和高度

int availableWidth  = MeasureSpec.getSize(widthMeasureSpec)                - getPaddingRight() - getPaddingLeft();        int availableHeight = MeasureSpec.getSize(heightMeasureSpec)                - getPaddingTop() - getPaddingBottom();        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

以上代码获取的是可用空间的宽和高,因为我们一般设置标签的时候会设置一些边距,关于上面的代码的不懂的可以点击查看自定义控件里面有详解。
获取到屏幕的可用的宽度以后,我们就需要根据我们自己标签的宽度和可用的宽度进行比较。所以我们还需要获取子控件的宽度和高度,实现代码如下

final View child = getChildAt(i);int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(availableWidth ,                widthMode== MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST: widthMode);childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(availableHeight,                heightMode== MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST: heightMode);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

第一种情况的发生,也就是添加子控件的时候超出边界的判断

int childWidth = child.getMeasuredWidth();            mHaveUsedWidth += childWidth;            if (mHaveUsedWidth <= availableWidth ) {                mTagLine.addView(child);// 添加子控件                mHaveUsedWidth += mHSpac;// 加上间隔                if (mHaveUsedWidth >= availableWidth ) {                    addLine();                }            }

上面的代码也包含了第二种情况mHaveUsedWidth >= availableWidth 就是说明如果加上间距等于可用空间,第三种情况的发生,也就是一个子控件的宽度很长已经超出了屏幕的宽度

if (mTagLine.getViewCount() == 0) {    mTagLine.addView(child);    addLine();}else {    addLine();    mTagLine.addView(child);    mHaveUsedWidth += childWidth + mHSpac;    }

判断mTagLine.getViewCount() == 0说明上一行已经被子控件占满,而下个子控件刚好是新的一行的第一个子控件,我们已经知道下一个子控件的长度已经超出了屏幕的宽度,所以下一个子控件肯定是要在新增加的那一行里面,新增加的那一行的子控件的数量当然也就是0了。else里面代码的意思就是此行已经有子控件了,因为这个子控件的长度大于屏幕的宽度,在任何一行上面只要有子控件,不论子控件的长度多小,都需要换行。
mTagLine.addView(child);的作用就是强制这个子控件在这一行。
addLine()的作用就是新增加一行,实现代码如下:

private void addLine() {        mTagLines.add(mTagLine);        mTagLine = new TagLine();        mHaveUsedWidth = 0;    }

TagLine是一个类代表的是一行,具体代码如下:

private class TagLine {        int mAllChildWidth = 0;// 该行中所有的子控件加起来的宽度        int mChildHeight = 0;// 子控件的高度        List<View> viewList= new ArrayList<View>();        public void addView(View view) {// 添加子控件            viewList.add(view);            mAllChildWidth += view.getMeasuredWidth();            int childHeight = view.getMeasuredHeight();            mChildHeight = childHeight;// 行的高度当然是有子控件的高度决定了        }        public int getViewCount() {            return viewList.size();        }        public void layoutView(int left, int top) {            int childCount = getViewCount();            //除去左右边距后可以使用的宽度            int validWidth= getMeasuredWidth() - getPaddingLeft()                    - getPaddingRight();            // 除了子控件以及子控件之间的间距后剩余的空间            int remainWidth = validWidth- mAllChildWidth - mHSpac                    * (childCount - 1);            if (remainWidth >= 0) {                int divideSpac = (int) (remainWidth / childCount + 0.5);                for (int i = 0; i < childCount; i++) {                    final View view = viewList.get(i);                    int childWidth = view.getMeasuredWidth();                    int childHeight = view.getMeasuredHeight();                    // 把剩余的空间平均分配到每个子控件上面                    childWidth = childWidth + divideSpac;                    view.getLayoutParams().width = childWidth;                    // 由于平均分配剩余空间导致子控件的长度发生了变化,需要重新测量                    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(                                childWidth, MeasureSpec.EXACTLY);                        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(                                childHeight, MeasureSpec.EXACTLY);                        view.measure(widthMeasureSpec, heightMeasureSpec);                    // 设置子控件的位置                    view.layout(left, top, left + childWidth, top                             + childHeight);                    left += childWidth + mHSpac; // 获取到的left值是下一个子控件的左边所在的位置                }            } else {                if (childCount == 1) {//这一种就是一行只有一个子控件的情况                    View view = viewList.get(0);                    view.layout(left, top, left + view.getMeasuredWidth(), top                            + view.getMeasuredHeight());                }             }        }    }

当然了,有时候标签平分后感觉控件不是那么的美观,不想平分剩下的空间怎么办?也就是下面的情况
这里写图片描述
要想实现上面的效果很简单,就是去掉两行代码即可。这两行代码就是关于平分剩余空间的

int divideSpac = (int) (remainWidth / childCount + 0.5);childWidth = childWidth + divideSpac;

divideSpac 的值就是剩余的空间平分到每个控件的值,childWidth 这个值就是自身的宽度加上平分的空间,其实就是我们所看到的平分后的控件的宽度。
下面就是全部的代码了
MainActivity 类

package com.lyxrobert.flowlayout;import android.app.Activity;import android.graphics.Color;import android.graphics.drawable.StateListDrawable;import android.os.Bundle;import android.util.TypedValue;import android.view.Gravity;import android.view.View;import android.widget.TextView;public class MainActivity extends Activity {    private FlowLayout flowLayout;    private ClearEditText et_clear;    private String[] data =  new String[]{"全部","这是","测试标签",        "这是测试标签","FlowLayout","衣服","鞋子",        "春","夏","深秋","寒冬",        "测一下看看效果如何","心情还不错哦","这是测试标签","这是测试标签",        "这是测试标签","受益匪浅啊","123456789","电话号码"};    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initView();        initData();        }    private void initView() {        flowLayout = (FlowLayout) findViewById(R.id.fl);        et_clear = (ClearEditText) findViewById(R.id.et_clear);    }    private void initData() {        int padding = dip2px(5);        flowLayout.setPadding(padding, padding, padding, padding);// 设置内边距        for (int i = 0; i < data.length; i++) {            final String tag = data[i];            TextView tv = new TextView(this);            tv.setText(tag);            tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);            tv.setPadding(padding, padding, padding, padding);            tv.setGravity(Gravity.CENTER);            int color = 0xffcecece;// 按下后偏白的背景色            StateListDrawable selector;            if (i==0){                tv.setTextColor(Color.WHITE);                tv.setEnabled(false);              selector = DrawableUtils.getSelector(false,Color.parseColor("#2c90d7"), color, dip2px(30));            }else {                selector = DrawableUtils.getSelector(true,Color.WHITE, color, dip2px(30));            }            tv.setBackgroundDrawable(selector);            flowLayout.addView(tv);            tv.setOnClickListener(new View.OnClickListener() {                @Override                public void onClick(View v) {                    et_clear.setText(tag);                    et_clear.setSelection(tag.trim().length());                }            });    }}    public int dip2px(float dip) {        float density = this.getResources().getDisplayMetrics().density;        return (int) (dip * density + 0.5f);    }}

FlowLayout类

package com.lyxrobert.flowlayout;import java.util.ArrayList;import java.util.List;import android.content.Context;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;public class FlowLayout extends ViewGroup {    /** 横向间隔 */    private int mHSpac = 0;    /** 纵向间隔 */    private int mVSpac = 0;    /** 当前行已用的宽度*/    private int mHaveUsedWidth = 0;    /** 每一行的集合 */    private final List<TagLine> mTagLines = new ArrayList<TagLine>();    private TagLine mTagLine = null;    public FlowLayout(Context context) {        super(context);    }    public FlowLayout(Context context, AttributeSet attrs) {        super(context, attrs);        setHorizontalSpacing(dip2px(5));        setVerticalSpacing(dip2px(5));    }    public int dip2px(float dip) {        float density = this.getResources().getDisplayMetrics().density;        return (int) (dip * density + 0.5f);    }    public void setHorizontalSpacing(int spacing) {        if (mHSpac != spacing) {            mHSpac = spacing;            requestLayout();        }    }    public void setVerticalSpacing(int spacing) {        if (mVSpac != spacing) {            mVSpac = spacing;            requestLayout();        }    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int availableWidth  = MeasureSpec.getSize(widthMeasureSpec)                - getPaddingRight() - getPaddingLeft();        int availableHeight = MeasureSpec.getSize(heightMeasureSpec)                - getPaddingTop() - getPaddingBottom();        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        resetLine();// 将行的状态重置为最原始的状态,因为新的一行的数据跟以往的无关        final int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            final View child = getChildAt(i);            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(availableWidth ,                    widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST                            : widthMode);            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(                    availableHeight,                    heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST                            : heightMode);            // 测量子控件            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);            if (mTagLine == null) {                mTagLine = new TagLine();            }            int childWidth = child.getMeasuredWidth();            mHaveUsedWidth += childWidth;// 增加使用的宽度            if (mHaveUsedWidth <= availableWidth ) {// 已经使用的宽度小于可用宽度,说明还有剩余空间,该子控件添加到这一行。                mTagLine.addView(child);// 添加子控件                mHaveUsedWidth += mHSpac;// 加上间距                if (mHaveUsedWidth >= availableWidth ) {// 加上间距后已经使用的宽度大于等于可用宽度,说明这一行已满或者已经超出需要换行                    addLine();                }            } else {                //说明上一行已经被子控件占满,而下个子控件刚好是新的一行的第一个子控件                if (mTagLine.getViewCount() == 0) {                    mTagLine.addView(child);                    addLine();                } else {                    //因为这个子控件的长度大于屏幕的宽度,在任何一行上面只要有子控件,不论子控件的长度多小,都需药换行                    addLine();                    mTagLine.addView(child);                    mHaveUsedWidth += childWidth + mHSpac;                }            }        }        if (mTagLine != null && mTagLine.getViewCount() > 0                && !mTagLines.contains(mTagLine)) {            //此段代码的作用是为了防止因最后一行代码的子控件未占满空间,但是毕竟也是一行,所以也要添加到行的集合里面            mTagLines.add(mTagLine);        }        int totalWidth = MeasureSpec.getSize(widthMeasureSpec);        int totalHeight = 0;        final int size = mTagLines.size();        for (int i = 0; i < size; i++) {// 加上所有行的高度            totalHeight += mTagLines.get(i).mChildHeight;        }        totalHeight += mVSpac * (size - 1);// 加上所有间距的高度        totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding        setMeasuredDimension(totalWidth,                resolveSize(totalHeight, heightMeasureSpec));    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {            int left = getPaddingLeft();// 获取最初的左上点            int top = getPaddingTop();            final int linesCount = mTagLines.size();            for (int i = 0; i < linesCount; i++) {                final TagLine oneLine = mTagLines.get(i);                oneLine.layoutView(left, top);// 设置每一行所在的位置                top += oneLine.mChildHeight + mVSpac;// 这个top的值其实就是下一个的上顶点值            }        }    /** 将行的状态重置为最原始的状态*/    private void resetLine() {        mTagLines.clear();        mTagLine = new TagLine();        mHaveUsedWidth = 0;    }    /** 新增加一行 */    private void addLine() {        mTagLines.add(mTagLine);            mTagLine = new TagLine();            mHaveUsedWidth = 0;    }    /**     * 代表着一行,封装了一行所占高度,该行子View的集合,以及所有View的宽度总和     */    private class TagLine {        int mAllChildWidth = 0;// 该行中所有的子控件加起来的宽度        int mChildHeight = 0;// 子控件的高度        List<View> viewList= new ArrayList<View>();        public void addView(View view) {// 添加子控件            viewList.add(view);            mAllChildWidth += view.getMeasuredWidth();            int childHeight = view.getMeasuredHeight();            mChildHeight = childHeight;// 行的高度当然是有子控件的高度决定了        }        public int getViewCount() {            return viewList.size();        }        public void layoutView(int left, int top) {            int childCount = getViewCount();            //除去左右边距后可以使用的宽度            int validWidth= getMeasuredWidth() - getPaddingLeft()                    - getPaddingRight();            // 除了子控件以及子控件之间的间距后剩余的空间            int remainWidth = validWidth- mAllChildWidth - mHSpac                    * (childCount - 1);            if (remainWidth >= 0) {                int divideSpac = (int) (remainWidth / childCount + 0.5);                for (int i = 0; i < childCount; i++) {                    final View view = viewList.get(i);                    int childWidth = view.getMeasuredWidth();                    int childHeight = view.getMeasuredHeight();                    // 把剩余的空间平均分配到每个子控件上面                    childWidth = childWidth + divideSpac;                    view.getLayoutParams().width = childWidth;                    // 由于平均分配剩余空间导致子控件的长度发生了变化,需要重新测量                    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(                            childWidth, MeasureSpec.EXACTLY);                    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(                            childHeight, MeasureSpec.EXACTLY);                    view.measure(widthMeasureSpec, heightMeasureSpec);                    // 设置子控件的位置                    view.layout(left, top, left + childWidth, top                            + childHeight);                    left += childWidth + mHSpac; // 获取到的left值是下一个子控件的左边所在的位置                }            } else {                if (childCount == 1) {//这一种就是一行只有一个子控件的情况                    View view = viewList.get(0);                    view.layout(left, top, left + view.getMeasuredWidth(), top                            + view.getMeasuredHeight());                }            }        }    }}

布局文件

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    >    <com.lyxrobert.flowlayout.ClearEditText        android:id="@+id/et_clear"        android:layout_margin="10dp"        android:paddingLeft="15dp"        android:paddingRight="15dp"        android:textSize="18dp"        android:textColor="@android:color/white"        android:background="@drawable/search_bg"        android:layout_width="match_parent"        android:layout_height="40dp"/>    <ScrollView        android:layout_width="match_parent"        android:layout_height="match_parent">    <com.lyxrobert.flowlayout.FlowLayout        android:id="@+id/fl"        android:layout_width="match_parent"        android:layout_height="match_parent"    >    </com.lyxrobert.flowlayout.FlowLayout>    </ScrollView></LinearLayout>

如有疑问欢迎留言

扫一扫关于个人公众号

这里写图片描述

点击下载源码

4 0
原创粉丝点击