自定义控件 一些基础知识点

自定义控件的完整步骤

  1. 抽取出我们需要【自定义的属性】,并在res/values/attrs.xml中声明(文件名可以随意)
  2. 在layout中【使用】我们自定义的属性,其中View要使用带包名的限定文件名Qualified name,命名空间xmlns要以package结尾
  3. 在【构造方法】中遍历我们自定义的属性,根据获取到的值对View的成员变量进行初始化,获取完记得调用TypedArray的recycle()方法
  4. 重写【onMesure】,根据自己的需求对view的宽高进行测量与限定,并最终通过setMeasuredDimension方法保存结果
  5. 重写【onLayout】,仅viewgroup需要重写,目的是确定子view的位置
  6. 重写【onDraw】,viewgroup一般不需要重写,绘制view的自身的内容,大多数情况下,这是最核心的一步
  7. 对需要的【事件】进行拦截、监听与处理,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可以直接使用这个值)。

  1. //获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
  2. int size = Math.max(0, specSize - padding);
  3. else if (childDimension == LayoutParams.MATCH_PARENT) {//如果设置为MATCH_PARENT=-1
  4. // Child wants to be our size. So be it. 设置child的size为size,mode为EXACTLY
  5. resultSize = size;
  6. 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。

  1. //获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
  2. int size = Math.max(0, specSize - padding);
  3. else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果设置为WRAP_CONTENT=-2
  4. //设置child的size为size,mode为AT_MOST
  5. // Child wants to determine its own size. It can‘t be bigger than us.
  6. resultSize = size;
  7. 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源码中这些方法的实现:

  1. public final int getMeasuredWidth() {
  2. return mMeasuredWidth & MEASURED_SIZE_MASK;//这些值都是在父View的【measure】过程中确定的
  3. }
  4. public final int getWidth() {
  5. return mRight - mLeft;//mRight、mLeft这些值都是在父View的【layout】过程中确定的
  6. }
  7. public final int getLeft() {
  8. return mLeft;//mLeft这些值都是在父View的【layout】过程中确定的
  9. }

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() 方法。于是进一步查看该方法的解释,如下:

  1. Recycles重新使用、回收 the TypedArray, to be re-used重新使用 by a later caller. 
  2. 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 方法得到的对象,该方法实现如下:

  1. public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
  2. final int len = attrs.length;
  3. final TypedArray array = TypedArray.obtain(Resources.this, len);
  4. // .....
  5. return array;
  6. }

从上面的代码片段得知,TypedArray是通过调用自身的一个静态方法并做一些处理后得到的一个实例。

我们进一步查看该静态方法的内部实现可知,该类是一个典型的单例模式,并且这个TypedArray是从一个池中获取的。

  1. final TypedArray attrs = res.mTypedArrayPool.acquire();

因此,我们得出结论:

程序在运行时维护了一个TypedArray的池,程序调用时,会向该池中请求一个实例,用完之后,调用 recycle() 方法来释放该实例,从而使其可被【其他模块】复用。

那为什么要使用这种模式呢?答案也很简单,TypedArray的使用场景之一,就是上述的自定义View,自定义View会随着 Activity的每一次Create而Create,因此,需要系统频繁的创建array,对内存和性能是一个不小的开销,如果不使用池模式,每次都让GC来回收,很可能就会造成OutOfMemory。

这就是使用池+单例模式的原因,这也就是为什么官方文档一再的强调:使用完之后一定 recycle。

时间: 2024-08-05 11:57:23

自定义控件 一些基础知识点的相关文章

黑马程序员——Java I/O流基础知识点(File类)

