自定义View、ViewGroup的最基本的支持要点
- 自定义View - onMeasure、onDraw
- wrap_content;
- padding;
- 自定义ViewGroup - onMeasure、onLayout
- wrap_content;
- padding;
- margin;
为啥View只需支持padding,而ViewGroup需要支持padding和margin?
这里注意了,margin属于父子View之间的布局关系,是用于父容器计算内部子View的显示位置(onLayout)的,为啥padding不需要父容器干涉?padding是View内部需要支持的属性,不属于父子View之间的布局关系。
- 红色框线 - 父容器
- 蓝色框线 - 子View的显示区域
- 绿色框线 - 子View的绘制区域
- A、B、C、D - paddingTop、paddingLeft、paddingRight 、paddingBottom
- E - 子View的绘制区域
- 1、2、3、4 - marginTop、marginLeft、marginBottom、marginRight
其中,子View的显示区域 = 子View的绘制区域 (E)+ Padding(A、B、C、D),这才是子View实际的地盘。
而margin是父容器的地盘,有一句话叫各扫门前雪,所以子View处理好自己的Padding就好了。ViewGroup也是一个View(ps:可以包括子View的容器View),因此需要处理Padding,又因为它是一个容器,需要计算子View(在父容器中)的显示位置,这就需要将margin考虑在内了,因此必须处理margin。
父View的大小 = 子View大小总和 + 父View的padding总和 + 子View的margin总和;
那么wrap_content为啥也要处理?后面说。
自定义View
自定义一个View,只需处理测量(onMeasure)和重绘(onDraw)即可。
可以发现自定义View和自定义ViewGroup需要重写的方法是不同的,除了都需要重写onMeasure之外:
- 自定义View只需额外重写onDraw即可
- 自定义ViewGroup只需额外重写onLayout即可
于是就有了下面的对话:
父容器:你们(Views)在lz的地盘摆摊,摊位的位置老子定(onLayout),你们说了不算;
子View:好的,这个摊位咋个摆放货品,lz说的算(onDraw),你(父容器)管不着了;
父容器:好啊,但是这个摊位面积问题,咱俩得商量一下撒?不然我咋个知道!lz定完了(onLayout),会告诉你们。之后lz就不管了!
子View:可以啊,听起来你(父容器)很叼的样子!
其实,说白了,还是地盘问题~
看过View源码的同学都知道,View的onLayout是空实现,为啥呢?因为View不是容器,所以没有子View,自然onLayout是需要实现的。那么一个View到底要绘制到屏幕的位置呢?不用我们管么?确实,一般这块是不需要我们处理的。View的layout方法已经为我们实现好了。
public void layout(int l, int t, int r, int b) {
......
// 关键方法:setOpticalFrame 、 setFrame
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
......
}
至于onLayout的四个参数,表示View在屏幕中的位置(左上右下),这四个参数的值是父容器的事情,由父容器计算子View该在屏幕的哪个位置显示,计算完成然后告诉子View(childView.layout(l,t,r,b)),然后子View就乖乖的在这个区域进行onDraw了。
- onMeasure
注意:此处可以解答为啥要支持wrap_content
-----------View的onMeasure处理逻辑----------
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
正常情况下,如果我们自定义View或者ViewGroup不重写onMeasure方法的话,则会调用View的onMeasure方法。注意,在View或者ViewGroup中,测量完成都是通过setMeasuredDimension保存宽高尺寸的。
可以看到,在默认的处理逻辑中,View是通过getDefaultSize来计算宽高的。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这个方法是系统给我们的兜底尺寸,mMinWidth和mMinHeight没记错的话应该都是100px。继续向下看。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以发现,系统默认的处理逻辑中,对于MATCH_PARENT和WRAP_CONTENT的处理是一毛一样的,这也是我们为啥不重写onMeasure方法,给View设置宽高属性值为wrap_content无效的原因。
so,自定义View和ViewGroup的要点说明就到此为止了。
MeasureSpec的生成规则
对于测量过程,这是一个三方协商的过程:开发者、父容器、子View。
开发者:我在XML文件设置好了我想要的尺寸,你俩(父容器和子View)看着办! -- 对应XML布局文件的各个ViewGroup和View的布局参数设置完毕;
父容器:对着各个子View说(measureChild Or measureChildren Or measureChildrenWithMargin):你们先开干吧,等你们都干完了,我的工作才能开始,同时将测量规格(测量规格可以理解为约束)依次告诉每个子View(MeasureSpec)。
子View:子View拿到测量规格,结合自己的实际需要尺寸,得出最终需要的尺寸,并通过setMeasuredDimension保存了起来;
父容器:既然你们都测量完了,我也就能通过你们各自的getMeasuredWidth和getMeasuredHeight获取测量的结果了,那轮到我开干了,我需要计算出我想要的尺寸(padding + Margin + 各个子View的高之和 以及 子View的最大宽度),现在我想要的尺寸我拿到手了,但是这就是最终尺寸么?很显然不是。为啥?老子也是有测量约束的人,想任性也不允许呀!最终,父容器会根据自己想要的尺寸和测量约束,得到最终的尺寸。
测量这块主要分两个部分需要掌握:
- 测量规格的生成规则(这个系统已经帮我们办好了,我们需要做的是掌握该生成规则)
MeasureSpec = View自身的LayoutParams + 父容器的MeasureSpec;
MeasureSpec的生成规则如下:
最终测量尺寸的生成逻辑
这里以宽度为例,View只是显示一个图片。
private int getWidthSize(int widthMeasureSpec) {
int resultSize = 0;
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
if (mode == MeasureSpec.EXACTLY) {// 精确测量,即ImgView的宽度是Match_Parent或者某个具体的测量值
resultSize = size;
} else {// wrap_content
int wantWidth= bitmapWidth + getPaddingLeft() + getPaddingRight();
// 实际所需需结合测量规格来算
// -> 你的所需是有约束的,不是想要多少就是多少!凶残!
resultSize = Math.min(wantWidth, size);
}
return resultSize;
}
绘制
可绘制的区域大小就是上面测量(onMeasure)给出的区域大小了!!
@Override
protected void onDraw(Canvas canvas) {
// FIXME 这里示范了一种错误的写法!!!
// 原本的意思是想将图片绘制在屏幕的(sw / 2 - bitmap.getWidth() / 2 + getPaddingLeft(), sh / 2 - bitmap.getHeight() / 2 + getPaddingTop())位置
// 但是忽略了一点:ImgView的大小并不是屏幕的大小,如果是屏幕的大小,这么绘制是没有问题的,但是ImgView的大小仅仅只是onMeasure中的测量的宽高那么大。
// 因此,只需管好自己的一亩三分地就好了
// canvas.drawBitmap(bitmap, sw / 2 - bitmap.getWidth() / 2 + getPaddingLeft(), sh / 2 - bitmap.getHeight() / 2 + getPaddingTop(), bitmapPaint);
canvas.drawBitmap(bitmap, getPaddingLeft(), getPaddingTop(), bitmapPaint);
}
自定义ViewGroup
自定义一个ViewGroup,只需处理测量(onMeasure)和布局(onLayout)即可。
自定义ViewGroup相对于单纯的自定义一个View,要复杂多了。除了完成自定义View的关于wrap_content、padding的工作外,还需要对margin进行支持。
测量
上面我们曾说过View的测量大小是由View和其父容器共同决定的,但是上述源码的分析中我们其实并没有体现,因为它们都在ViewGroup中,这里我们就要涉及ViewGroup中与测量相关的另外几个方法:measureChildren、measureChild和measureChildWithMargins还有getChildMeasureSpec,见名知意这几个方法都跟ViewGroup测量子元素有关,其中measureChildWithMargins和measureChildren类似只是加入了对Margins外边距的处理,ViewGroup提供对子元素测量的方法从measureChildren开始:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
继续看measureChild
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 获取子元素的布局参数
final LayoutParams lp = child.getLayoutParams();
// 依据我们之前总结的测量规格的生成规则表格,生成子View的测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 告诉子View:测量规格给你了,可以开始测量你自己了
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
子View的measure方法内部会调用我们重写的onMeasure方法,此处分析可以看上面第一部分的啦。
好了,子View测量工作完美的完成了,接下来就看父容器自己的啦!
这里我们自定义的ViewGroup是垂直分布的,类似Vertical LinearLayout - 为了支持margin,这里并没有直接采用measureChildren,而是采用了如下的逻辑:
{
int childCount = getChildCount();
if (childCount == 0) {
return;
}
int parentDesiredWidth = 0;
int parentDesiredHeight = 0;
// 计算一个ViewGroup想要的尺寸
for (int i = 0; i < childCount; i++) {
// 额外考虑Margin
View child = getChildAt(i);
VerticalLayoutParams clp = (VerticalLayoutParams) child.getLayoutParams();
// taking into account both the MeasureSpec requirements for this view and its padding and margins
// ## The child must have MarginLayoutParams ##
// 子View可以得到的最大规格尺寸是:specSize - parentPadding - 子View想要的margin
// 如果采用measureChild,则子View可以得到的最大规格尺寸是:specSize - parentPadding
// 可以看到:measureChildWithMargins与measureChild区别在于,前者在测量子View的时候,将Margin额外考虑进去了(padding的话,两者都有处理)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 计算父容器尺寸
parentDesiredWidth = Math.max(child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin, parentDesiredWidth);
parentDesiredHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
// 别忘记了父容器自己的padding,这里为啥不考虑margin?margin的处理是父容器的事情,即ViewGroup也有自己的父容器,ViewGroup只处理自己的子View的margin就好了
// 自己的margin是交给自己的父容器去处理的。
parentDesiredWidth += getPaddingLeft() + getPaddingRight();
parentDesiredHeight += getPaddingTop() + getPaddingBottom();
// 得到想要的尺寸之后,需再结合widthMeasureSpec 或者 heightMeasureSpec,得出最终的尺寸
// 方式1
// setMeasuredDimension(getWidth(resultWidthSize, widthMeasureSpec), getHeight(resultHeightSize, heightMeasureSpec));
// 方式2
setMeasuredDimension(resolveSize(parentDesiredWidth, widthMeasureSpec), resolveSize(parentDesiredHeight, heightMeasureSpec));
}
建议使用方式2,为啥?系统已经给轮子了,我们不需要重新造轮子。
这里再看一下measureChild和measureChildWithMargins的区别!这两个方法都是父容器测量子View的方法,可以看出区别的是如下这个方法:(这两个方法内部在传递给子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);// 子View可得的最大规格
......
}
其实区别就一句话:告诉子View规格的时候,是否扣除了margin!!(padding两者都扣除了)
扣除后的才是最终给你的!!
与普通的自定义View相比,自定义ViewGroup除了需要完成自定义View的工作外,有如下两点不同:
- 父容器的尺寸计算必须等到各个子View测量完成才能开始的呀,而且父容器的尺寸是各个子View的尺寸之和;
- 需要额外考虑Margin,看看上面的VerticalLayoutParams是个什么鬼?
基本的布局参数
ViewGroup
有两个最基本的布局参数类,
ViewGroup.LayoutParams
和
ViewGroup.MarginLayoutParams
。
所有的自定义
ViewGroup
的布局参数类必须要直接或者间接继承自
ViewGroup.LayoutParams
类,因为你需要支持
layout_width
和
layout_height
布局属性。
如果你想要让自定义
ViewGroup
的布局参数属性支持
margin
,例如
layout_marginLeft
,那么自定义
ViewGroup
的布局参数类需要直接或者间接继承自
ViewGroup.MarginLayoutParams
,因为它有解析
margin
属性的功能。
至于为啥重写这些方法,详见:https://juejin.im/post/5dc1225c5188255f7b0fa42d#heading-6
这篇博文的分析,很详细
----------------VerticalViewGroup中重写如下方法 -----------------------
注意:如果不需要在LayoutParams处理自己的逻辑,单纯地计算margins就没必要去实现一个自定义的MarginLayoutParams子类了,可以直接返回一个MarginLayoutParams的实例对象
-----> 此处先已MarginLayoutParams子类为例子而已
@Override
protected VerticalLayoutParams generateDefaultLayoutParams() {
return new VerticalLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
return new VerticalLayoutParams(p);
}
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new VerticalLayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
return p instanceof VerticalLayoutParams;
}
自定义一个ViewGroup.MarginLayoutParams的子类
// 用于VerticalViewGroup测量时,获取子View的Margin
public class VerticalLayoutParams extends ViewGroup.MarginLayoutParams {
public VerticalLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public VerticalLayoutParams(int width, int height) {
super(width, height);
}
public VerticalLayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public VerticalLayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
注意:如果你没有额外添加对Margin的处理的话,却又想获取Margin(直接将LayoutParams强转成ViewGroup.MarginLayoutParams,然后通过xxxmargin属性获取属性值),你会收到一个“礼物”
java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
布局
布局很简单,就是计算一下各个子View在父容器中的位置就可以了
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 获取一个子元素
View child = getChildAt(i);
// 通知子元素进行布局
child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
以下内容节选自:教你步步为营掌握自定义View
我要改变这个View的行为,外观,肯定是覆写View类中的方法,但是怎么覆写,覆写哪些方法能够改变哪些行为?
好了,View的位置和大小怎么确定我们都清楚了,现在,是时候开始自定义View了。
首先,关于View所要具备的一般功能,View类中都有了基本的实现,比如确定位置,它有layout方法,当然,这个只适用于ViewGroup,实现自己的ViewGroup时,才需要修改该方法。确定大小,它有onMeasure方法,如果你不满意默认的确认大小的方法,也可以自己定义。改变默认的绘制,就覆写onDraw方法。下面,我们通过一张图,来看看,自定义View时,我们最可能需要修改的方法是哪些:
把这些方法都搞明白了,你也就理解了View的生命周期了。
比如View被inflated出来后,系统会回调该View的onFinishInflate方法,你的View可以在这个方法中,做一些准备工作。
如果你的View所属的Window可见性发生了变化,系统会回调该View的onWindowVisibilityChanged方法,你也可以根据需要,在该方法中完成一定的工作,比如,当Window显示时,注册一个监听器,根据监听到的广播事件改变自己的绘制,当Window不可见时,解除注册,因为此时改变自己的绘制已经没有意义了,自己也要跟着Window变成不可见了。
当ViewGroup中的子View数量增加或者减少,导致ViewGroup给自己分配的屏幕区域大小发生变化时,系统会回调View的onSizeChanged方法,该方法中,View可以获取自己最新的尺寸,然后根据这个尺寸相应调整自己的绘制。
当用户在View所占据的屏幕区域发生了触摸交互,系统会将用户的交互动作分解成如DOWN、MOVE、UP等一系列的MotionEvent,并且把这些事件传递给View的onTouchEvent方法,View可以在这个方法中进行与用户的交互处理。
参考学习博文,感谢:
https://blog.csdn.net/aigestudio/article/details/42989325
https://blog.csdn.net/u012732170/article/details/55045472###
https://www.jianshu.com/p/d507e3514b65
https://juejin.im/post/5dc1225c5188255f7b0fa42d#heading-6