三思系列:为什么要自定义View
前言
或许你掌握了
measure的细节
,layout机制
,事件传递机制
,canvas各种API
,但是,你们想过这个问题吗?这一篇,不仅仅是对一个面试必会题的解析,更是透过这个问题的思考,寻找
最佳实践
,拓展思维角度
,少走弯路
三思系列是我最新的学习、总结形式,着重于:问题分析、技术积累、视野拓展,关于三思系列
关于View系列 View系列旨在通过
对现实问题
的思考,建立完善的View体系认知
,极力建议读者了解一下我为什么撰写、分享这个系列
先给出思考这个问题的 脑图
,文章内容会按照思考过程展开
思考这类问题,为什么要这样干
是最基本,作为三思系列的成员,本篇还将对以下内容点进行展开论述:
- 怎么干 --
How to do
- 是否一定要这样干 --
适用场景
- 如果不这样干,还可以怎么干 --
Best Practice
- 各种干法的
注意事项
从View体系出现的目的说起
作为 GUI
Graphical User Interface,图形用户接口 类型的程序 framework
,View体系
是其 必不可少
的一部分。参与了两件重要的事情:
描述
、呈现
界面- 参与
人机交互
笼统的讲,当
现有
的View体系
内的控件簇
无法满足合理需求时,可以在遵从 framework内在
的规则
、机制
,进行扩展,以满足需求。
从这个角度看,扩展可以有两个方面:
- 扩展
显示
功能 - 扩展
交互
功能
扩展显示功能
我们知道,这又分为3种:
- 通过一组控件,共同完成特定的功能,
- 扩展布局规则
- 扩展内容显示
最简单的,一组控件完成特定功能
举个例子:
输入框
右侧加一个 模态的图片
,输入框有内容时显示,无内容时隐藏。图片显示一个❌,点击时清除输入框的内容
经过简单的封装,我们可以很快的完成这样的功能。
Android的UI描述并不那么方便,为了方便,往往会定义一个ViewGroup的子类,来描述这个 控件组
。
但是 组合优于继承
,这样的做法让人有点 膈应
,不能算作是最佳实践。-- 这一点对应了脑图中的 扩展类簇1
相比于这样干,我更建议使用 Facade模式
进行逻辑封装,采用xml方式
声明这个控件组,或者封装 命令式构建函数
构建这个控件组。
继承ViewGroup,扩展布局规则
Android中ViewGroup来封装布局规则,并提供了一套Layout。
当这些布局规则 无法满足
我们的需求时,我们可以通过 自定义ViewGroup
的方式来实现 自定义布局规则
。
当然,Android发展到如今,已经 很难
找到一个相对抽象
的布局规则,却没有被官方支持。
若确有必要,扩展布局规则时需要处理:
- 封装规则描述,并实现
契约式编程设计
- 定义LayoutParams,封装规则的细节点描述
- 覆写 checkLayoutParams 以实现规则校验,
契约式编程设计
- 覆写 generateLayoutParams(AttributeSet attrs) 以实现
从xml属性生成LayoutParams
- 覆写 generateLayoutParams(ViewGroup.LayoutParams p) 以实现
当规则不满足契约
时,生成一个满足契约的LayoutParams,注意:可以从原LayoutParams中 采纳一些内容。 - 覆写 generateDefaultLayoutParams 以实现生成符合契约的
默认布局规则
,如果返回null,在addView(View)
时,会引起运行时异常
- 在
onMeasure
方法中处理测量的逻辑,以实现确定自身大小
和触发子View测量
- 接受
Parent
给到自身的尺寸测量信息
,如果测量模式是EXACTLY
,即可直接确定自身对应维度的尺寸;如果是AT_MOST
或者UNSPECIFIED
, 则需要先测量子View,再确定自身。 - 按照布局特性,自身的
尺寸测量信息
,和子View的布局规则属性值,确定子View
的尺寸测量信息
,调用子View
的measure
方法触发测量
- 接受
- 在
onLayout
方法中,处理布局,使用子View的尺寸测量值
和LayoutParams规则值
,计算子View 的布局位置,并调用子View
的layout
方法触发子View布局 - 如果有特定需求,可以在
onDraw
中进行绘制,例如绘制分隔线
继承View,扩展内容显示能力
一般来说,少数情况下,继承View
或者 特定的Widget
是为了扩展 布局尺寸上的特性
,这基本是从 measure机制
上入手。除此之外,一些场景下,
可以通过 继承View
实现 自定义内容绘制
。
例如,显示图表的View。
这种场景下,一般需要处理:
- 尺寸测量流程中,
Content
的尺寸测量,并在onMeasure
中实现:测量模式为AT_MOST
或UNSPECIFIED
时,利用Content的大小确定显示尺寸。 - 绘制流程中,
onDraw
中实现内容的绘制
注意: 如果并不牵涉到
交互
,这并不是唯一方案,自定义Drawable
的方案,也是很棒的方案。
借用 PhotoView
举个例子,如果交互局限为:双指缩放
,拖拽
,单击
,双击
。
那么通过 OnTouchListener
+ GestureDetector
+ 自定义Drawable
, 对于绝大多数场景,都可以胜任。
扩展交互功能
在这个方向上,主要还是和 事件处理
体系有关。在 View体系
中,存在三个方法
和这个过程直接相关:
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
对于 onInterceptTouchEvent
,非ViewGroup
的 View子类
是不参与的,因为这部分View,已经是事件处理的末端。
话分两头。
对于ViewGroup
扩展的目的一般有二:
- 在恰当的场景下,拦截事件并自身处理,处理逻辑在
onTouch
中实现 - 处理可能存在的
事件处理冲突
,当然,按照Android的规则,利用requestDisallowInterceptTouchEvent
可以要求直系的
所有Parent
不拦截事件。 但难免有意外,可以通过onInterceptTouchEvent
来决定是否自身拦截处理事件,或者更加复杂的场景。
对于View而言
扩展的目的在于 定义事件的含义
举个例子,继承View实现一个字母表导航控件,
点击
、滑动
被定义为切换到对应的字母
进行导航
我们需要在 onTouchEvent
中进行处理。
在前面,我们提到了 PhotoView
的例子,如果:事件
的含义 足够抽象
,例如,对View 进行了:
- 单击
- 双击
- 拖拽
- 缩放
而不是 点击了View的特定区域
,滑动至View的特定位置
等。 我们可以利用 Android屏幕事件处理机制
中的 OnTouchListener
来获取事件信息,
并进行处理。在这种做法中,利用 GestureDetector
可以大大降低这一过程的难度。
总结
这一篇中,我们比较 随性
的思考了 为什么要自定义View
的问题,并展开了:
- 为什么需要这么干
- 具体做法
- 是否有其他方案,并简单交代了
哪种方案更适合
这篇文章比较短,但是这部分内容的背后,还是值得继续深究、挖掘的