三思系列:为什么要自定义View

前言

或许你掌握了 measure的细节layout机制事件传递机制canvas各种API ,但是,你们想过这个问题吗?

这一篇,不仅仅是对一个面试必会题的解析,更是透过这个问题的思考,寻找 最佳实践拓展思维角度少走弯路

三思系列是我最新的学习、总结形式,着重于:问题分析技术积累视野拓展关于三思系列

关于View系列 View系列旨在通过 对现实问题 的思考,建立完善的 View体系认知,极力建议读者了解一下 我为什么撰写、分享这个系列

先给出思考这个问题的 脑图 ,文章内容会按照思考过程展开

guide

思考这类问题,为什么要这样干 是最基本,作为三思系列的成员,本篇还将对以下内容点进行展开论述:

  • 怎么干 -- How to do
  • 是否一定要这样干 -- 适用场景
  • 如果不这样干,还可以怎么干 -- Best Practice
  • 各种干法的 注意事项

从View体系出现的目的说起

作为 GUI Graphical User Interface,图形用户接口 类型的程序 frameworkView体系是其 必不可少 的一部分。参与了两件重要的事情:

  • 描述呈现 界面
  • 参与 人机交互

笼统的讲,当 现有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尺寸测量信息,调用 子Viewmeasure 方法触发测量
  • onLayout 方法中,处理布局,使用子View的 尺寸测量值LayoutParams规则值,计算子View 的布局位置,并调用 子Viewlayout 方法触发子View布局
  • 如果有特定需求,可以在 onDraw 中进行绘制,例如绘制分隔线

继承View,扩展内容显示能力

一般来说,少数情况下,继承View 或者 特定的Widget 是为了扩展 布局尺寸上的特性,这基本是从 measure机制 上入手。除此之外,一些场景下, 可以通过 继承View 实现 自定义内容绘制

例如,显示图表的View。

这种场景下,一般需要处理:

  • 尺寸测量流程中,Content的尺寸测量,并在 onMeasure 中实现:测量模式为 AT_MOSTUNSPECIFIED 时,利用Content的大小确定显示尺寸。
  • 绘制流程中,onDraw 中实现内容的绘制

注意: 如果并不牵涉到 交互,这并不是唯一方案,自定义Drawable的方案,也是很棒的方案。

借用 PhotoView 举个例子,如果交互局限为:双指缩放拖拽单击双击

那么通过 OnTouchListener + GestureDetector + 自定义Drawable, 对于绝大多数场景,都可以胜任。

扩展交互功能

在这个方向上,主要还是和 事件处理 体系有关。在 View体系 中,存在三个方法 和这个过程直接相关:

  • dispatchTouchEvent
  • onInterceptTouchEvent
  • onTouchEvent

对于 onInterceptTouchEvent非ViewGroupView子类 是不参与的,因为这部分View,已经是事件处理的末端。

话分两头。

对于ViewGroup

扩展的目的一般有二:

  • 在恰当的场景下,拦截事件并自身处理,处理逻辑在 onTouch 中实现
  • 处理可能存在的 事件处理冲突,当然,按照Android的规则,利用 requestDisallowInterceptTouchEvent 可以要求 直系的 所有 Parent 不拦截事件。 但难免有意外,可以通过 onInterceptTouchEvent 来决定是否自身拦截处理事件,或者更加复杂的场景。

对于View而言

扩展的目的在于 定义事件的含义

举个例子,继承View实现一个字母表导航控件,点击滑动 被定义为切换到 对应的字母 进行导航

我们需要在 onTouchEvent 中进行处理。


在前面,我们提到了 PhotoView 的例子,如果:事件 的含义 足够抽象,例如,对View 进行了:

  • 单击
  • 双击
  • 拖拽
  • 缩放

而不是 点击了View的特定区域滑动至View的特定位置 等。 我们可以利用 Android屏幕事件处理机制 中的 OnTouchListener 来获取事件信息, 并进行处理。在这种做法中,利用 GestureDetector 可以大大降低这一过程的难度。

总结

这一篇中,我们比较 随性 的思考了 为什么要自定义View 的问题,并展开了:

  • 为什么需要这么干
  • 具体做法
  • 是否有其他方案,并简单交代了 哪种方案更适合

这篇文章比较短,但是这部分内容的背后,还是值得继续深究、挖掘的