Android自定ViewGroup实现流式布局
前言
在Android开发过程中,难免会遇到一些现有View实现不了的效果,这个时候我们就需要自定义View或自定义ViewGroup来实现各种各样的UI,来满足项目开发的需要,但是很多人可能又用不好自定义View或ViewGroup,面试中还经常被问到自定义View,真的很蛋疼,所以接下来把我学习到的自定义流式布局分享给大家!
话不多说,直接上干货!
一、自定义View分类
1.自定义View
在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View。
自定义View最基本的两个方法是:onMeasure()、onDraw();View在Activity显示出来,要经历测量、绘制两个步骤,分别对应上述两个动作。
- 测量:onMeasure()决定View的大小;
- 绘制:onDraw()决定绘制这个View。
2.自定义ViewGroup
自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout。
自定义ViewGroup最基本的两个方法是:onMeasure()、onLayout();View在Activity显示出来,要经历测量、绘制两个步骤,分别对应上述两个动作。
- 测量:onMeasure()决定View的大小;
- 布局:onLayout()决定View在ViewGroup中的位置。
二、自定义ViewGroup实现流式布局
1.测量(onMeasure)
代码如下(示例):
/**
* 度量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
clearMeasureParams();//内存 抖动
int childCount = getChildCount();
//先度量孩子
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//解析父亲给ViewGroup的高度宽度
int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "onMeasure: selfWidth = " + selfWidth + " ,selfHeight = " + selfHeight);
//保存一行中的所有的view
List<View> lineViews = new ArrayList<>();
//记录这行已经使用了多宽的size
int lineWidthUsed = 0;
// 一行的行高
int lineHeight = 0;
// measure过程中,子View要求的父ViewGroup的宽
int parentNeededWidth = 0;
// measure过程中,子View要求的父ViewGroup的高
int parentNeededHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
LayoutParams childViewLayoutParams = childView.getLayoutParams();
if (childView.getVisibility() != GONE) {
//将layoutParams转变成为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childViewLayoutParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childViewLayoutParams.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取子View的度量宽高
int childViewMeasuredWidth = childView.getMeasuredWidth();
int childViewMeasuredHeight = childView.getMeasuredHeight();
//如果需要换行
if (childViewMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + heightMeasureSpec);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
//恢复初始化
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
// view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
lineViews.add(childView);
lineWidthUsed = lineWidthUsed + childViewMeasuredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childViewMeasuredHeight);
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
}
}
}
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
setMeasuredDimension(realWidth, realHeight);
}
2.布局(onLayout)
代码如下:
int lineCount = allLines.size();
int curL = getPaddingLeft();
int curT = getPaddingTop();
for (int i = 0; i < lineCount; i++) {
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View view = lineViews.get(j);
int left = curL;
int top = curT;
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
curL = right + mHorizontalSpacing;
}
curT = curT + lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
/**
* 每个item横向间距
*/
private int mHorizontalSpacing = dp2px(16);
/**
* 每个item纵向间距
*/
private int mVerticalSpacing = dp2px(8);
/**
* 记录所有的行,一行一行的存储,用于layout
*/
private List<List<View>> allLines = new ArrayList<>();
/**
* 记录每一行的行高,用于layout
*/
List<Integer> lineHeights = new ArrayList<>();
实现效果如下:
完成FlowLayout代码如下:
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义流式布局
*/
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
/**
* 每个item横向间距
*/
private int mHorizontalSpacing = dp2px(16);
/**
* 每个item纵向间距
*/
private int mVerticalSpacing = dp2px(8);
/**
* 记录所有的行,一行一行的存储,用于layout
*/
private List<List<View>> allLines = new ArrayList<>();
/**
* 记录每一行的行高,用于layout
*/
List<Integer> lineHeights = new ArrayList<>();
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);
}
private void clearMeasureParams() {
allLines.clear();
lineHeights.clear();
}
/**
* 度量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
clearMeasureParams();//内存 抖动
int childCount = getChildCount();
//先度量孩子
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//解析父亲给ViewGroup的高度宽度
int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "onMeasure: selfWidth = " + selfWidth + " ,selfHeight = " + selfHeight);
//保存一行中的所有的view
List<View> lineViews = new ArrayList<>();
//记录这行已经使用了多宽的size
int lineWidthUsed = 0;
// 一行的行高
int lineHeight = 0;
// measure过程中,子View要求的父ViewGroup的宽
int parentNeededWidth = 0;
// measure过程中,子View要求的父ViewGroup的高
int parentNeededHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
LayoutParams childViewLayoutParams = childView.getLayoutParams();
if (childView.getVisibility() != GONE) {
//将layoutParams转变成为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childViewLayoutParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childViewLayoutParams.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取子View的度量宽高
int childViewMeasuredWidth = childView.getMeasuredWidth();
int childViewMeasuredHeight = childView.getMeasuredHeight();
//如果需要换行
if (childViewMeasuredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + heightMeasureSpec);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
//恢复初始化
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
// view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
lineViews.add(childView);
lineWidthUsed = lineWidthUsed + childViewMeasuredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childViewMeasuredHeight);
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
}
}
}
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
setMeasuredDimension(realWidth, realHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int lineCount = allLines.size();
int curL = getPaddingLeft();
int curT = getPaddingTop();
for (int i = 0; i < lineCount; i++) {
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View view = lineViews.get(j);
int left = curL;
int top = curT;
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
curL = right + mHorizontalSpacing;
}
curT = curT + lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
public static int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
}
完整xml布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="搜索历史"
android:textColor="@android:color/black"
android:textSize="18sp" />
<com.lcy.base.customview.view.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:paddingLeft="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="水果味孕妇奶粉" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童洗衣机" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="洗衣机全自动" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="小度" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童汽车可坐人" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="抽真空收纳袋" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="儿童滑板车" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="稳压器 电容" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="羊奶粉" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="奶粉1段" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="图书勋章日" />
</com.lcy.base.customview.view.FlowLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="搜索发现"
android:textColor="@android:color/black"
android:textSize="18sp" />
<com.lcy.base.customview.view.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="惠氏3段" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="奶粉2段" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="图书勋章日" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="伯爵茶" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="阿迪5折秒杀" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="蓝胖子" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="婴儿洗衣机" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="小度在家" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="遥控车可坐" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="搬家袋" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="剪刀车" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="滑板车儿童" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="空调风扇" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="空鼓锤" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_button_circular"
android:text="笔记本电脑" />
</com.lcy.base.customview.view.FlowLayout>
</LinearLayout>
</ScrollView>
总结
自定义流式布局难点1: 在onMeasure方法中,需根据子View的LayoutParams的宽高属性,调用子View的measure方法;//将layoutParams转变成为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childViewLayoutParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childViewLayoutParams.height);
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
自定义流式布局难点2: 在onMeasure方法中,再次获取子View的宽高,需获取测量完成后的宽高,而不是:
int childViewMeasuredWidth = childView.getWidth();
int childViewMeasuredHeight = childView.getHeight();
自定义流式布局难点3: 在onMeasure方法中,最后一行,需单独处理:
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
}
自定义流式布局难点4:
在onMeasure方法中,需关注子View是否为GONE状态。