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


非嵌套滑动 | 嵌套滑动

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

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

同级视图的触摸事件

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

YY预览页
YY预览页

在这个页面上有很多对触摸事件的处理,包括且不限于:

  • 在屏幕上点击,会触发摄像头的聚焦(黄色框出现的地方)
  • 双指缩放,会触发摄像头的缩放
  • 左右滑动,可以切换 ViewPager ,从“直播”和“玩游戏”两个选项卡之间切换
  • “玩游戏”选项卡上的列表可以滑动
  • “直播”选项卡上的控件可以点击(开播按钮,添加图片…)
  • 由于预览页和开播页是同一个 Activity ,所以这个 Activity 上还有很多开播后的 Fragment,比如公屏等等也有触摸事件

从视觉上可以判断出View Tree的层级以及对触摸处理的层级:

处理顺序
处理顺序

图左侧是 UI 的层级,上层是一些按钮控件和 ViewPager ,下层是视频流展示的 Fragment。右边是触摸事件处理的层级,双指缩放/View点击/聚焦点击需要在 ViewPager上面,否则都会被 ViewPager 消费掉,但是 ViewPager 的 UI 层级又比视频的 Fragment 要高。这就是非嵌套的滑动冲突的核心矛盾:

业务逻辑的层级用户看到的UI层级 不一致

对触摸事件的重新分发

手机YY直播间中的 Fragment 非常多,而且因为插件化的原因,各个业务插件可以动态地往直播间添加/移除自己业务的 Fragment ,这些 Fragment 层级相同互不嵌套,有自己比较独立的业务逻辑,也会有点击/滑动等事件处理的需求。但由于业务场景复杂,Fragment 的上下层级顺序也会动态改变,这就很容易导致一些 Fragment 一直收不到触摸事件或者在切换业务模板的时候触摸事件被其他业务消费。

TouchEventBus 用于这种场景下对触摸事件进行重新分发,我们可以随心所欲地决定业务逻辑的层级顺序。

TouchEventBus重新分发触摸事件
TouchEventBus重新分发触摸事件

每个手势的处理就是一个 TouchEventHandler,比如镜头的缩放是 CameraZoomHandler ,镜头的聚焦点击是 CameraClickHandlerViewPager 滑动是 PreviewSlideHandler ,然后为这些 Handler 重新排序,按照业务的需要来传递 MotionEvent 。然后是 TouchEventHandler 和ui的对应关系:通过Handler的 attach / dettach 方法来绑定/解绑对应的 ui 。而 ui 可以是一个具体的 Fragment,也可以是一个抽象的接口,一个对触摸事件作出响应的业务。

比如开播预览页的聚焦点击处理,先是定义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
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'
    }
    

项目地址

https://github.com/YvesCheung/TouchEventBus

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容