自定义控件系列:
秒懂OnMeasure
秒懂OnLayout
让自定义ViewGroup里的子控件支持Margin
让自定义ViewGroup支持Padding
自定义ViewGroup的一个综合实践 FlowLayout
onDraw
最简单的自定义View:SwitchView
我感觉之所以写不好自定义view,是因为我们了解的自定义View的基础知识知道的太少,但是在了解自定义view的基础知识的过程中,又很容易被源码带跑偏,找不到重点,结果是看了很多源码,云里雾里等于没看。
很多时候,源码是很重要,但是不懂适可而止的看源码,你就陷入了汪洋大海。
例如:初中几何里老师讲了“两点之间、直线最短”这个公理后,我们就可以做很多几何题目了,做的过程中还很爽,但是老师没讲“两点之间、直线最短”这个公理的源码是什么,为什么“两点之间、直线最短”,你要想证明这个公理,对于初中生甚至大学生都是不可能解答的,但是这丝毫不影响一个初中生做几何题目(当然,我记得老师说过,公理不需要证明)
所以这个系列博客采用知识点+应用的模式,有重点,有举例
当这些结论性的知识点积累到足够多,很多自定义view,不过就是多个结论的综合应用+小小逻辑算法,我们怕的不是小小逻辑算法,再绕的算法,多试验就出来了,但是不懂基本的结论性知识点,就很茫然了
知识点
关于MeasureSpec是什么,不懂的朋友请先搜索一下,这里对这个不做解释。
-
如果你的自定义view的宽高只支持MeasureSpec.EXACTLY(即:match_parent和具体的数值),那么onMeasure方法不需要重写,因为View这个基类已经默认实现了
-
如果你想支持MeasureSpec.AT_MOST(即:wrap_content),必须重写onMeasure方法,不然你写wrap_content和match_parent效果是一样的(即系统默认返回一个父容器所能给予你的最大尺寸)。
想想为什么,View这个基类,不帮我们实现MeasureSpec.AT_MOST模式呢?
因为不同的view,对于自己的MeasureSpec.AT_MOST(包裹内容)有自己特有的计算方式,例如:ImageView的MeasureSpec.AT_MOST,ImageView会根据你设置的图片,来计算在wrap_content时候的宽高。
TextView的MeasureSpec.AT_MOST,TextView会根据你设置的文字内容多少,(因为内容多了可能换行)和你设置的TextSize(字体大了,自然需要更大的宽高)来计算Textview在wrap_content时候的宽高
FrameLayout在MeasureSpec.AT_MOST模式下,宽度就是所有子view里面最大的那个View的宽度,高度就是所有子view里的最大的那个View的高度
LinearLayout在MeasureSpec.AT_MOST模式下(假设是竖向布局),宽度是所有View里最大的那个View的宽度,高度是所有View的高度的总和
所以View天生支持MeasureSpec.EXACTLY,但是他对于MeasureSpec.AT_MOST是无能为力的,需要具体的View自己具体实现 -
最重要就是这个方法,计算出控件在各种模式下的宽高,通过这个方法设置进去,就好了
setMeasuredDimension(width, height);
以下就是View的onMeasure的默认实现(源码)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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://关键在这里系统对AT_MOST和EXACTLY的处理是一样的,
//都是返回父容器的最大尺寸,不信你可以自己打印出来看看
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
应用
1. 给出支持MeasureSpec.AT_MOST的模板代码
其实这个代码是套路代码,结构是不变的
当写wrap_content,意思是父布局不传给你确定的尺寸,需要这个view自己确定个默认的尺寸,这个尺寸是你自己根据自己的情况计算出来的。
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 首先计算一下在AtMost模式下,这个自定义view的宽高,
// 这里把计算出来的宽高封装在了一个Point里,x为宽,y为高
// 这是模板代码,你只需要按照自己的实现caculateAtMostSize()的具体逻辑,其余的不用变
Point point = caculateAtMostSize(widthMeasureSpec,heightMeasureSpec);
// 根据 默认宽高、AtMost下的宽高、MeasureSpec测量规格,计算出最终这个view的宽高
int width = measureSize(0, point.x, widthMeasureSpec);
int height = measureSize(0, point.y, heightMeasureSpec);
// 把上面计算出来的宽高作为参数设置给setMeasuredDimension就ok了
setMeasuredDimension(width, height);
}
/**
* 通过widthMeasureSpec计算出这个View最终的宽高
*
* @param defalut 这个view的默认值,仅仅是为了支持下UNSPECIFIED模式,但是这个模式其实用不到
* @param atMostSize AT_MOST下的尺寸
* @param measureSpec 测量规格(包含了模式+尺寸)
* @return
*/
private int measureSize(int defalut, int atMostSize, int measureSpec) {
int result = defalut;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = defalut;
break;
case MeasureSpec.AT_MOST:
//在AT_MOST模式下,系统传来的specSize是一个父容器所能容纳的最大值,你这个自定义view计算的尺寸不能大于这个值
result = Math.min(atMostSize, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
/**
* 计算本View在AtMost模式下的宽高
* 其他代码都是不用动的,在这里写下你特有的逻辑就可以
* 我这里只是简单的返回宽高都是 200
*
* @return
*/
private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {
//一般情况,写的自定义view是不需要特别计算这个值的,我会直接给一个默认值
//但是你是自定义ViewGroup的话,这里你必须好好写了
int width = 200;
int height = 200;
return new Point(width, height);
}
2. 一个自定义ViewGroup支持MeasureSpec.AT_MOST的例子:
效果如图:
对应的布局文件时这样的
注意:这里仅仅写支持AT_MOST的代码,还没写onLayout,所以代码运行是看不到效果的,可以打印log,来看下这个view的宽高是不是正确的
/**
* 计算本View在AtMost模式下的宽高
* 其他代码都是不用动的,在这里写下你特有的逻辑就可以
*
* @param widthMeasureSpec
* @param heightMeasureSpec
* @return
*/
private Point caculateAtMostSize(int widthMeasureSpec, int heightMeasureSpec) {
int width = 0;
int height = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 测量一下子控件的宽高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 获得子控件的宽高
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 因为我们的自定义View模拟的是竖向的LinearLayout,所以:
// 控件的宽度为所有子控件里,宽度最大的那个view的宽度,
// 控件高度是所有子空间的高度之和
width = Math.max(childWidth, width);
height += childHeight;
}
return new Point(width, height);
}
参考资料:
《Android群英传》