java流式布局换行_FlowLayout流式布局实战

本文介绍了如何在Android中实现流式布局换行功能,通过自定义FlowLayout,实现子View横向排列并自动换行。文章展示了布局文件和部分源码,解释了布局的实现逻辑和测量过程。
摘要由CSDN通过智能技术生成

背景

流式布局在移动端或者前端开发中很常见,特别是在多标签的展示中, 往往起到了关键的作用。然而Android 官方, 并没有为开发者提供这样一个布局,可参考github有名的仓库:https://github.com/google/flexbox-layout

上效果图

14f9054f224b

image.png

实现逻辑

往FlowLayout里面放子View,FlowLayout会横向对齐布局子View同时自动换行,当子View超出FlowLayout最大高度时支持滑动查看

布局文件

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity">

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical">

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginLeft="8dp"

android:text="搜索历史"

android:textColor="@android:color/black"

android:textSize="18sp"/>

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:paddingLeft="10dp"

android:layout_margin="8dp">

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="水果味孕妇奶粉" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="儿童洗衣机" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="洗衣机全自动" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="小度" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="儿童汽车可坐人" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="抽真空收纳袋" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="儿童滑板车" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="稳压器 电容" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="羊奶粉" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="奶粉1段" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="图书勋章日" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginLeft="8dp"

android:text="搜索发现"

android:textColor="@android:color/black"

android:textSize="18sp" />

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_margin="8dp">

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="惠氏3段" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="奶粉2段" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="图书勋章日" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="伯爵茶" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="阿迪5折秒杀" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="蓝胖子" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="婴儿洗衣机" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="小度在家" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="遥控车可坐" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="搬家袋" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="剪刀车" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="滑板车儿童" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="空调风扇" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="空鼓锤" />

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/shape_button_circular"

android:text="笔记本电脑" />

package com.example.flowlayout;

import android.content.Context;

import android.content.res.Resources;

import android.graphics.Canvas;

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";

private int mHorizontalSpacing = dp2px(16); //每个item横向间距

private int mVerticalSpacing = dp2px(8); //每个item横向间距

private List> allLines = new ArrayList<>(); // 记录所有的行,一行一行的存储,用于layout

List lineHeights = new ArrayList<>(); // 记录每一行的行高,用于layout

public FlowLayout(Context context) {

super(context);

// initMeasureParams();

}

//反射

public FlowLayout(Context context, AttributeSet attrs) {

super(context, attrs);

// initMeasureParams();

}

//主题style

public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

// initMeasureParams();

}

//四个参数 自定义属性

private void clearMeasureParams() {

allLines.clear();

lineHeights.clear();

}

//度量

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

clearMeasureParams();//内存 抖动

//先度量孩子

int childCount = getChildCount();

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 lineViews = new ArrayList<>(); //保存一行中的所有的view

int lineWidthUsed = 0; //记录这行已经使用了多宽的size

int lineHeight = 0; // 一行的行高

int parentNeededWidth = 0; // measure过程中,子View要求的父ViewGroup的宽

int parentNeededHeight = 0; // measure过程中,子View要求的父ViewGroup的高

for (int i = 0; i < childCount; i++) {

View childView = getChildAt(i);

LayoutParams childLP = childView.getLayoutParams();

if (childView.getVisibility() != View.GONE) {

//将layoutParams转变成为 measureSpec

int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight,

childLP.width);

int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom,

childLP.height);

childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

//获取子view的度量宽高

int childMesauredWidth = childView.getMeasuredWidth();

int childMeasuredHeight = childView.getMeasuredHeight();

//如果需要换行

if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {

//一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来

allLines.add(lineViews);

lineHeights.add(lineHeight);

parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;

parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);

lineViews = new ArrayList<>();

lineWidthUsed = 0;

lineHeight = 0;

}

// view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局

lineViews.add(childView);

//每行都会有自己的宽和高

lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;

lineHeight = Math.max(lineHeight, childMeasuredHeight);

//处理最后一行数据

if (i == childCount - 1) {

allLines.add(lineViews);

lineHeights.add(lineHeight);

parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;

parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);

}

}

}

//再度量自己,保存

//根据子View的度量结果,来重新度量自己ViewGroup

// 作为一个ViewGroup,它自己也是一个View,它的大小也需要根据它的父亲给它提供的宽高来度量

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 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.getWidth();

// int bottom = top + view.getHeight();

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();

}

}

// @Override

// protected void onDraw(Canvas canvas) {

// super.onDraw(canvas);

// }

public static int dp2px(int dp) {

return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());

}

}

源码ViewGroup getChildMeasureSpec()方法解析

传入父类的spec, 让子类根据父类的参数测量自己,测量规则参考对应表

/**

* Does the hard part of measureChildren: figuring out the MeasureSpec to

* pass to a particular child. This method figures out the right MeasureSpec

* for one dimension (height or width) of one child view.

*

* The goal is to combine information from our MeasureSpec with the

* LayoutParams of the child to get the best possible results. For example,

* if the this view knows its size (because its MeasureSpec has a mode of

* EXACTLY), and the child has indicated in its LayoutParams that it wants

* to be the same size as the parent, the parent should ask the child to

* layout given an exact size.

*

* @param spec The requirements for this view

* @param padding The padding of this view for the current dimension and

* margins, if applicable

* @param childDimension How big the child wants to be in the current

* dimension

* @return a MeasureSpec integer for the child

*/

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;

int resultMode = 0;

switch (specMode) {

// Parent has imposed an exact size on us

case MeasureSpec.EXACTLY:

if (childDimension >= 0) {

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size. So be it.

resultSize = size;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent has imposed a maximum size on us

case MeasureSpec.AT_MOST:

if (childDimension >= 0) {

// Child wants a specific size... so be it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size, but our size is not fixed.

// Constrain child to not be bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size. It can't be

// bigger than us.

resultSize = size;

resultMode = MeasureSpec.AT_MOST;

}

break;

// Parent asked to see how big we want to be

case MeasureSpec.UNSPECIFIED:

if (childDimension >= 0) {

// Child wants a specific size... let him have it

resultSize = childDimension;

resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

// Child wants to be our size... find out how big it should

// be

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

resultMode = MeasureSpec.UNSPECIFIED;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

// Child wants to determine its own size.... find out how

// big it should be

resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;

resultMode = MeasureSpec.UNSPECIFIED;

}

break;

}

//noinspection ResourceType

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);

}

与下面这张表对应

14f9054f224b

image.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值