Android 系统的触摸事件分发老是从父布局开始分发,从最顶层的子 View 开始处理,这种特性有时候会限制了咱们一些很复杂的交互设计。git
TouchEventBus
致力于解决非嵌套的滑动冲突,好比多个 在同一层级 的Fragment
对触摸事件的处理:触摸事件会先到达顶层Fragment
的onTouch
方法,而后逐层判断是否消费,在都不消费的状况下才到达底层的Fragment
。并且这些层级互不嵌套,没有造成 parent 和 child 的关系,意味着想经过onInterceptTouchEvent()
或者requestDisallowInterceptTouchEvent()
方法来调整事件分发都是不可能的。github
下面是手机YY的开播预览页:maven
在这个页面上有不少对触摸事件的处理,包括且不限于:ide
ViewPager
,从“直播”和“玩游戏”两个选项卡之间切换Activity
,因此这个 Activity
上还有不少开播后的 Fragment
,好比公屏等等也有触摸事件从视觉上能够判断出View Tree的层级以及对触摸处理的层级:布局
图左侧是 UI 的层级,上层是一些按钮控件和 ViewPager
,下层是视频流展现的 Fragment
。右边是触摸事件处理的层级,双指缩放/View点击/聚焦点击须要在 ViewPager
上面,不然都会被 ViewPager
消费掉,可是 ViewPager
的 UI 层级又比视频的 Fragment
要高。这就是非嵌套的滑动冲突的核心矛盾:gradle
业务逻辑的层级 与 用户看到的UI层级 不一致ui
手机YY直播间中的 Fragment
很是多,并且由于插件化的缘由,各个业务插件能够动态地往直播间添加/移除本身业务的 Fragment
,这些 Fragment
层级相同互不嵌套,有本身比较独立的业务逻辑,也会有点击/滑动等事件处理的需求。但因为业务场景复杂,Fragment
的上下层级顺序也会动态改变,这就很容易致使一些 Fragment
一直收不到触摸事件或者在切换业务模板的时候触摸事件被其余业务消费。this
TouchEventBus
用于这种场景下对触摸事件进行从新分发,咱们能够为所欲为地决定业务逻辑的层级顺序。url
每一个手势的处理就是一个 TouchEventHandler
,好比镜头的缩放是 CameraZoomHandler ,镜头的聚焦点击是 CameraClickHandler ,ViewPager
滑动是 PreviewSlideHandler ,而后为这些 Handler 从新排序,按照业务的须要来传递 MotionEvent
。而后是 TouchEventHandler
和ui的对应关系:经过Handler的 attach
/ dettach
方法来绑定/解绑对应的 ui 。而 ui 能够是一个具体的 Fragment
,也能够是一个抽象的接口,一个对触摸事件做出响应的业务。spa
好比开播预览页的聚焦点击处理,先是定义ui的接口:
public interface CameraClickView {
/** * 在指定位置为中心显示一个黄色矩形的聚焦框 * * @param x 手指触摸坐标x * @param y 手指触摸坐标y */
void showVideoClickFocus(float x, float y);
/** * 给VideoSdk传递触摸事件,让其在指定坐标进行摄像头聚焦 * * @param e 触摸事件 */
void onTouch(MotionEvent e);
}
复制代码
而后是 TouchEventHandler
的定义:
public class CameraClickHandler extends TouchEventHandler<CameraClickView> {
private boolean performClick = false;
//...
@Override
public boolean onTouch(@NonNull CameraClickView v, MotionEvent e, boolean hasBeenIntercepted) {
super.onTouch(v, e, hasBeenIntercepted);
if (!isCameraFocusEnable()) { //一些特殊业务须要禁止摄像头聚焦
return false;
}
//经过MotionEvent判断performClick是否为true
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
//...
break;
case MotionEvent.ACTION_MOVE:
//...
break;
case MotionEvent.ACTION_UP:
//...
break;
default:
break;
}
if (performClick) { //认为是点击行为,调用ui的接口
v.showVideoClickFocus(e.getRawX(), e.getRawY());
v.onTouch(e);
}
return performClick; //点击的时候消费掉触摸事件
}
}
复制代码
最后是 TouchEventHandler
与 ui 的对应的绑定
public class MobileLiveVideoComponent extends Fragment implements CameraClickView{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//...
//CameraClickHandler与当前Fragment绑定
TouchEventBus.of(CameraClickHandler.class).attach(this);
}
@Override
public void onDestroyView() {
//...
//CameraClickHandler与当前Fragment解绑
TouchEventBus.of(CameraClickHandler.class).dettach(this);
}
@Override
public void showVideoClickFocus(float x, float y) {
//todo: 展现一个黄色框ui
}
@Override
public void onTouch(MotionEvent e) {
//todo: 调用SDK的摄像头聚焦
}
}
复制代码
当用户对ui的进行手势操做时,MotionEvent
就会沿着 TouchEventBus
里面的顺序进行分发。若是在 CameraClickHandler 以前没有别的 Handler 把事件消费掉,那么就能在 onTouch
方法进行处理,而后在 ui 做出响应。
多个 TouchEventHandler
之间须要定义一个分发的顺序,最早接收到触摸事件的 Handler 能够拦截后面的 Handler。在顺序的定义上,很难固定一条绝对的分发路线,由于随着直播间模版的切换,Fragment
的层级可能会产生变化。 因此 TouchEventBus
使用相对的顺序定义。每一个 Handler 能够决定要拦截哪些其余的 Handler。好比要把 CameraClickHandler 排在其余几个Handler前面:
public class CameraClickHandler extends AbstractTouchEventHandler<CameraClickView> {
//...
@Override
public boolean onTouch(@NonNull CameraClickView v, MotionEvent e, boolean hasBeenIntercepted) {
//...
}
/** * 定义哪些Handler须要排在个人后面 **/
@Override
protected void defineNextHandlers(@NonNull List<Class<? extends TouchEventHandler<?, ? extends TouchViewHolder<?>>>> handlers) {
//下面的Handler都会在CameraClickHandler后面,但他们之间的顺序还未定义
handlers.add(CameraZoomHandler.class);
handlers.add(MediaMultiTouchHandler.class);
handlers.add(PreviewSlideHandler.class);
handlers.add(VideoControlTouchEventHandler.class);
}
}
复制代码
每一个 Handler 都会指定排在本身后面的 Handler,从而造成一张图。经过拓扑排序咱们就能动态地得到一条分发路径。下图的箭头指向 “A->B” 表示A须要排在B的前面:
在直播间模版切换的时候,任何一个 Handler 均可以动态地添加到这个图当中,也能够从这个图中随时移除,不会影响其余业务的正常进行。
互不嵌套的 Fragment
层级才须要使用 TouchEventBus
,Fragment
内部用 Android 默认的触摸事件分发。以下图:红色箭头部分为 TouchEventBus
的分发,按 Handler 的拓扑顺序进行逐层调用。蓝色箭头部分为 Fragment
内部 ViewTree 的分发,彻底依照 Android 系统的分发顺序,即从父布局向子视图分发,子视图向父布局逐层决定是否消费。
运行本工程的 TouchSample 模块,是一个使用 TouchEventBus
的简单 Demo 。
ui的层级:Activity -> 背景图 -> 侧边面板 -> 选项卡 -> 文本框
触摸处理的顺序:侧边面板 -> 文本缩放 -> 背景图滑动 -> 底部导航点击 -> 选项卡滑动
这里还作了一个操做是:让底部导航点击不消费触摸事件。因此你能够在底部的导航栏区域上左右滑动,切换的是一级Tab。而在背景图区域左右滑动,切换的是二级Tab。
在项目 build.gradle 添加仓库地址
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
复制代码
对应模块添加依赖
dependencies {
compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3'
}
复制代码