File工具 File类就是用俩将文件或者文件夹封装对象,弥补流对象的不足--流只能操作数据,不能操作文件夹的 封装的是路径!!! 构造方法演示 1.可以将已有的未出现的文件或者文件夹封装成对象. File f1=new File("c:\\abc\\a.txt"): File f2=new File("d:\\abc","ab.txt"打印,会打印路径.:目录分隔符,为了更好地跨平台File. File类常见功能 1,创建 createNewF

HTML5基础知识点

今天了解html,下面是我分享的有关html的基础知识点: ①什么是html? html是一种超文本标记语言(超文本标记标签) ②html标签是html中的最基本单位 也是最重要的部分,html标签分为单标签和双标签. 单标签</>   双标签<></> html中的一些常见标签 1,<br/>换行标签    2,<hr/>水平分割线标签   3,<p></p>段落标签     4,<pre></pre&

C# .Net基础知识点解答

C# .Net基础知识点解答 1. 什么是.NET?什么是CLI?什么是CLR?IL是什么?JIT是什么,它是如何工作的?GC是什么,简述一下GC的工作方式? 通俗的讲,.Net是微软开发应用程序的一个平台: CLI是Common Language Infrastructure,是公共语言架构: CLR是Common Language Runtime,即公共语言运行时: IL是.Net编译器产生的中间代码,称为通用中间语言CIL(Common Intermediate Language),CIL

js基础知识点收集

js基础知识点收集 js常用基本类型 function show(x) { console.log(typeof(x)); // undefined console.log(typeof(10)); // number console.log(typeof('abc')); // string console.log(typeof(true)); // boolean console.log(typeof([])); // object console.log(typeof(function (

js基础知识点总结

js基础知识点总结 如何在一个网站或者一个页面,去书写你的js代码:1.js的分层(功能):jquery(tool) 组件(ui) 应用(app),mvc(backboneJs)2.js的规划():避免全局变量和方法(命名空间,闭包,面向对象),模块化(seaJs,requireJs) 常用内部类:Data Array Math String HTML属性,CSS属性HTML:属性.HTML属性="值":CSS:对象.style.CSS属性="值"; class和f

JavaScript语言基础知识点图示

原文:JavaScript语言基础知识点图示 一位牛人归纳的JavaScript 语言基础知识点图示. 1.JavaScript 数据类型 2.JavaScript 变量 3.Javascript 运算符 4.JavaScript 数组 5.JavaScript 流程控制 6.JavaScript 函数基础 7.DOM 基本操作 8.Window 对象 9.JavaScript 字符串函数 10.正则表达式 JavaScript语言基础知识点图示,布布扣,bubuko.com

Spring4.x 基础知识点

# Spring4.x 基础知识点## 第二章 快速入门- 一般情况下,需要在业务模块包下进一步按分层模块划分子包,如user\dao.user\service.viewspace\dao.viewspace\service等.对于由若干独立子系统组成的大型应用,在业务分层包前还需要加上子系统的前缀.包的规划对于大型应用非常重要,它直接关系到应用部署和分发的便利性.- 在配置文件的定义上,一般也是按模块进行划分,一定程度上降低争用.- 在拼接SQL语句的句前和句后都加一个空格,这样避免分行SQL

fastclick 源码注解及一些基础知识点

在移动端,网页上的点击穿透问题导致了非常糟糕的用户体验.那么该如何解决这个问题呢? 问题产生的原因 移动端浏览器的点击事件存在300ms的延迟执行,这个延迟是由于移动端需要通过在这个时间段用户是否两次触摸屏幕而触发放大屏幕的功能.那么由于click事件将延迟300ms的存在,开发者在页面上做一些交互的时候往往会导致点击穿透问题(可以能是层之间的,也可以是页面之间的). 解决问题 之前遇到这个问题的时候,有在网上看了一些关于解决移动端点击穿透的问题,也跟着网上提出的方式进行了各项测试,最终还是觉得

jQuery基础知识点(下)

在实际开发中,jQuery的实践性非常强大.上一篇本人已经整理出了一部分基础知识点,该文即是对以上的补充和扩展. 1.表单值的操作 1 //获取文本框的值 2 //txt.value 3 var val = $("#txt").val(); //没有参数表示获取值 4 //设置文本框的值 5 //txt.value = "123123"; 6 $("#txt").val("这是val设置的值"); // 有参数表示设置值 2.