前言
或许你掌握了 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 的问题,并展开了:
• 为什么需要这么干。
• 具体做法。
• 是否有其他方案,并简单交代了 哪种方案更适合。
这篇文章比较短,但是这部分内容的背后,还是值得继续深究、挖掘的。