在日常的app使用中,我们会在Android 的app中看见,比如淘宝购物页面尺寸的选取,脉脉和慕课技术职位的选取等等热门标签自动换行的流式布局,今天,我们就来看看如何自定义一个类似热门标签那样的流式布局吧,老规矩,直接上效果图
在布局内,随意摆放任意个view,每行所摆放的view个数,根据实施计算出来的宽度,一旦当前要摆放的view宽度和之前摆放的所有view宽度加在一起,超过了布局的宽度,那么就把该view换行摆放
一般,像这种流式布局会应用在一些热门标签,热门推荐之类的应用上
谈到FlowLayout流式布局,不得不提及他的测量模式:
1:如果布局指定的宽是match_parent或者精确的宽度值,那么直接就可以从父控件传入的测量规格中直接获取布局宽度,高度同理.
2:如果布局指定的宽高不是EXACTLY,而是AT_MOST,那么这时候,就需要计算每一个子view的宽高,来决定布局的宽高了。
宽度:摆放的所有子view占据宽度最多的一行,作为布局宽度。
高度:摆放的所有子view总共占据几行的高度总和。
使用onLayout():设置ViewGroup内包含的所有子view的位置;
获取到每一行的每一个子view,计算出它的left,top,right,bottom,调用layout方法设置其在流式布局当中的位置。
宽度=子view最多的那行的宽度=那一行每一个子view的宽度+leftMargin+rightMargin;
高度=所有行的高度 = 每一行的高度+topMargin+bottomMargin;
ViewGroup LayoutParams :每个 ViewGroup 对应一个 LayoutParams; 即 ViewGroup -> LayoutParams
getLayoutParams 不知道转为哪个对应的LayoutParams ,其实很简单,就是如下:
子View.getLayoutParams 得到的LayoutParams对应的就是 子View所在的父控件的LayoutParams;
例如,LinearLayout 里面的子view.getLayoutParams ->LinearLayout.LayoutParams
所以 咱们的FlowLayout 也需要一个LayoutParams,由于上面的效果图是子View的 margin,
所以应该使用MarginLayoutParams。即FlowLayout->MarginLayoutParams
根据上面的技术分析,自定义类继承于ViewGroup,并重写 onMeasure和onLayout等方法。具体实现代码如下:
package com.example.administrator.p2pinvest.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
//自定义ViewGroup实现
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
this(context, null);
}
//这个方法必须实现
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//布局:给每一个子view布局,childView.layout(l,t,r,b)
private List allHeights = new ArrayList<>();//集合中的元素:记录每一行的高度
private List> allViews = new ArrayList<>();//外层集合中的元素:由每行元素构成的集合
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = this.getWidth();//得到父视图的宽度
int lineWidth = 0;
int lineHeight = 0;
// 一、给集合元素赋值
int childCount = getChildCount();
List lineList = new ArrayList<>();//一行元素构成的集合
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//子视图的宽高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//获取视图的边距
MarginLayoutParams mp = (MarginLayoutParams) childView.getLayoutParams();
if (lineWidth + childWidth + mp.leftMargin + mp.rightMargin < width) {//不换行
lineList.add(childView);//添加子视图到集合中
lineWidth += childWidth + mp.leftMargin + mp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + mp.topMargin + mp.bottomMargin);
} else {//换行
allViews.add(lineList);
allHeights.add(lineHeight);
//换行以后需要执行的情况
lineList = new ArrayList<>();
lineList.add(childView);
lineWidth = childWidth + mp.leftMargin + mp.rightMargin;
lineHeight = childHeight + mp.topMargin + mp.bottomMargin;
}
if (i == childCount - 1) {//如果最后一个元素
allViews.add(lineList);
allHeights.add(lineHeight);
}
}
Log.e("TAG", "allViews.size()==" + allViews.size() + "allHeights.size()==" + allHeights.size());
//二、遍历集合元素,调用元素的layout()
int x = 0;
int y = 0;
for (int i = 0; i < allViews.size(); i++) {
List lineViews = allViews.get(i);//获取每一行的集合
for (int j = 0; j < lineViews.size(); j++) {
View childView = lineViews.get(j);//获取一行的指定的j位置
MarginLayoutParams mp = (MarginLayoutParams) childView.getLayoutParams();
//计算的到left,top,right,bottom
int left = x + mp.leftMargin;
int top = y + mp.topMargin;
int right = left + childView.getMeasuredWidth();
int bottom = top + childView.getMeasuredHeight();
childView.layout(left, top, right, bottom);
//重新赋值x,y
x += childView.getMeasuredWidth() + mp.leftMargin + mp.rightMargin;
}
//换行
x = 0;
y += allHeights.get(i);
}
}
//测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽度和高度的布局的数值,以及各自的设计模式,精确模式,至多模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//声明当前视图的宽和高,如果是至多模式,需要计算出此两个变量的值
int width = 0;
int height = 0;
//声明每行的宽度和高度
int lineWidth = 0;
int lineHeight = 0;
int childCount = getChildCount();//获取子视图的个数
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//为了保证能够获取子视图的测量的宽高,需要调下面的方法
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//获取子视图测量的宽高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//获取视图的边距
MarginLayoutParams mp = (MarginLayoutParams) childView.getLayoutParams();
if (lineWidth + childWidth + mp.leftMargin + mp.rightMargin <= widthSize) {//不换行
lineWidth += childWidth + mp.leftMargin + mp.rightMargin;
lineHeight = Math.max(lineHeight, childHeight + mp.topMargin + mp.bottomMargin);
} else {//换行
width = Math.max(width, lineWidth);
height += lineHeight;
//重新赋值
lineWidth = childWidth + mp.leftMargin + mp.rightMargin;
lineHeight = childHeight + mp.topMargin + mp.bottomMargin;
}
//单独的考虑一下最后一个!因为最后一个元素并没有计算进去
if (i == childCount - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}
Log.e("TAG", "width ==" + width + ",height==" + height);
Log.e("TAG", "widthSize ==" + widthSize + ",heightSize==" + heightSize);
//调用此方法,设置当前布局的宽高
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
//FlowLayout中有了如下的方法,在onMeasure()中可通过child就可以getLayoutParams()
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
MarginLayoutParams mp = new MarginLayoutParams(getContext(), attrs);
return mp;
}
}
在布局文件中加入自定义的flowLayout
初始化布局使用的是butterknife
@Bind(R.id.flow_layout)
FlowLayout flowLayout;
提供页面要显示的数据,这个数据也可以放在服务器中进行联网获取
private String[] datas = new String[]{"新手计划", "乐享活系列90天计划", "钱包", "30天理财计划(加息2%)",
"林业局投资商业经营与大捞一笔", "中学老师购买车辆", "屌丝下海经商计划", "新西游影视拍",
"Java培训老师自己周转", "HelloWorld", "C++-C-ObjectC-java", "Android vs ios", "算法与数据结构", "JNI与NDK", "team working"};
//初始化随机
private Random random;
在其他类中直接进行调用即可
@Override
public void initData(String content) {
random = new Random();
for(int i = 0; i < datas.length; i++) {
final TextView textView = new TextView(getActivity());
textView.setText(datas[i]);
//提供边距的对象,并设置到textView中
ViewGroup.MarginLayoutParams mp = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
mp.leftMargin = UIUtils.dp2px(8);
mp.rightMargin = UIUtils.dp2px(8);
mp.topMargin = UIUtils.dp2px(8);
mp.bottomMargin = UIUtils.dp2px(8);
textView.setLayoutParams(mp);
//设置背景
//设置textView的背景
int red = random.nextInt(211);
int green = random.nextInt(211);
int blue = random.nextInt(211);
//方式一:
// textView.setBackground(DrawUtils.getDrawable(Color.rgb(red, green, blue),UIUtils.dp2px(5)));
//方式二:
//保存按下能显示selector的效果,需要设置一个如下的属性
textView.setBackground(DrawUtils.getSelector(DrawUtils.getDrawable(Color.rgb(red, green, blue),UIUtils.dp2px(5)),DrawUtils.getDrawable(Color.WHITE,UIUtils.dp2px(5))));
//方式一:
// textView.setClickable(true);
//添加点击事件,也是实现显示selector的效果的一种方式
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(ProductHotFragment.this.getActivity(), textView.getText(), Toast.LENGTH_SHORT).show();
}
});
//设置边距
//设置内边距
int padding = UIUtils.dp2px(10);
textView.setPadding(padding, padding, padding, padding);
// 2.添加到FlowLayout布局中
flowLayout.addView(textView);
}
}
用到的工具类
public class DrawUtils {
//提供一个指定颜色和圆角半径的Drawable对象
public static GradientDrawable getDrawable(int rgb,float radius){
GradientDrawable gradientDrawable = new GradientDrawable();
gradientDrawable.setColor(rgb);//设置颜色
gradientDrawable.setGradientType(GradientDrawable.RECTANGLE);//设置显示的样式
gradientDrawable.setCornerRadius(radius);//设置圆角的半径
gradientDrawable.setStroke(UIUtils.dp2px(1),rgb);//描边
return gradientDrawable;
}
public static StateListDrawable getSelector(Drawable normalDrawable,Drawable pressDrawable) {
StateListDrawable stateListDrawable = new StateListDrawable();
//给当前的颜色选择器添加选中图片指向状态,未选中图片指向状态
stateListDrawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, pressDrawable);
stateListDrawable.addState(new int[]{android.R.attr.state_enabled}, normalDrawable);
//设置默认状态
stateListDrawable.addState(new int[]{}, normalDrawable);
return stateListDrawable;
}
}