View的原理学习的差不过了,是时候可以自定义View。记得,3个步骤,测量(onMeasure)- 布局(onLayout)- 绘画(onDraw)。
onMeasure
测量上一节已经学过onMeasure方法的主要参数MeasureSpec,我们主要根据MeasureSpec得到的mode和size,测量View的宽高。
// 定义mWidth和mHeight
int mWidth = 200;
int mHeight = 100;
// 重写onMeasure方法
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**
* 因为View的LayoutParams是wrap_content,就是AT_MOST模式,如果父容器是match_parent,那么View的模式也会跟着父容器,为match_content,所以wrap_content会不起作用,所以我们自己设置大小。mWidth和mHeight可以根据实际情况而定。
**/
if (widthMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if(widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSize);
} else if(heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, mHeight);
}
}
如果我们需要重新调用View的onMeasure方法,可以调用requestLayout方法即可。
要想在onCreate或者onResume直接调用getWidth/getHeight获取View的大小,貌似是不行的。作者体用了几种常用的方法:
(1)利用onWindowFocusChanged()方法获取
public void onWindowFocusChanged(boolean hadFocus) {
if (hadFocus) {
// 此时View已经初始化完毕了
int width = view.getMeasuredWidth();
int height = view.getMeasureHeight();
}
}
(2)利用view.post(runnable)方法获取
// 将消息加到消息队列最后,此时view也已经初始化完毕,也就是在onCreate和onResume之后才获取view的大小
view.post(new Runnable() {
int width = view.getMeasuredWidth();
int height = view.getMeasureHeight();
});
我认为这种方式是最简单的。
(3)ViewTreeObserver
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutLisetner(new OnGlobalLayoutLisetner(){
public void onGlobalLayout() {
// 一定要移除监听,否则会执行很多遍
view.getViewTreeObserver().removeGlobalLayoutLisetner(this);
int width = view.getMeasuredWidth();
int height = view.getMeasureHeight();
}
});
这种方式是作者推荐使用的一种方法。
(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)
之前百度过有这个方法,这里作者指出这个方法可以用,但是要谨慎,分分钟获取不到值,so就不记录了。
onLayout
onLayout方法用于布局,一般自定义Viewgroup时才需要重写该方法,确定子View的位置,子View只有layout方法(确定自己位置)。onLayout会遍历子View,调用其layout方法确定各个子View的方法。
// 这里用伪代码只做演示,请自行修改
@Override
public void onLayout(boolean isChanged, int l, int t, int r, int b) {
int childCount = getChildCount();
for(int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() == GONE) {
continue();
}
// 计算ChildView左上角的x坐标
int left = caculateChildLeft();
// 计算ChildView左上角的y坐标
int top = caculateChildTop();
// 确定ChildView的位置
childView.layout(left, top, left + viewWidth, top + viewHeight);
}
}
我在慕课网的Android自定义View学到,应该把比较耗时的操作尽量延迟到onLayout这个方法中,因为onMeasure方法会执行多次,而onLayout方法只会执行一次,比较轻量级。如果想重新调用onLayout方法,也是调用View的requestLayout方法(调用requestLayout方法时,会调用View的onLayout方法和onMeasure方法进行重新测量和布局)。
onDraw
onDraw用于绘画,一般用在自定义View(在自定义ViewGroup时一般不进行绘画)。学会怎么使用Canvas.drawXXX方法进行绘制就行。可以配合translate、rotate等进行一些动画,不过要记得使用save和restore来进行动画状态的操作,还有重要的一点,就是如果Activity销毁时,也就是自定义View被移除时,记得要停止动画,如果不这样的话,会造成内存泄露(特别注意)。在View被移除时或者Activity销毁时,会调用View的onDetachedFromWindow方法,我们重写这个方法,停止动画或者销毁一些线程,回收资源就可以避免内存泄露问题啦。
还有一点,就是我们没有必要在自定义View使用handler。View已经提供了子线程更新View的机制,就是使用postInvalidate方法。如果需要在ui线程更新View,可以使用invalidate方法。
自定义View时的注意事项
(1)让View支持wrap_content
这个问题在上面的onMeasure中已经说的很清楚了。
(2)如果有必要,让你们View支持padding
如果是自定义View,如果需要padding,那么在onDraw方法中处理padding。可以在onDraw方法中利用getPaddingLeft等方法获取到padding的值,然后canvas.drawXXX方法时,计算位置加上一个padding即可。
如果是自定义ViewGroup,如果需要padding,在重写onLayout和onMeasure方法计算padding值。
(3)尽量在View中不要使用Handler,没必要
上面onDraw中已经说明白了。
(4)View如果有线程或者动画,需要及时停止和释放资源
在上面onDraw也已经说明白了。
(5)View如果带有滑动嵌套情形时,需要处理好滑动冲突。
具体处理方法参考我之前写的View的滑动冲突处理一节。