效果
child按行显示,显示不下就换行。
分析
- 继承ViewGroup
- onDraw()不重写,使用ViewGroup的
- 测量-重点 (测量child、测量自己)
- 布局-重点 (布局child)
知识点
执行顺序
构造函数 -> onMeasure() -> onLayout() -> onDraw()
问:onMeasure()后getMeasuredHeight()和getMeasuredWidth()可以获得到值。
答:因为onMeasure()中会使用setMeasuredDimension(width,height)设置宽高值。
问:onLayout() 后getHeight()和getWidth()可以获得到值
答:getWidth()是使用onLayout() 后的left、right得到的,没有布局的时候是没有left、right值的,getHeight()同理。
onMeasure()度量
MeasurSpec类
32位整型数据,高两位是模式,低30位是宽/高数据,用位运算来获取数据。(一个数据分为两部分)
模式: EXACTLY(确定的值)、AT_MOST(需要多少给多少,最多不能超过低30位的数据)、UNSPECIFIED(不指定,滚动的时候)
用于在向下执行测量的时候传递View能用的宽/高数据。
MeasurSpec值是根据父容器和自己LayoutParam分配给View的大小,View需要再onMeasure()时确定自己需要的大小(允许你自己调整),然后设置。
如何获取MeasurSpec值?(LayoutParam转化为px)
View的层级是树形的,度量是递归实现的,child的度量是由father分发的,想要知道MeasurSpec值是如何获取的?可以看ViewGroup中的度量child时,是如何获得child的MeasurSpec值的。
ViewGroup类中提供了getChildMeasureSpec方法,用于获得child的MeasurSpec。
方法参数说明如下:
spec :父布局的MeasurSpec
padding:父布局的padding,如获取child宽度的MeasurSpec,就需要paddingLeft+paddingRight。
childDimension:child的布局,有xxdp/wrap_content/match_parent三种情况,如获取child宽度的MeasurSpec值,就是child布局中的android:layout_width值。
经过对getChildMeasureSpec()源码的分析得出如下规则:(宽高都使用此规则)
View的onMeasure()
1. 据MeasureSpec的不同模式确定自己的宽高
如果是MeasureSpec.EXACTLY ,使用MeasureSpec中的size
如果是MeasureSpec.AT_MOST,使用自身需要的大小
如果是MeasureSpec.UNSPECIFIED,0/自身需要的大小
2. 设置View的宽高setMeasuredDimension(width,height)
ViewGroup的onMeasure()
除了 View的onMeasure()中要做的,还要度量child。
先度量谁?自己/child?
一般都是先度量child,在度量自己,ViewPager除外,ViewPager高度设置为wrap_content无效。
流式布局
度量child
度量自己
如果是MeasureSpec.EXACTLY ,使用MeasureSpec中的size,不用计算。
如果是MeasureSpec.AT_MOST,使用自身需要的大小(child撑起的宽高)。
如果是MeasureSpec.UNSPECIFIED,0/自身需要的大小(child撑起的宽高)。
根据上面规则可知,要计算出child撑起的宽高。
child撑起的宽度(width) = 单行总宽度最大值(包含水平间隔) + paddingLeft + paddingRight
child撑起的高度(height) = 每行中高度最高的child的高度 + 垂直间隔 + paddingTop + paddingBottom
设置初始值
int height = getPaddingTop()+getPaddingBottom();
int width = getPaddingLeft()+getPaddingRight();
循环child,记录单行的数据
int lineHeight = 0; //记录每行的高度 放入一个child的时候对比取最大值
int curWidth = getPaddingLeft()+getPaddingRight(); //记录每行的当前宽度 放入一个child的时候加上child的宽度以及水平间隔
换行的时候,保存单行数据至width和height,并清空单行数据,注意换行的判断条件。
注意最后一行(执行到最后一个元素时,保存最后一行的数据)。
另外,为了方便布局,记录每行的View以及每行的高度,布局时不需要再次计算。
另设置自己的宽高的时候,记得用规则判断下。
具体实现见文章底部源码。
布局
根据测量的宽高,确定child的位置(left、right、top、bottom)
在自定义ViewGroup中布局child的时候,使用的是视图坐标系。
在布局时获取child的宽高使用getMeasuredWidth()/getMeasuredHeight(),此时getWidth()/getHeight()还没有数值。
自定义ViewGroup流程
扩展
ViewPager高度设置为wrap_content无效的原因?
在度量时没有先度量child,先设置了自己的宽高,宽高获取使用的是getDefaultSize()方法。
根据文中的获取child的MeasureSpec的规则可知,当LayoutParam的值为wrap_content时,其MeasureSpec的mode为AI_MOST。
看上面源码可知,当model=MeasureSpec.AT_MOST时,没有使用根据child计算高度,用的是父布局能给的所有高度,所以当高度设置为wrap_content,高度仍是占满了父布局的全部。
资料
- MeasureSpec.UNSPECIFIED说明:https://www.cnblogs.com/liushilin/p/11055741.html
- google官方提供的流式布局 flexbox-layout
看的时候注重两点,如何测量子布局child.measure和
设置自己宽高的位置setMeasuredDimension
github地址:https://github.com/google/flexbox-layout
使用文档:FlexboxLayout全攻略(Google官方灵活实现流式布局控件)-CSDN博客
注意:在真实项目中,可以使用FlexboxLayout实现流式布局,也可使用RecyclerView+FlexboxLayoutManager实现。
- 可以自适应child高度的ViewPager(自定义ViewPager,具体实现见源码)
源码
FlowLayout.java
package com.learn.customui.custom;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* 问题:
* 1.没有考虑child的margin
* 2.宽度多了一个水平间隔
* 3.未处理child的visiable属性
* 4.没有考虑gravity
*/
public class FlowLayout extends ViewGroup {
private int mHorizontalSpace = 10; //px 水平间隔
private int mVerticalSpace = 10; //px 垂直间隔
private List<List<View>> mAllViews = new ArrayList<>();//按行存储View,用于布局使用
private List<Integer> lineHeights = new ArrayList<>(); //存储每行的高度
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
*
* @param widthMeasureSpec 根据该View父布局的宽度(MeasureSpec)与自身的LayoutParam(android:layout_width)结合获得的MeasureSpec类型值
* @param heightMeasureSpec 根据该View父布局的高度(MeasureSpec)与自身的LayoutParam(android:layout_height)结合获得的MeasureSpec类型值
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//会多次测量,使用最后一次测量
mAllViews.clear();
lineHeights.clear();
/**
* 1 度量Child
* 1.1 度量child
* 1.2 度量自己的时候需要child的宽高
* 重点:使用getChildMeasureSpec()获取child的MeasureSpec (规则就是固定的,至于能不能显示开发者自定)
*/
for(int i = 0;i<getChildCount();i++){
View child = getChildAt(i);
//获取child宽度的MeasureSpec
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeft()+getPaddingRight(),child.getLayoutParams().width);
//获取child高度的MeasureSpec
int childHeightMeasureSpec= getChildMeasureSpec(heightMeasureSpec,getPaddingTop()+getPaddingBottom(),child.getLayoutParams().height);
//测量child
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
//child被度量过,可以获取宽高
/**
* 2 度量自己 根据不同模式下设置不同的大小
* MeasureSpec.EXACTLY 使用方法参数中的size
* MeasureSpec.AT_MOST 使用子布局撑起来的size
* MeasureSpec.UNSPECIFIED 使用方法参数中的size 0/size 0代表不指定(滚动时)
*/
//child撑起的高度 = 每行中高度最高的child的高度 + 垂直间隔 + paddingTop + paddingBottom
int height = getPaddingTop()+getPaddingBottom();
//child撑起的宽度 = 单行总宽度最大值(包含水平间隔) + paddingLeft + paddingRight
int width = getPaddingLeft()+getPaddingRight();
//当前View能用的最大宽度(父亲给的)
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
//记录每行的高度
int lineHeight = 0;
//记录每行的当前宽度
int curWidth = getPaddingLeft()+getPaddingRight();
//记录每行的child 布局时使用
List<View> lineView = new ArrayList<>();
//遍历child 计算高度和宽度
for(int i = 0;i<getChildCount();i++){
View child = getChildAt(i);
//换行
if(curWidth+child.getMeasuredWidth()>widthSpecSize){
//存储当前行的数据
mAllViews.add(lineView);
lineView = new ArrayList<>();
lineHeights.add(lineHeight);
height += lineHeight + mVerticalSpace;
width = Math.max(width,curWidth);
//清空
lineHeight = 0;
curWidth = getPaddingLeft()+getPaddingRight();
}
//放入child
lineHeight = Math.max(lineHeight,child.getMeasuredHeight());
curWidth += child.getMeasuredWidth()+mHorizontalSpace;
lineView.add(child);
//最后一个child 存储当前行数据
if(i == getChildCount()-1){
//最后一行不加垂直间隔
height += lineHeight;
width = Math.max(width,curWidth);
mAllViews.add(lineView);
lineHeights.add(lineHeight);
}
}
if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) width= MeasureSpec.getSize(widthMeasureSpec);
if(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) height = MeasureSpec.getSize(heightMeasureSpec);
//设置自己的宽高
setMeasuredDimension(width,height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int curLeft = getPaddingLeft();
int curTop = getPaddingTop();
for (int i = 0; i < mAllViews.size(); i++) {
for (View child : mAllViews.get(i)) {
child.layout(curLeft,curTop,curLeft+child.getMeasuredWidth(),curTop+child.getMeasuredHeight());
curLeft += child.getMeasuredWidth()+mHorizontalSpace;
}
curLeft = getPaddingLeft();
curTop += lineHeights.get(i)+mVerticalSpace;
}
}
}
activity_flow.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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"
android:orientation="vertical"
tools:context=".FlowActivity">
<com.learn.customui.custom.FlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/black">
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red"
android:text="1111"/>
<TextView
android:layout_width="300dp"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red"
android:text="2222"/>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red"
android:text="1111"/>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red"
android:text="2222"/>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red"
android:text="1111"/>
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:gravity="center"
android:background="@color/red"
android:text="2222"/>
</com.learn.customui.custom.FlowLayout>
</LinearLayout>
WrapHeightViewPager.java
package com.learn.customui.custom;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;
public class WrapHeightViewPager extends ViewPager {
public WrapHeightViewPager(@NonNull Context context) {
super(context);
}
public WrapHeightViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 方法1:在super前改变heightMeasureSpec中size(targetSDK = Android33,此方法不使用,获取到的child=null,targetSDK=28 可用,其余版本未测试)
* 1.如何改变MeasureSpec中的size?使用MeasureSpec.makeMeasureSpec(size,mode)重新生成一个。
* 2.获取高度mode的方法?使用MeasureSpec.getMode(heightMeasureSpec)/设置为固定的MeasureSpec.EXACTLY
*
* 方法2:在super后重新设置高度(不存在方法1的问题)setMeasuredDimension(getMeasuredWidth(),height);
* 问题:
* 1.宽度使用 getMeasuredWidth(),已在super中设置过。
*
* 共同问题:
* 1.高度size怎么获取?child中最高的?
* 循环遍历child,使用所有child中高度最高的(适合child高度统一的情况)
* 2.当前child的高度?如何获取当前child,切换child的时候会重新onMeasure()吗?(当child高度不统一,需要再切换的时候重新设置时)(待验证,会有很多坑)
* getChildAt(getCurrentItem())获取当前child,注意:切换的时候会销毁其他,注意越界问题。
* 如果不会重新onMeasure(),可以自定义方法自行刷新。
*/
//此处使用方法2实现
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height);
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
int height = 0;
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
switch (heightMode){
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
height = Math.max(height,child.getMeasuredHeight());
}
break;
}
setMeasuredDimension(getMeasuredWidth(),height);
}
}