Android手势拦补点

阅读: 评论:0

Android手势拦补点

Android手势拦补点

一、 前言

在Android日常开发中,我们时有处理业务中手势的需求,即:基于事件的拦截、分发、消费三个回调,判断手势逻辑。

我们知道,当一个View消费了ACTION_DOWN事件,才可以接受到后续的事件,反之无法收到后续事件。那么如果一个View消费了事件后,判断为自己不需要的事件,又想将事件重新传递给子View处理怎么办呢?

这就引出了本文的要点——手势拦补点操作,以Android Q为例,进行介绍。


二、 拦点

1. 什么是拦点

拦点,即拦截触摸事件点位,不让事件向下传递。这并非我们常规的onInterceptTouchEvent方法中返回一个true来实现拦截这么简单,而是宏观上的以应用或窗口为单位的拦截,比如Android Q开始原生支持的手势交互。


2. 为什么拦点

以OPPO手机ColorOS 7为例,在设置-便捷辅助-导航键中选择两侧滑动手势,打开一个应用,我们在屏幕两侧偏下的边缘,向内滑动,可以触发手势操作:

而在这个位置点击,如果对应位置有按钮之类的控件(比如设置页的Preference项),也可以响应点击事件:

根据这些,我们提出疑惑,手势图形和应用页面二者应该属于不同的窗口,且手势的窗口层级高于应用,一个事件同一时刻应该只有一个地方可以消费,那么这里是如何实现二者均可以消费呢?这便是基于拦点实现的。


3. 怎么拦点

由于这部分功能的具体实现,位于每个ROM厂商的非开放仓库中,这里仅从技术侧介绍大致的实现思想,不同厂商之间实现原理大致相同:

对于一个窗口,事件的源头为ViewRootImpl类中InputEventReceiver的实例,其接收来自于硬件层传感器经Framework层传到App层的Input事件,并通过DecorView向整个View树进行深度优先的传递。

所以,每一个MotionEvent通过注册有InputEventReceiver的事件通道,先经过ViewRootImpl,之后才会传递到应用中的View。

当事件来到,在ViewRootImpl中会进行拦截和判断,是否满足当前用户选择的手势的判定,同时将每一个事件点存进cache中。假设以10个点为阈值,10个点内判断手势成功,则响应手势的逻辑。如果10个点判断手势失败,则不再判断手势,将cache中的点位全部分发下去,且后续的事件来到后也不再拦截,直接分发。

  • ViewRootImpl中做的这套拦截逻辑,即为拦点,它避免了手势的事件误传到应用层。
  • 手势图形绘制位于另一个窗口层级更高的常驻进程,从而保证绘制的层级能在绝大部分窗口之上,其也注册了InputEventReceiver作为事件通道。
  • cache中的点位重新分发的过程即为补点,这将在下一节介绍。

三、 补点

1. 什么是补点

补点,即补充触摸事件点位,让事件重新沿系统传递链传递。这也并非我们主动去调用dispatchTouchEventonTouchEvent方法来传递一个MotionEvent,而是真正地模拟用户触摸事件,从底层到上层的传递,走一遍完整的流程,最终将返回值交给ViewRootImpl处理。


2. 为什么补点

假设我们有这样一个需求,一个父View里面塞了很多个子View,产品希望以父View为整体,可以实现在全屏内跟手拖动,而里面的子View各自又可以响应点击事件。

这个需求拆分成两部分:

  • 父View跟手位移:拦截事件,自己消费,实时更新父View坐标
  • 子View响应点击事件:设置点击监听器

问题来了,当父View拦截并开始消费事件后,子View因为没有ACTION_DOWN消费,是无法收到后续事件的,因此永远无法正常响应点击事件。

那么怎么实现既可以让父View消费,又可以让子View消费呢?这就需要用到补点。


3. 怎么补点

对于该需求场景,如果我们在跟手位移结束,即ACTION_UP时,判断本次操作应该为一次click事件,然后将这个click事件对应的ACTION_DOWNACTION_UP传给系统,重新从ViewRootImpl向下传递给子View,便可解决该问题。

这个补偿事件点的操作称为补点。

与触摸事件相关的系统服务为InputManagerService,其对应用层开放的管理类为InputManager,其中有一个方法injectInputEvent,用于主动注入Input事件到IMS中:

遗憾的是,它是一个Hide API,且需要INJECT_EVENTS权限,即仅向系统层或有特权的系统应用(拥有AOSP证书签名、安装目录位于/system/app/中)开放。

幸运的是,可以使用替代方案Instrumentation

通过该API,可以间接地向系统注入点击事件对应的两个MotionEvent。需要注意的是:

  • 需在子线程调用
  • MotionEvent通过MotionEvent#obtain构造
  • 时间戳使用SystemClock#uptimeMillis()

由于我们在父View进行了事件拦截以使自身消费事件,因此这里构造的MotionEvent需要通过setEdgeFlags设置一个特定的标识,当拥有标识的事件经过onInterceptTouchEvent方法时,对其放行,使其正常分发。

以上便是一次完整的补点操作。


4. 实战

以上一节补点的需求案例为例:

  • 在一个名为Container的父View中,放入一个ImageView和两个Button
  • Container为整体,在全屏内可以跟手拖动
  • Container里面的子View各自能响应点击事件

(1) 自定义ViewGroup实现跟手拖动

  • 由于需要全屏范围内拖动,父布局需使用FrameLayout
  • 自定义ViewGroup这里继承ConstraintLayout
  • 重写onInterceptTouchEvent返回true,使事件全部由自身消费
  • 重写onTouchEvent,保存ACTION_DOWN时的落点和View的坐标,以计算相对位移,ACTION_MOVE时更新View的坐标,实现跟手拖动
  • 位移时判断是否超出屏幕,限制坐标边界值

(2) 判断是否满足点击事件

  • 点击事件的判定通常需要考虑位移差间隔时长两个因素,如果严谨一点还可以加上对离手速度的判断
  • 本例中,ACTION_DOWNACTION_UP的横纵坐标差均小于50px,间隔时长小于250ms,不考虑离手速度

(3) 注入事件到系统

  • 在子线程中实例化一个Instrumentation实例
  • 根据离手坐标构造MotionEvent
  • 对其设置edgeFlags,注意不能和系统标识位重复
  • 连续注入两个事件

(4) 拦截回调中放行注入的事件

  • 如果事件的edgeFlags等于自定义的标识,则不拦截事件,使其分发给子View

(5) 运行效果


四、 总结

拦补点思想用于解决一些手势冲突问题,其本质还是基于事件分发机制,即:InputEventReceiver->ViewRootImpl->PhoneWindow->DecorView->ViewGroup->View这一分发链。

因此对这一机制越熟悉,运用起来也就越得心应手。

本文发布于:2024-01-29 19:34:26,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170652807017774.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:手势   Android   拦补点
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23