Android FlowLayout实现热门标签功能

本文介绍了一种使用FlowLayout实现热门标签的方法。通过自定义控件,实现了屏幕宽度充满的布局效果,并考虑了边界值、标签宽度及间距等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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>

如有疑问欢迎留言

点击下载源码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值