自定义View绘制流程:
概述
自定义View的基本方法
自定义 View 的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View 在 Activity 中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout 和 draw。
- 测量:onMeasure() 决定 View 的大小;
- 布局:onLayout() 决定 View 在 ViewGroup 中的位置;
- 绘制:onDraw() 决定绘制这个 View。
自定义 View 控件分类
- 自定义 View: 只需要重写 onMeasure() 和 onDraw()
- 自定义 ViewGroup: 则只需要重写 onMeasure() 和 onLayout()
1. 自定义ViewGroup
自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout,包含有子View。
例如:应用底部导航条中的条目,一般都是上面图标(ImageView),下面文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。
2. 自定义View
在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View,不包含子View。
例如:制作一个支持自动加载网络图片的ImageView,制作图表等
PS: 自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦等诸多问题。
自定义View基础
View 类简介
- View 类是Android中各种组件的基类,如View是ViewGroup基类
- View表现为显示在屏幕上的各种视图
Android中的UI组件都由View、ViewGroup组成。
- View的构造函数:共有4个
构造函数是View的入口,可以用于初始化一些内容,和获取自定义属性
View的构造函数有四种重载分别如下:
// 如果View是在Java代码里面new的,则调用第一个构造函数
public void SloopView(Context context) {}
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的 public
public void SloopView(Context context, AttributeSet attrs) {}
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如 view 有 style 属性时
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
// API 21 之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}
可以看出,关于View构造函数的参数有多有少。有四个参数的构造函数在API21的时候才添加上,暂不考虑
有三个参数的构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或Activity所用的Theme中的默认Style,且只有在明确调用的时候才会生效,以系统中的ImageButton为例说明:
public ImageButton(Context context, AttributeSet attrs) {
//调用了三个参数的构造函数,明确指定第三个参数
this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
}
public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
//此处调了四个参数的构造函数,无视即可
this(context, attrs, defStyleAttr, 0);
}
注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。
由于三个参数的构造函数第三个参数一般不用,暂不考虑,第三个参数的具体用法会在以后用到的时候详细介绍。
排除了两个之后,只剩下一个参数和两个参数的构造函数,他们的详情如下:
//一般在直接New一个View的时候调用。
public void SloopView(Context context) {}
//一般在layout文件中使用的时候会调用,关于它的所有属性(包括自定义属性)都会包含在attrs中传递进来。
public void SloopView(Context context, AttributeSet attrs) {}
以下方法调用的是一个参数的构造函数:
//在Avtivity中
SloopView view = new SloopView(this);
以下方法调用的是两个参数的构造函数:
//在layout文件中 - 格式为: 包名.View名
<com.sloop.study.SloopView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
AttributeSet 与自定义属性
系统自带的 View 可以在 xml 中配置属性,对于写的好的自定义 View 同样可以在 xml 中配置属性,为了使自定义的 View 的属性可以在 xml 中配置,需要以下4个步骤:
- 通过
<declare-styleable>
为自定义 View 添加属性 - 在 xml 中为相应的属性声明属性值
- 在运行时(一般为构造函数)获取属性值
- 将获取到的属性值应用到 View
View 视图结构
- PhoneWindow 是 Android 系统中最基本的窗口系统,继承自 Windows 类,负责管理界面显示以及事件响应。它是 Activity 与 View 系统交互的接口
- DecorView 是 PhoneWindow 中的起始节点 View,继承于 View 类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个 FrameLayout。
- ViewRoot 在 Activity 启动时创建,负责管理、布局、渲染窗口 UI 等。
对于多 View 的视图,结构是树形结构:最顶层是 ViewGroup,ViewGroup下可能有多个 ViewGroup 或 View,如下图:
无论是 measure 过程、layout 过程还是 draw 过程,永远都是从 View 树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个 View 树中各个 View,最终确定整个 View 树的相关属性。
Android 坐标系及位置获取方式
Android 中颜色相关内容
Android 支持的颜色模式:
以 ARGB8888 为例介绍颜色定义:
测量View大小(onMeasure)
View 的大小不仅由自身所决定,同时也会受到父控件的影响,为了我们的控件能更好的适应各种情况,一般会自己进行测量。
测量View大小使用的是onMeasure函数,我们可以从onMeasure的两个参数中取出宽高的相关数据:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//取出宽度的确切数值
int widthsize = MeasureSpec.getSize(widthMeasureSpec);
//取出宽度的测量模式
int widthmode = MeasureSpec.getMode(widthMeasureSpec);
//取出高度的确切数值
int heightsize = MeasureSpec.getSize(heightMeasureSpec);
//取出高度的测量模式
int heightmode = MeasureSpec.getMode(heightMeasureSpec);
}
从上面可以看出 onMeasure 函数中有 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的参数, 毫无疑问他们是和宽高相关的, 但它们其实不是宽和高, 而是由宽、高和各自方向上对应的测量模式来合成的一个值:
测量模式一共有三种, 被定义在 Android 中的 View 类的一个内部类View.MeasureSpec中:
模式 | 二进制数值 | 描述 |
---|---|---|
UNSPECIFIED | 00 | 默认值,父控件没有给子view任何限制,子View可以设置为任意大小。 |
EXACTLY | 01 | 表示父控件已经确切的指定了子View的大小。 |
AT_MOST | 10 | 表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。 |
在int类型的32位二进制位中,31-30这两位表示测量模式,29~0这三十位表示宽和高的实际值
用 MeasureSpec 的 getSize是获取数值, getMode是获取模式即可。
注意:
如果对View的宽高进行修改了,不要调用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要调用setMeasuredDimension(widthsize,heightsize); 这个函数。
确定View大小(onSizeChanged)
这个函数在视图大小发生改变时调用。
Q: 在测量完View并使用setMeasuredDimension函数之后View的大小基本上已经确定了,那么为什么还要再次确定View的大小呢?
A: 这是因为View的大小不仅由View本身控制,而且受父控件的影响,所以我们在确定View大小的时候最好使用系统提供的onSizeChanged回调函数。
onSizeChanged如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
可以看出,它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。
我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。
确定子View布局位置(onLayout)
确定布局的函数是onLayout,它用于确定子View的位置,在自定义ViewGroup中会用到,他调用的是子View的layout函数。
在自定义ViewGroup中,onLayout一般是循环取出子View,然后经过计算得出各个子View位置的坐标值,然后用以下函数设置子View位置。
child.layout(l, t, r, b);
四个参数分别为:
名称 | 说明 | 对应的函数 |
---|---|---|
l | View左侧距父View左侧的距离 | getLeft(); |
t | View顶部距父View顶部的距离 | getTop(); |
r | View右侧距父View左侧的距离 | getRight(); |
b | View底部距父View顶部的距离 | getBottom(); |
具体可以参考 坐标系 这篇文章。
5.绘制内容(onDraw)
onDraw是实际绘制的部分,也就是我们真正关心的部分,使用的是Canvas绘图。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
getMeasureWidth 与 getWidth 的区别
- getWidth 在layout()过程结束后才能获取到;通过视图右边的坐标减去左边的坐标计算出来的.
- getMeasuredWidth 在measure()过程结束后就可以获取到对应的值;通过setMeasuredDimension()方法来进行设置的.
LayoutParams
LayoutParams 翻译过来就是布局参数,子 View 通过 LayoutParams 告诉父容器(ViewGroup)应该如何放置自己。从这个定义中也可以看出来 LayoutParams 与 ViewGroup 是息息相关的,因此脱离 ViewGroup 谈 LayoutParams 是没有意义的。
事实上,每个 ViewGroup 的子类都有自己对应的 LayoutParams 类,典型的如 LinearLayout.LayoutParams 和 FrameLayout.LayoutParams 等,可以看出来 LayoutParams 都是对应 ViewGroup 子类的内部类
MarginLayoutParams
MarginLayoutParams 是和外间距有关的。事实也确实如此,和 LayoutParams 相比,MarginLayoutParams 只是增加了对上下左右外间距的支持。实际上大部分 LayoutParams 的实现类都是继承自 MarginLayoutParams,因为基本所有的父容器都是支持子 View 设置外间距的。
- 属性优先级问题
MarginLayoutParams 主要就是增加了上下左右4种外间距。在构造方法中,先是获取了 margin 属性;如果该值不合法,就获取 horizontalMargin;如果该值不合法,再去获取 leftMargin 和 rightMargin 属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优先级
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
- 属性覆盖问题
优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
LayoutParams 与 View 如何建立联系
- 在XML中定义 View
- 在 Java 代码中直接生成 View 对应的实例对象
addView
/**
* 重载方法1:添加一个子View
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重载方法2:在指定位置添加一个子View
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
* @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
/**
* 重载方法3:添加一个子View
* 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成当前ViewGroup默认的LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重载方法4:添加一个子View,并使用传入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
*/
public void addView(View child, int index, LayoutParams params) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
.....
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
} else {
child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
.....
}
自定义LayoutParams
- 创建自定义属性
<resources>
<declare-styleable name="xxxViewGroup_Layout">
<!-- 自定义的属性 -->
<attr name="layout_simple_attr" format="integer"/>
<!-- 使用系统预置的属性 -->
<attr name="android:layout_gravity"/>
</declare-styleable>
</resources>
- 继承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int simpleAttr;
public int gravity;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 解析布局属性
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);
typedArray.recycle();//释放资源
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
- 重写ViewGroup中几个与LayoutParams相关的方法
// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
// 对传入的LayoutParams进行转化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new SimpleViewGroup.LayoutParams(p);
}
// 对传入的LayoutParams进行转化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
LayoutParams常见的子类
在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:
- ViewGroup.MarginLayoutParams
- FrameLayout.LayoutParams
- LinearLayout.LayoutParams
- RelativeLayout.LayoutParams
- RecyclerView.LayoutParams
- GridLayoutManager.LayoutParams
- StaggeredGridLayoutManager.LayoutParams
- ViewPager.LayoutParams
- WindowManager.LayoutParams
MeasureSpec
定义
测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize 是指在某种 SpecMode 下的参考尺寸,其中 SpecMode 有如下三种:
- UNSPECIFIED
父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大) - EXACTLY
父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。 - AT_MOST
你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
MeasureSpecs 的意义
通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法
MeasureSpec值的确定
MeasureSpec值到底是如何计算得来的呢?
子 View 的 MeasureSpec 值是根据子 View 的布局参数(LayoutParams)和父容器的 MeasureSpec 值计算得来的,具体计算逻辑封装在 getChildMeasureSpec()
里
/**
*
* 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个
* 最可能符合条件的child view的测量规格。
* @param spec 父控件的测量规格
* @param padding 父控件里已经占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return child view 的测量规格
*/
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) {
// 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了
case MeasureSpec.EXACTLY:
//如果child的布局参数有固定值,比如"layout_width" = "100dp"
//那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局参数是"match_parent",也就是想要占满父控件
//而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
//比如TextView根据设置的字符串大小来决定自己的大小
//那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
//所以测量模式就是AT_MOST,测量大小就是父控件的size
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size
case MeasureSpec.AT_MOST:
//同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小
//但同样的,child的尺寸上限也是父控件的尺寸上限size
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
//child想要根据自己逻辑决定大小,那就自己决定呗
else if (childDimension == LayoutParams.WRAP_CONTENT) {
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
针对上表,这里再做一下具体的说明
- 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
- 对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。
1. 当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
2. 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
3. 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
4. Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要处理)。
实例 流式布局
public class FlowLayout extends ViewGroup {
/**
* 每个item 横向间距
*/
private final int mHorizontalSpacing = dp2px(16);
/**
* 每个item 竖向间距
*/
private final int mVerticallSpacing = dp2px(16);
/**
* 记录所有的行,一行一行的存储,用于layout
*/
private List<List<View>> allLines = new ArrayList<>();
/**
* 记录每一行的行高,用于layout
*/
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);
}
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 paddingBottom = getPaddingBottom();
int paddingTop = getPaddingTop();
//记录这行已经使用了多宽的size
int lineWidthUsed = 0;
// ViewGroup解析的父亲给我的宽度
int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
// ViewGroup解析的父亲给我的高度
int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
// 保存一行中的所有的view
List<View> lineView = new ArrayList<>();
// 一行的行高
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 childLP = childView.getLayoutParams();
if (childView.getVisibility() != View.GONE) {
// 将layoutParams转变成为 measureSpec
// 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
// 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
// 参数说明
// * @param spec 父view的详细测量值(MeasureSpec)
// * @param padding view当前尺寸的的内边距和外边距(padding,margin)
// * @param childDimension 子视图的布局参数(宽/高)
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
int childHeigthMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
// 测量子view的方法,就把孩子测量完了
childView.measure(childWidthMeasureSpec, childHeigthMeasureSpec);
// 获取子view的测量宽高
int childMesauredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
// 这里需要换行,等于说 接下来要放置的控件放不下了,需要换行
if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
// 一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
allLines.add(lineView);
lineHeights.add(lineHeight);
// 一旦换行,我们就可以判断当前需要的宽和高了,所以要记录起来
parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
lineView = new ArrayList<>();
lineHeight = 0;
lineWidthUsed = 0;
}
// view 是分行layout的,所以要记录每一行有哪些view,这样可以方便layout布局
lineView.add(childView);
// 每行也需要加上空格
lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;
// 获取每行最高的高度
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//处理最后一行数据
if (i == childCount - 1) {
allLines.add(lineView);
lineHeights.add(lineHeight);
// 一旦换行,我们就可以判断当前需要的宽和高了,所以要记录起来
parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
}
}
}
// setMeasuredDimension 此接口是设置自己的大小,并且保存起来
// 在度量自己,并保存,父亲想要获取的时候,直接调用孩子的 child.getMeasureWidth就行
// setMeasuredDimension(width,height);
// 再测量自己,保存
// 根据子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;
// 这个传递的是具体的size,不是MeasureSpec
setMeasuredDimension(realWidth, realHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 总共的行数
int lineCount = allLines.size();
int curT = getPaddingTop();
int curL = getPaddingLeft();
for (int i = 0; i < lineCount; i++) {
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
// 每行的view进行布局
for (int j = 0; j < lineViews.size(); j++) {
View view = lineViews.get(j);
int left = curL;
int top = curT;
// getWidth 在layout()过程结束后才能获取到;通过视图右边的坐标减去左边的坐标计算出来的.
// int right = left + view.getWidth();
// int bottom = top + view.getHeight();
// getMeasuredWidth 在measure()过程结束后就可以获取到对应的值;通过setMeasuredDimension()方法来进行设置的.
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
curL = right + mHorizontalSpacing;
}
curL = getPaddingLeft();
curT = curT + lineHeight + mVerticallSpacing;
}
}
public static int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
}
效果图: