效果图:
概述
1.流式布局原理:
在布局内,随意摆放任意个view,每行所摆放的view个数,根据实施计算出来的宽度,一旦当前要摆放的view宽度和之前摆放的所有view宽度加在一起,超过了布局的宽度,那么就把该view换行摆放
2.应用场景:
一般,像这种流式布局会应用在一些热门标签,热门推荐之类的应用上
3.测量模式:
谈到FlowLayout流式布局,不得不提及他的测量模式:
* MeasureSpec.EXACTLY:精确模式, eg:100dp,match_parent.(明确指出)
* MeasureSpec.AT_MOST: 至多模式, view最多可以获得的宽高值,它需要计算所有包含的子view的宽高,最后计算出来的宽高总和值,eg:wrap_content.
* UNSPECIFIED:未指定模式,想设置多宽多高,就给你多宽多高,一般的控件不会指定这种模式,但也存在,这种模式用的不多。eg:scrollview的宽高测量,就是使用的此种模式
4.在我们的流式布局内,应该怎么设置布局的宽高呢? onMeasure()
1:如果布局指定的宽是match_parent或者精确的宽度值,那么直接就可以从父控件传入的测量规格中直接获取布局宽度,高度同理.
2:如果布局指定的宽高不是EXACTLY,而是AT_MOST,那么这时候,就需要计算每一个子view的宽高,来决定布局的宽高了。
宽度:摆放的所有子view占据宽度最多的一行,作为布局宽度。
高度:摆放的所有子view总共占据几行的高度总和。
5.子View的布局方式: onLayout()
使用onLayout():设置ViewGroup内包含的所有子view的位置;
获取到每一行的每一个子view,计算出它的left,top,right,bottom,调用layout方法设置其在流式布局当中的位置。
宽度=子view最多的那行的宽度=那一行每一个子view的宽度+leftMargin+rightMargin;
高度=所有行的高度 = 每一行的高度+topMargin+bottomMargin;
LayoutParams参数的设置
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的实现流式布局
根据上面的技术分析,自定义类继承于ViewGroup,并重写 onMeasure和onLayout等方法。具体实现代码如下:
package com.zhuoshi.inspecting.mvp.ui.view.views;
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;
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<Integer> allHeights = new ArrayList<>();//集合中的元素:记录每一行的高度
private List<List<View>> 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<View> 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<View> 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;
}
}
需要用到的工具类:
------------------------DrawUtils------------------------
package com.zhuoshi.inspecting.app.utils;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
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;
}
}
-------------------------MyApplication-----------------------
package com.zhuoshi.inspecting.app;
import android.content.Context;
public class MyApplication extends Application{
private static MyApplication instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
}
public static MyApplication getInstance(){
return instance;
}
}
--------------------------UIUtils--------------------------
package com.zhuoshi.inspecting.app.utils;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.zhuoshi.inspecting.app.MyApplication;
public class UIUtils {
/**
* @return 应用的上下文
*/
public static Context getContext() {
return MyApplication.getInstance();
}
/**
* 获取资源对象
*/
public static Resources getResources() {
return getContext().getResources();
}
/**
* @param id
* @return 资源文件字符串
*/
public static String getString(int id) {
return getResources().getString(id);
}
/**
* @param id
* @return 资源文件字符串数组
*/
public static String[] getStringArray(int id) {
return getResources().getStringArray(id);
}
/**
* @param id
* @return 资源文件图片
*/
public static Drawable getDrawable(int id) {
return ContextCompat.getDrawable(getContext(), id);
}
/**
* @param id
* @return 资源文件颜色
*/
public static int getColor(int id) {
return ContextCompat.getColor(getContext(), id);
}
/**
* @param id
* @return 颜色的状态选择器
*/
public static ColorStateList getColorStateList(int id) {
return ContextCompat.getColorStateList(getContext(), id);
}
/**
* @param id
* @return 尺寸
*/
public static int getDimen(int id) {
// 返回具体像素值
return getResources().getDimensionPixelSize(id);
}
/**
* dp ->px
*
* @param dp
* @return
*/
public static int dp2px(float dp) {
float density = getResources().getDisplayMetrics().density;
return (int) (dp * density + 0.5f);
}
/**
* px ->dp
*
* @param px
* @return
*/
public static float px2dp(int px) {
float density = getResources().getDisplayMetrics().density;
return px / density;
}
/**
* 加载布局文件
*
* @param id
* @return
*/
public static View inflate(int id) {
return View.inflate(getContext(), id, null);
}
/**
* 把自身从父View中移除
*
* @param view
*/
public static void removeSelfFromParent(View view) {
if (view != null) {
ViewParent parent = view.getParent();
if (parent != null && parent instanceof ViewGroup) {
ViewGroup group = (ViewGroup) parent;
group.removeView(view);
}
}
}
/**
* 请求View树重新布局,用于解决中层View有布局状态而导致上层View状态断裂
*
* @param view
* @param isAll
*/
public static void requestLayoutParent(View view, boolean isAll) {
ViewParent parent = view.getParent();
while (parent != null && parent instanceof View) {
if (!parent.isLayoutRequested()) {
parent.requestLayout();
if (!isAll) {
break;
}
}
parent = parent.getParent();
}
}
/**
* 判断触点是否落在该View上
*
* @param ev
* @param v
* @return
*/
public static boolean isTouchInView(MotionEvent ev, View v) {
int[] vLoc = new int[2];
v.getLocationOnScreen(vLoc);
float motionX = ev.getRawX();
float motionY = ev.getRawY();
return motionX >= vLoc[0] && motionX <= (vLoc[0] + v.getWidth())
&& motionY >= vLoc[1] && motionY <= (vLoc[1] + v.getHeight());
}
/**
* findViewById的泛型封装,减少强转代码
*
* @param layout
* @param id
* @param <T>
* @return
*/
public static <T extends View> T findViewById(View layout, int id) {
return (T) layout.findViewById(id);
}
/**
* *获取屏幕的比例
*
* @param context *@return
*/
public static float getScaledDensity(Context context) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
float value = dm.scaledDensity;
return value;
}
/**
* 获取控件的高度,如果获取的高度为0,则重新计算尺寸后再返回高度
*
* @param view
* @return
*/
public static int getViewMeasuredHeight(View view) {
calcViewMeasure(view);
return view.getMeasuredHeight();
}
/**
* 获取控件的宽度,如果获取的宽度为0,则重新计算尺寸后再返回宽度
*
* @param view
* @return
*/
public static int getViewMeasuredWidth(View view) {
calcViewMeasure(view);
return view.getMeasuredWidth();
}
/**
* 测量控件的尺寸
*
* @param view
*/
public static void calcViewMeasure(View view) {
int width = View.MeasureSpec.makeMeasureSpec(0,
View.MeasureSpec.UNSPECIFIED);
int expandSpec = View.MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
view.measure(width, expandSpec);
}
/**
* 设置textview指定文字为某一颜色
*
* @param content 显示的文字
* @param color 需要转换成的颜色值
* @param start 需要变色文字开始位置
* @param end 需要变色文字结束位置
*/
public static SpannableStringBuilder changeTextColor(String content, int color, int start, int end) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(content);
spannableStringBuilder.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableStringBuilder;
}
}
在布局文件中加入自定义的flowLayout
<?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.example.administrator.p2pinvest.ui.FlowLayout
android:id="@+id/flow_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.administrator.p2pinvest.ui.FlowLayout>
</LinearLayout>
初始化布局
private FlowLayout flowLayout;
flowLayout = view.findViewById(R.id.flow_layout);
提供页面要显示的数据,这个数据也可以放在服务器中进行联网获取
private String[] datas = new String[]{"重点关注车辆", "逾期未年检车辆", "疑似套牌车辆", "本地布控车辆",
"违法未处理车辆", "黑名单车辆", "重点区域车辆"};
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);
}
}