自定义控件 一些基础知识点
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义控件 一些基础知识点相关的知识,希望对你有一定的参考价值。
自定义控件的完整步骤
- 抽取出我们需要【自定义的属性】,并在res/values/attrs.xml中声明(文件名可以随意)
- 在layout中【使用】我们自定义的属性,其中View要使用带包名的限定文件名Qualified name,命名空间xmlns要以package结尾
- 在【构造方法】中遍历我们自定义的属性,根据获取到的值对View的成员变量进行初始化,获取完记得调用TypedArray的recycle()方法
- 重写【onMesure】,根据自己的需求对view的宽高进行测量与限定,并最终通过setMeasuredDimension方法保存结果
- 重写【onLayout】,仅viewgroup需要重写,目的是确定子view的位置
- 重写【onDraw】,viewgroup一般不需要重写,绘制view的自身的内容,大多数情况下,这是最核心的一步
- 对需要的【事件】进行拦截、监听与处理,onTouchEvent、onInterceptTouchEvent
自定义属性可设置的类型
reference:参考某一资源ID, = "@drawable/图片ID"
color:颜色值, = "#00FF00"(不能引用colors中定义的值,若要引用请使用reference)
boolean:布尔值, = "true"
dimension:尺寸值, = "42dp"
float:浮点值, = "0.7"
integer:整型值, = "100"
string:字符串, = "string"
fraction:百分数, = "200%"
enum:枚举值, <attr name="orientation"> <enum name="vertical" value="1" />, = "vertical"
flag:位"或", <attr name="windowSoftInputMode"> <flag name = "stateUnspecified" value= "0" /> <flag name = "stateUnchanged" value= "1" />, = "stateUnspecified | stateUnchanged">
属性定义时可以指定多种类型值, <attr name = "background" format= "reference | color" />, = "@drawable/图片ID 或 #00FF00"
View的内部类MeasureSpec简介
一般来说,自定义控件都会去重写View的onMeasure方法,该方法用于指定该控件在屏幕上的大小,源码为:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。这个值由两部分组成,高2位表示MODE,可以通过MeasureSpec.getMode()获取;低30位表示size,也就是父View的大小,可以通过MeasureSpec.getSize()获取。两个方法的源码为:
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
其中,mode共有三种情况,如下:
/**
* Measure specification规格 mode: The parent has not imposed强迫、要求 any constraint约束
* on the child. It can be whatever size it wants.可以是任意你想要的大小,如ListView的高度
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;//未指定尺寸,适用于AdapterView
/**
* Measure specification mode: The parent has determined确定的 an exact size for the child.
* The child is going to be given指定 those bounds regardless不管怎样 of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;//精确尺寸,包括FILL_PARENT和具体的值
/**
* Measure specification mode: The child can be as large as it wants up to the specified指定的 size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;//父控件允许的最大尺寸,WRAP_CONTENT
随便看一下生成widthMeasureSpec的方法:
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) return size + mode;
else return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
对MeasureSpec中的测量模式的理解
1、当view的layout_w/h设为fill_parent时,为何获取到的为MeasureSpec.EXACTLY模式?
这是因为这种情况下的效果是:子view会占据父容器剩余的所有空间。在父View的measure过程中,因为这个空间已经是可以确定的具体值了(即下面的size),所以子view的大小也就可以确定了,所以父容器在测量时调用子view的measure方法时传入的模式是EXACTLY(表明子View可以直接使用这个值)。
//获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
int size = Math.max(0, specSize - padding);
else if (childDimension == LayoutParams.MATCH_PARENT) {//如果设置为MATCH_PARENT=-1
// Child wants to be our size. So be it. 设置child的size为size,mode为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
2、当view的layout_w/h设为wrap_content时,为何获取到的为MeasureSpec.AT_MOST模式?
这是因为这种情况下的效果是:子view的大小是根据自身的内容二改变的。在父View的measure过程中,在父View调用子View的测量方法measure之前,父View是不知道子View的大小的(只有在子View的onMeasure方法调用后才知道), 所以父View只能是给子View一个【参考值】,并且要求子View不能大于这个参考值。为了传达上述状态,就定义了另一个模式AT_MOST。
//获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
int size = Math.max(0, specSize - padding);
else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果设置为WRAP_CONTENT=-2
//设置child的size为size,mode为AT_MOST
// Child wants to determine its own size. It can‘t be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
View中onMeasure方法的默认实现
自定义View一般需要重写onMeasure方法,根据不同的需求onMeasure的实现也不同,如果你的View不是非常特别,可以参考View类中onMeasure方法默认的实现:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
再看看getDefaultSize是怎么实现的:
/**
* Utility用于 to return a default size. Uses the supplied size if the MeasureSpec
* imposed no constraints没有强加的约束. Will get larger if allowed by the MeasureSpec.
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
可以看到,就是我们平时自己的判断逻辑。
再看看里面调用的getSuggestedMinimumHeight方法:
/**
* Returns the suggested minimum最小的 height that the view should use. This returns the
* maximum最大值 of the view‘s minimum height and the background‘s minimum height
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure确保 the returned height is within在要求之内 the requirements of the parent.
*/
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
啥也没有,就是一个最小可以接受的值。
View的resolveSize等方法简介
之前自定义View时,一直都是按照自己的想法重写onMeasure,有时会出现大小跟预期不一致的情况,后来发现View中有几个工具方法:resolveSize、resolveSizeAndState,使用这些方法去测量宽高时,发现大小还是挺"正常"的。
使用方法很简单,比如:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(resolveSize(50, widthMeasureSpec), resolveSize(50, heightMeasureSpec));
}
不过使用这些方法也有一个重要的隐患:如果用户胡乱写宽高,虽然View的宽高正常了,但你的View的显示效果很可能有大问题。
我们看下resolveSize方法是怎么实现的:
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
再看看resolveSizeAndState方法:
/**
* Utility用于 to reconcile调节 a desired期望 size and state, with constraints imposed限制
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound符合的 integer,
* with the resolved分解 size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally随意的 the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
* resulting size is smaller than the size the view wants to be.
*
* @param size How big the view wants to be.你期望View是多大(仅仅是一个参考值)
* @param measureSpec Constraints imposed by the parent.父类强加的限制
* @param childMeasuredState Size information bit mask for the view‘s children.
* @return Size information bit mask as defined by
* MEASURED_SIZE_MASK and MEASURED_STATE_TOO_SMALL.
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) result = specSize | MEASURED_STATE_TOO_SMALL;
else result = size;
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
实现是很简单的,不过只适用于比较简单的View。
getWidth和getMeasuredWidth的区别
先看下View源码中这些方法的实现:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
//这些值都是在父View的【measure】过程中确定的}
public final int getWidth() {
return mRight - mLeft;
//mRight、mLeft这些值都是在父View的【layout】过程中确定的}
public final int getLeft() {
return mLeft;//mLeft这些值都是在父View的【layout】过程中确定的
}
Google文档的说明:
- getWidth():Return the width of the your view, in pixels
- getMeasuredWidth():The width of this view as measured in the most recent call to measure(). This should be used during measurement and layout calculations only仅仅在测量和布局时使用. Use getWidth() to see how wide a view is after layout在布局后要使用getWidth().
getWidth()的解释:
- 从源码可以看出,getWidth返回的是右坐标-左坐标,因为要在布局之后(即父View在执行完layout方法之后)才能确定它的坐标,所以说只能在布局后才可以通过调用getWidth来获取View的宽。
- 由于measure、onMeasure方法是在布局前调用的,所以在measure、onMeasure方法中不能使用getWidth方法。
- getWidth()获取的是这个view最终显示的大小,这个大小有可能等于原始的大小(正常情况下)也有可能不等于原始大小(如果父View在layout时故意改变了子View的位置,那么大小就和原始大小不一样了)。
- 简单说就是,getWidth是用于View在设定好布局后获取View实际宽度的方法。
getMeasuredWidth()的解释:
- 从源码可以看出,getMeasuredWidth()是对View上的内容进行测量后得到的View内容占据的宽度。或者说,其得到的是最近一次调用measure()方法测量后得到的是View的宽度,它应该仅仅用在测量和Layout的计算中。
- getMeasuredWidth():对View上的内容进行测量后(执行完measure方法之后)得到的View的内容占据的宽度。
- getMeasuredWidth()获取的是view原始的大小,也就是这个view在XML文件中配置或者是代码中设置的大小(测量值),这个大小不一定是view最终显示的大小。
注意,在View初始化时,即在构造函数当中是得不到View的实际大小的,getWidth()和getMeasuredWidth()得到的结果都是0,但是可以在onDraw()方法或者dispatchDraw()方法里面获得。
为何要在TypedArray后调用recycle方法
在 android 自定义 View 的时候,需要使用 TypedArray 来获取 XML layout 中的属性值,使用完之后,需要调用 recyle() 方法将 TypedArray 回收。那么问题来了,这个TypedArray是个什么东西?为什么需要回收呢?TypedArray并没有占用IO线程,它仅仅是一个变量而已,为什么需要 recycle?
为了解开这个谜,首先去找官网的 Documentation,到找 TypedArray 方法,得到下面一个简短的回答:
告诉我们在确定使用完之后调用 recycle() 方法。于是进一步查看该方法的解释,如下:
Recycles重新使用、回收 the TypedArray, to be re-used重新使用 by a later caller.
After calling this function you must not ever touch操作 the typed array again.
简单翻译下就是:回收 TypedArray,用于后续调用时可复用之。当调用该方法后,不能再操作该变量。
同样是一个简洁的答复,但没有解开我们心中的疑惑,这个TypedArray背后,到底隐藏着怎样的秘密……
我们决定深入源码,一探其究竟……
首先,是 TypedArray 的常规使用方法:
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerificationCode, defStyle, 0);
可见,TypedArray不是我们new出来的,而是调用了 obtainStyledAttributes 方法得到的对象,该方法实现如下:
public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
final int len = attrs.length;
final TypedArray array = TypedArray.obtain(Resources.this, len);
// .....
return array;
}
我们进一步查看该静态方法的内部实现可知,该类是一个典型的单例模式,并且这个TypedArray是从一个池中获取的。
final TypedArray attrs = res.mTypedArrayPool.acquire();
因此,我们得出结论:
程序在运行时维护了一个TypedArray的池,程序调用时,会向该池中请求一个实例,用完之后,调用 recycle() 方法来释放该实例,从而使其可被【其他模块】复用。
那为什么要使用这种模式呢?答案也很简单,TypedArray的使用场景之一,就是上述的自定义View,自定义View会随着 Activity的每一次Create而Create,因此,需要系统频繁的创建array,对内存和性能是一个不小的开销,如果不使用池模式,每次都让GC来回收,很可能就会造成OutOfMemory。
这就是使用池+单例模式的原因,这也就是为什么官方文档一再的强调:使用完之后一定 recycle。
以上是关于自定义控件 一些基础知识点的主要内容,如果未能解决你的问题,请参考以下文章