View的工作流程主要分为onMeasure、onSizeChanged、onLayout、onDraw;
onMeasure
onMeasure方法在控件的父元素正要放置它的子控件时调用。它会问一个问题,“你想要用多大地方啊?”,然后传入两个参数——widthMeasureSpec和heightMeasureSpec。它们指明控件可获得的空间以及关于这个空间描述的元数据。具体使用如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int width = 0;
int height = 0;
// 记录每一行的宽度与高度
int lineWidth = 0;
int lineHeight = 0;
// 得到内部元素的个数
int cCount = getChildCount();
for (int i = 0; i < cCount; i++) {
View child = getChildAt(i);
// 测量子View的宽和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到LayoutParams
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
// 子View占据的宽度
int childWidth = child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;
child.setLayoutParams(lp);
// 子View占据的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin
+ lp.bottomMargin;
// 换行
if (lineWidth + childWidth > sizeWidth - getPaddingLeft()
- getPaddingRight()) {
// 对比得到最大的宽度
width = Math.max(width, lineWidth);
// 重置lineWidth
lineWidth = childWidth;
// 记录行高
height += lineHeight;
lineHeight = childHeight;
} else
// 未换行
{
// 叠加行宽
lineWidth += childWidth;
// 得到当前行最大的高度
lineHeight = Math.max(lineHeight, childHeight);
}
// 最后一个控件
if (i == cCount - 1) {
width = Math.max(lineWidth, width);
height += lineHeight;
}
}
setMeasuredDimension(
//
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width
+ getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height
+ getPaddingTop() + getPaddingBottom()//
);
}
MeasureSpec
MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸。
SpecMode值有如下三种:
- UNSPECIFIED:父容器不对 view 有任何限制,要多大给多大
- EXACTLY:父容器已经检测出 view 所需要的大小
- AT_MOST:父容器指定了一个大小, view 的大小不能大于这个值
关于应用层View,这里是指我们布局中的view,其MeasureSpec的创建遵循下表中的规则
针对上表,这里再做一下具体的说明。前面已经提到,对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自
身的 LayoutParams 来共同决定,那么针对不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。这里简单说下,当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。可能大家会发现,在我们的分析中漏掉了Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式。
onSizeChanged(int w, int h, int oldw, int oldh)
这个是系统回调方法,在这个View的大小发生改变的时候会被系统调用,我们要监控view的大小变化,重写这个方法就可以了。
onLayout
主要作用:为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。当 viewgroup 的位置被确定后,它在 onLayout 会遍历所有的 child 并调用其 layout 。在 layout 中 onLayout 会被调用。
onDraw
绘图时在onDraw中绘制,如果是GONE状态下,View的绘制周期会在onLayout的时候停止,不会执行这个方法。
draw 的大致流程
a. 画背景 background.draw(canvas)
b. 绘制自己( onDraw )
c. 绘制 children(dispatchDraw )
d. 绘制装饰( onDrawScrollBars )
备注:dispatchDraw 会遍历调用所有 child 的 draw ,如此 draw 事件就一层层地传递了下去
invalidate
用于刷新View
说明:请求重绘View树,即draw()过程,假如视图发生大小没有变化就不会调用layout()过程,并且只绘制那些“需要重绘的”视图,即谁(View的话,只绘制该View;ViewGroup,则绘制整个ViewGroup)请求invalidate()方法,就绘制该视图。
一般引起invalidate()操作的函数如下:
- 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
- setSelection()方法:请求重新draw(),但只会绘制调用者本身,listview.setselection(position),表示将列表移动到指定的Position处。
- setVisibility()方法:当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
- setEnabled()方法:请求重新draw(),但不会重新绘制任何视图包括该调用者本身。
自定义View注意事项
让MyView支持Warp_content 如果不在onMeasure()方法中对warp_content做特殊处理,就不能达预期的效果。
解决方法是对在onMeasure()方法中做相应的处理,把需要设定的宽或者高,设置成匹配内容宽高的int值,当然也要考虑内边距padding。
思路如下:判断widthMeasureSpec和heightMeasureSpec是否为AT_MOST,也就是wrap_content,如果想让实际宽度为占满父控件,就把widthSpecSize当做参数,如果不想这样,想要自己定义宽度,就填入具体数值,也可以计算内容的宽度来作为参数。
让MyView支持padding 这是因为直接继承了View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,如果直接继承自ViewGroup的控件,需要在onMeasure()和onLayout()中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。(padding是内边距,需要控件自己控制,而margin是外边距,由父控件影响)
- 尽量不要在View中使用Handler,因为View本身就提供了post系列的方法,完全可以替代Handler的作用。
- View中如有线程或者动画,需要及时停止: 如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow方法会被调用。当View变得不可见时也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
- 注意特殊情况下的View滑动冲突
总结
从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种
类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()
方法,onLayout()方法,onDraw()方法。
onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定
View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会
执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循
环测量子View。
onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是
个抽象方法,实现该方法,来对子View进行布局。
onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空
方法