一种非嵌套滑动冲突的解决方案


非嵌套滑动 | 嵌套滑动

Android 系统的触摸事件分发老是从父布局开始分发,从最顶层的子 View 开始处理,这种特性有时候会限制了咱们一些很复杂的交互设计。git

TouchEventBus 致力于解决非嵌套的滑动冲突,好比多个 在同一层级Fragment 对触摸事件的处理:触摸事件会先到达顶层 FragmentonTouch 方法,而后逐层判断是否消费,在都不消费的状况下才到达底层的 Fragment 。并且这些层级互不嵌套,没有造成 parent 和 child 的关系,意味着想经过 onInterceptTouchEvent() 或者 requestDisallowInterceptTouchEvent() 方法来调整事件分发都是不可能的。github

同级视图的触摸事件

下面是手机YY的开播预览页:maven

YY预览页

在这个页面上有不少对触摸事件的处理,包括且不限于: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

TouchEventBus从新分发触摸事件

每一个手势的处理就是一个 TouchEventHandler,好比镜头的缩放是 CameraZoomHandler ,镜头的聚焦点击是 CameraClickHandlerViewPager 滑动是 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 均可以动态地添加到这个图当中,也能够从这个图中随时移除,不会影响其余业务的正常进行。

嵌套的视图用 Android 系统的触摸分发

互不嵌套的 Fragment 层级才须要使用 TouchEventBusFragment 内部用 Android 默认的触摸事件分发。以下图:红色箭头部分为 TouchEventBus 的分发,按 Handler 的拓扑顺序进行逐层调用。蓝色箭头部分为 Fragment 内部 ViewTree 的分发,彻底依照 Android 系统的分发顺序,即从父布局向子视图分发,子视图向父布局逐层决定是否消费。

触摸事件分发

使用例子

运行本工程的 TouchSample 模块,是一个使用 TouchEventBus 的简单 Demo 。

TouchSample

  • 单指左右滑动切换选项卡
  • 双指缩放中间的"Tab%_subTab%"文本框
  • 双指左右滑动切换背景图
  • 滑动屏幕左侧拉出侧边面板

ui的层级:Activity -> 背景图 -> 侧边面板 -> 选项卡 -> 文本框

触摸处理的顺序:侧边面板 -> 文本缩放 -> 背景图滑动 -> 底部导航点击 -> 选项卡滑动

这里还作了一个操做是:让底部导航点击不消费触摸事件。因此你能够在底部的导航栏区域上左右滑动,切换的是一级Tab。而在背景图区域左右滑动,切换的是二级Tab。

配置

  1. 在项目 build.gradle 添加仓库地址

    allprojects {
        repositories {
            maven { url 'https://jitpack.io' }
    	}
    }
    复制代码
  2. 对应模块添加依赖

    dependencies {
        compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3'
    }
    复制代码

项目地址

github.com/YvesCheung/…

相关文章
相关标签/搜索