上期回顾
Android高级UI系列教程(一)_我想月薪过万的博客-CSDN博客https://blog.csdn.net/qq_41885673/article/details/121870917
Android两种坐标系
FlowLayout布局效果展示
FlowLayout布局代码解析
根据我们上一节的测量算法分析可得:先测量子View 再测量并保存自己
- 测量孩子
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//先度量孩子
//第一步:先获取孩子总个数
int childCount = getChildCount();
//第二步:逐个测量
for (int i = 0; i < childCount; i++) {
//获取到当前的 子View
View childView = getChildAt(i);
//如果孩子不可见 则取消测量
if (childView.getVisibility() != GONE) {
LayoutParams childLP = childView.getLayoutParams();
//将 LayoutParams 转变为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
//获取当前的 子View 的宽高
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取子View的度量宽高
int childMeasuredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
}
}
- 计算并设置自己的宽高
//度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这个不写 会导致 UI 混乱 因为父布局可能多次调用子 view.measure
clearMeasureParams();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的参考的宽度
int selfHeight = MeasureSpec.getSize(heightMeasureSpec); //ViewGroup解析的父亲给我的参考高度
List<View> lineViews = new ArrayList<>(); //保存一行中的所有的view
int lineWidthUsed = 0; //记录这行已经使用了多宽的size
int lineHeight = 0; //一行的行高
int parentNeededWidth = 0; //measure过程中 子View要求的父ViewGroup的宽
int parentNeededHeight = 0; //measure过程中 子View要求的父ViewGroup的高
//先度量孩子
//第一步:先获取孩子总个数
int childCount = getChildCount();
//第二步:逐个测量
for (int i = 0; i < childCount; i++) {
//获取到当前的 子View
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
LayoutParams childLP = childView.getLayoutParams();
//将 LayoutParams 转变为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
//获取当前的 子View 的宽高
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取子View的度量宽高
int childMeasuredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
//如果需要换行
if (lineWidthUsed + childMeasuredWidth + mHorizontalSpacing > selfWidth) {
//一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(lineWidthUsed + mHorizontalSpacing, parentNeededWidth);
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
//View 是分行Layout的 所以要记录每一行有哪些View,这样可以方便layout布局
lineViews.add(childView);
//每行都会有自己的宽和高
lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(lineWidthUsed + mHorizontalSpacing, parentNeededWidth);
}
}
}
//再度量自己,并保存
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 top = curT;
int left = curL;
//不能使用 view.getWidth() 因为这个方法是 执行onLayout之后才会有值
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
curL = right + mHorizontalSpacing;
}
curT += lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
- 完整代码展示
package com.wust.testalipay;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* ClassName: FlowLayout <br/>
* Description: <br/>
* date: 2021/12/14 16:16<br/>
*
* @author yiqi<br />
* @QQ 1820762465
* @微信 yiqiideallife
* @技术交流QQ群 928023749
*/
public class FlowLayout extends ViewGroup {
private int mHorizontalSpacing = 0;
private int mVerticalSpacing = 0;
private List<List<View>> allLines = new ArrayList<>(); // 记录所有的行,一行一行的存储,用于layout
List<Integer> lineHeights = new ArrayList<>(); //记录每一行的行高,用于layout
// new 一个对象的时候调用
public FlowLayout(Context context) {
this(context, null);
}
// 解析 xml 布局得时候调用
public FlowLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
// 主题style
public FlowLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 我们在编写自定义布局得时候 一般重写这三个构造方法即可
private void clearMeasureParams() {
//这种写法会出现内存抖动
//allLines = new ArrayList<>();
//lineHeights = new ArrayList<>();
allLines.clear();
lineHeights.clear();
}
//度量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这个不写 会导致 UI 混乱 因为父布局可能多次调用子 view.measure
clearMeasureParams();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int selfWidth = MeasureSpec.getSize(widthMeasureSpec); //ViewGroup解析的父亲给我的参考的宽度
int selfHeight = MeasureSpec.getSize(heightMeasureSpec); //ViewGroup解析的父亲给我的参考高度
List<View> lineViews = new ArrayList<>(); //保存一行中的所有的view
int lineWidthUsed = 0; //记录这行已经使用了多宽的size
int lineHeight = 0; //一行的行高
int parentNeededWidth = 0; //measure过程中 子View要求的父ViewGroup的宽
int parentNeededHeight = 0; //measure过程中 子View要求的父ViewGroup的高
//先度量孩子
//第一步:先获取孩子总个数
int childCount = getChildCount();
//第二步:逐个测量
for (int i = 0; i < childCount; i++) {
//获取到当前的 子View
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
LayoutParams childLP = childView.getLayoutParams();
//将 LayoutParams 转变为 measureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
//获取当前的 子View 的宽高
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//获取子View的度量宽高
int childMeasuredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
//如果需要换行
if (lineWidthUsed + childMeasuredWidth + mHorizontalSpacing > selfWidth) {
//一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(lineWidthUsed + mHorizontalSpacing, parentNeededWidth);
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
//View 是分行Layout的 所以要记录每一行有哪些View,这样可以方便layout布局
lineViews.add(childView);
//每行都会有自己的宽和高
lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(lineWidthUsed + mHorizontalSpacing, parentNeededWidth);
}
}
}
//再度量自己,并保存
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 top = curT;
int left = curL;
//不能使用 view.getWidth() 因为这个方法是 执行onLayout之后才会有值
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
curL = right + mHorizontalSpacing;
}
curT += lineHeight + mVerticalSpacing;
curL = getPaddingLeft();
}
}
}
易错点提示
1、onMeasure()可能会执行多次,所以得在 onMeasure() 方法中 初始化清空 allLines 和 lineHeights ,要不然你会发现布局会向下偏移。同时,初始化清空最好使用 clear() 方法,如果使用 new ArrayList<>() 就会导致内存抖动。
2、最后一行的处理不要忘记,处理逻辑如下:
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
parentNeededWidth = Math.max(lineWidthUsed + mHorizontalSpacing, parentNeededWidth);
}