线上产品中自定义View应该如何完善?

2,408 阅读6分钟

在真实的产品环境中使用的自定义 View 与平时写 Demo 时的自定义 View 有没有区别呢?

答案是肯定的,有区别。在产品环境中由于产品的使用者人数庞大因此很多在 Demo 中发现不了的问题也会一一暴露出来,甚至一些平时所认知的正常的情况,在用户看来也是不合常理的。

这次我司有个展示公告的需求,需求是当有公告时,公告内容从左到右匀速划过屏幕,每隔一分钟出现一次。刚拿到需求,心中还是有点窃喜的,一次自定 View 的实战机会来了。

需求上线之后没有出现 bug,但是客服那边却接到用户反馈。用户反馈在他正在看公告时只是返回桌面了几秒钟,再进入 App 时发现公告已经滑到左边接近尾声了,导致没有及时看见公告内容,因此反馈给客服,然后客服就直接反馈到了产品 Bug 群里。

这就尴尬了,进入后台 App 没有被杀,滑动动画肯定在运行呀,在我们看来一切都是完美的。然而产品最终还是决定,改!在该页面不可见时暂停公告滚动,等再次可见时再恢复滚动!

没办法,最后通过该 View 的可见性变化进行监听,通过对动画的暂停和继续操作进行了修改。

通过该次事情我也意识到,对自定 View 的掌握仅仅只是会写 onMeasure() / onLayout() / onDraw() 这个三个方法是不够的,而我们在平时的 Demo 练习中也是着重于该三个函数上,忽略了其它方面的认识,下面我将会以如下的顺序展示我对这个问题的思考:

  1. 写好 onMeasure() / onLayout() / onDraw(),做好变量的初始化工作避免 OOM 产生。
  2. 对于需要依赖 View 自身尺寸的变量,需要做好初始尺寸的获取以及 onSizeChanged() 函数的处理。
  3. 做好生命周期处理,完美断后。

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 列出来,然后列举出其调用的时机和意义。

  1. onLaout()
  2. getViewTreeObserver.addGloblLayout() 在 onLayout() 之后执行,需要注意该方法会调用多次。
  3. onDraw() 第一次肯定在 onLayout() 之后。
  4. onWindowFocusChanged() 当 View 所有在的 Window 焦点发生变化时调用,肯定调用在 onLayout() 之后,但是要注意焦点的获得和失去都会调用。

我的建议

那么我的建议是利用 getViewTreeObserver.addGloblLayout() 初始获 View 尺寸,获取完成之后即移除该监听,防止后续重复调用。我的理由呢,有二:

  1. 利用该方法肯定能获取到 View 的时机尺寸。
  2. 初始获取 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 / 动画等,总结下来的方式如下:

  1. 做好资源的使用及释放工作。 这里主要涉及 onAttachedToWindow() / onDetachedFromWindow() 两个函数的处理,这里需要注意 onDeatachedFromWindow() 的调用仅仅表示该 View 从 Window 上移除,后续是可以重新添加的。
  2. 处理 View 可见性变化处理。 这里就看 onVisbilityChanged() 函数。
  3. 处理数据保存于重建工作 onSaveInstanceState()onRestoreInstanceState() ,该方法和 Activity 中的方法进行对应。

小结

我们平时的 Demo 练习呢,其实就相当于试验,是为了快速验证某种问题或者实现特定的工作,为了快速且简洁的完成不会关注其他的问题。在我们将 Demo 搬上线上产品时需要有一个产品化过程,全面的思考可能将出现的问题并进行处理。

参考

blog.csdn.net/asd7364645/… measureWidth和width 区别