在真实的产品环境中使用的自定义 View 与平时写 Demo 时的自定义 View 有没有区别呢?
答案是肯定的,有区别。在产品环境中由于产品的使用者人数庞大因此很多在 Demo 中发现不了的问题也会一一暴露出来,甚至一些平时所认知的正常的情况,在用户看来也是不合常理的。
这次我司有个展示公告的需求,需求是当有公告时,公告内容从左到右匀速划过屏幕,每隔一分钟出现一次。刚拿到需求,心中还是有点窃喜的,一次自定 View 的实战机会来了。
需求上线之后没有出现 bug,但是客服那边却接到用户反馈。用户反馈在他正在看公告时只是返回桌面了几秒钟,再进入 App 时发现公告已经滑到左边接近尾声了,导致没有及时看见公告内容,因此反馈给客服,然后客服就直接反馈到了产品 Bug 群里。
这就尴尬了,进入后台 App 没有被杀,滑动动画肯定在运行呀,在我们看来一切都是完美的。然而产品最终还是决定,改!在该页面不可见时暂停公告滚动,等再次可见时再恢复滚动!
没办法,最后通过该 View 的可见性变化进行监听,通过对动画的暂停和继续操作进行了修改。
通过该次事情我也意识到,对自定 View 的掌握仅仅只是会写 onMeasure() / onLayout() / onDraw() 这个三个方法是不够的,而我们在平时的 Demo 练习中也是着重于该三个函数上,忽略了其它方面的认识,下面我将会以如下的顺序展示我对这个问题的思考:
- 写好 onMeasure() / onLayout() / onDraw(),做好变量的初始化工作避免 OOM 产生。
- 对于需要依赖 View 自身尺寸的变量,需要做好初始尺寸的获取以及 onSizeChanged() 函数的处理。
- 做好生命周期处理,完美断后。
onMeasure() / onLayout() / onDraw() ,变量初始化
关于如何写好自定义的三大方法这里不做详细介绍,网上有很多相应的教程,这里推荐 HenCoder 的自定View教程,全面、清晰。
对于 View 变量的初始化工作,我认为始终不应该在 onLayout() / onMeasure() / onDraw() 三个函数中处理。
onLayout() 和 onMeasure() 方法的调用和父 View 息息相关,可能被调用多次也可能只被调用一次我们无法确保自己的 View 被添加到何种 ViewGroup 中。
onDraw() 会被调用多次,每次 View 发生重绘都会被调用,如果在该方法中创建对象会出现频繁的创建对象,从而不断引发 GC ,造成所谓的内存抖动现象拉低 App 性能。提高发生 OOM 的几率。
变量分类
这里我将自定 View 的变量按照使用者分为两类,一类是不管外界是否调用暴露的方法自身都将使用,二类是只在外界调用对外暴露的函数时才会使用。
第一类变量,我认为应该在自定义 View 的初始化函数中进行初始化。
第二类变量,则可以在当外界进行函数调用时进行初始化,一来减少内存使用,二来逻辑上也会清晰许多。
依赖 View 的尺寸?做好初始尺寸获取和 onSizeChanged() 尺寸改变处理
区分 getWidth() / getMeasureWidth()
在许多文章和 Demo 中我们常看到直接以 getMeasureWidth() 作为 View 的宽度,在绝大多数情况下也是正确的,但是 getMeasureWidth() 确实不是 View 最终的宽度。从源码来看 getMesaurWidth() 获取的是 setMeasuredDimensionRaw() 即 etMeasuredDimension() 函数的值,而 getWidth() 获取的是 mRight - mLeft 的值,mRight 与 mLeft 是在 layout() 时由父 View 传入的值。
因此,在认识清楚两者的区别之后,我们应该在产品项目中需要获取 View 的尺寸时应该使用 getWidth() 系函数,避免和 Demo 一样使用 getMeasureWidth() 系函数。
具体的源码截图如下:
在自定义 View 中获取 View 的尺寸是非常常见的,onDraw() 中绘制位置的确定都依赖于 View 的尺寸,而在 onDraw() 中通过 getWidth() 等函数进行获取虽然可取,但会使得 onDraw() 中的代码较为难看显得不优雅,其次,getWidth() 方法带有数学操作,频率过大时,还是显得不太优雅。
那么应该在那里对 View 尺寸进行获取呢?
对于初始尺寸的获取
首先明确一点,需要在 onLayout() 函数执行之后才能正确获取 View 的尺寸,那么将可用的 api 列出来,然后列举出其调用的时机和意义。
onLaout()getViewTreeObserver.addGloblLayout()在 onLayout() 之后执行,需要注意该方法会调用多次。onDraw()第一次肯定在 onLayout() 之后。onWindowFocusChanged()当 View 所有在的 Window 焦点发生变化时调用,肯定调用在 onLayout() 之后,但是要注意焦点的获得和失去都会调用。
我的建议
那么我的建议是利用 getViewTreeObserver.addGloblLayout() 初始获 View 尺寸,获取完成之后即移除该监听,防止后续重复调用。我的理由呢,有二:
- 利用该方法肯定能获取到 View 的时机尺寸。
- 初始获取 View 尺寸的方法是独立的,仅仅会调用一次。
是否需要处理 onSizeChanged() ?
对于响应 View 的尺寸发生改变在目前 Android 环境来讲大部分情况时不需要的,但在可见的将来折叠屏、伸缩屏的出现对于尺寸变化的适配也是一大需求。
onSizeChanged() 只会在 View 的尺寸发生改变时触发,即 View 的 width / height 发生改变。onSizeChanged() 触发后续必定会调用 onDraw() 方法重绘 View。
那么什么时候需要处理 onSizeChanged() 方法呢? 我认为只有当 View 所展示的内容需要随着 View 尺寸的改变而变化时需要处理,如:TextView 尺寸变化,展示的文字需要重新布局、ImageView 显示的图片需要更改大小。
注意:使用属性动画操作 View 的 top / left / right 等值时亦有可能触发
onSizeChanged()方法。
做好生命周期的处理
View 的生命周期的处理思想和 Activity 的处理思想相似,做好 View 中使用的资源处理,如:Handler / 动画等,总结下来的方式如下:
- 做好资源的使用及释放工作。
这里主要涉及
onAttachedToWindow()/onDetachedFromWindow()两个函数的处理,这里需要注意onDeatachedFromWindow()的调用仅仅表示该 View 从 Window 上移除,后续是可以重新添加的。 - 处理
View可见性变化处理。 这里就看onVisbilityChanged()函数。 - 处理数据保存于重建工作
onSaveInstanceState()和onRestoreInstanceState(),该方法和 Activity 中的方法进行对应。
小结
我们平时的 Demo 练习呢,其实就相当于试验,是为了快速验证某种问题或者实现特定的工作,为了快速且简洁的完成不会关注其他的问题。在我们将 Demo 搬上线上产品时需要有一个产品化过程,全面的思考可能将出现的问题并进行处理。
参考
blog.csdn.net/asd7364645/… measureWidth和width 区别