Android滑动冲突处理


物语.jpeg

导言

Android中的滑动冲突很常见,例如ScrollView/ListView,ViewPager/ViewPager,相信各位或多或少都了解Android事件分发机制,以及滑动冲突产生的原理。网上相关的文章也很多,并且都讲解的很详细。但那毕竟是别人的成果,我觉得有必要通过一篇文章来记录自己的理解。

大纲

我将从下面几个方面来理解事件分发和解决滑动冲突:

  1. 理解四个方法
  2. Android事件分发机制
  3. 解决滑动冲的思路
  4. 一个滑动冲突场景
  5. 总结
  6. 参考文章

1.理解四个方法

讲到Android事件分发机制和解决滑动冲突,就离不开这四个方法:

  • dispatchTouchEvent(MotionEvent ev)
  • onInterceptTouchEvent(MotionEvent ev)
  • onTouchEvent(MotionEvent ev)
  • requestDisallowInterceptTouchEvent(boolean disallowIntercept)

大概介绍一下前三个方法的关系:

/**
 * 伪代码
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    }
    return child.dispatchTouchEvent(ev);;
}

dispatchTouchEvent()

正如其方法名,该方法是用来传递事件的,传递的顺序是Activity -> ViewGroup -> View
只要事件传递到当前view,就一定会调用该方法,返回结果表示是否消费该事件:

  • true : 消费该事件,不再继续传递
  • false : 事件不再向下传递,并且把事件交给parent处理
  • super : 事件继续 向下传递

onInterceptTouchEvent()

是否拦截事件(不再向下传递),该方法只存在于ViewGroup,一旦拦截,那么当前整个事件序列不会再调用该方法,后续事件都交给当前ViewGroup处理。返回结果表示是否拦截:

  • true : 拦截,事件不再向下传递
  • false/super : 不拦截,事件继续传递

onTouchEvent()

该方法用来处理事件,处理的顺序是View -> ViewGroup -> Activity,返回结果表示是否处理事件:

  • true : 处理事件,不再向下传递
  • false : 不处理事件,同一个事件序列里面,该View无法收到后续事件
  • super : 交给上层View处理

requestDisallowInterceptTouchEvent()

该方法是在子view中请求父view不要拦截事件,参数disallowIntercept的值表示:

  • true : 请求所有父view不要拦截事件,即当前事件序列不走父view的onInterceptTouchEvent()方法,直接向下传递
  • false : 请求所有父View拦截事件,即子view不需要处理该事件,直接交给父view处理

该方法的作用下面还会详细介绍。

2.Android事件分发机制

在介绍事件分发机制之前,先介绍一下事件序列(上文有提到过):


事件序列.png

注:一般情况下,事件列都是以DOWN事件开始,UP事件结束,中间有很多MOVE事件

接下来,用一张图看明白事件传递的过程中的方法调用:


事件分发图.png

假如事件传递不中断的话,方法调用的整个流程如下图:


事件方法调用顺序.png

如果仔细看上面两张图,大家基本就能明白事件的传递流程了,下面我用文字描述一下整个流程:

  1. 事件从Activity的dispatchTouchEvent开始传递,传递给ViewGroup
  2. 如果ViewGroup的onInterceptTouchEvent不拦截事件,则继续向下面(ViewGroup或者View)传递,如果拦截了,则事件直接交给ViewGroup的onTouchEvent处理
  3. 当事件传递到了View,View就会调用onTouchEvent处理事件,正常情况下,还会把事件交给ViewGroup的onTouchEvent处理
  4. ViewGroup处理事件之后,正常情况下,又会交给Activity的onTouchEvent处理。

这里再把几个需要注意的点提一下:

  • 如果ViewGroup的onInterceptTouchEvent方法拦截了事件,事件序列的后续事件不会再调用次方法,也不会向下传递,都直接交给该ViewGroup处理
  • 如果View没有对ACTION_DOWN事件进行消费,事件序列的后续事件都不会传递过来了

3.滑动冲突解决方案

面对滑动冲突,我们可以有2种解决思路:

  1. 外部拦截法:是指我们可以重写parent的onInterceptTouchEvent方法,判断当前的事件是否需要拦截,伪代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean isIntercept = false;
    switch(event.getAction) {
        case MotionEvent.ACTION_DOWN:
            isIntercept = false;
            //todo 记录点击初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            if (子控件不需要处理滑动事件) {
                isIntercept = true;
            } else {
                isIntercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            isIntercept = false;
            break;
    }
    super.onInterceptTouchEvent(event);
    return isIntercept;
}

在这里,down事件必须返回false,不然事件无法传递到子view,后续事件序列都会交给parent处理。而up事件也需要返回false,因为up事件对parent来说没有什么意义,其次若子view处理事件,却没有收到up事件会让子view的onClick事件无法触发。

  1. 内部拦截法:是指我们可以重写child的dispatchTouchEvent方法,判断是否需要让parent拦截事件,伪代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch(event.getAction) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptToucehEvent(true);
            //todo 记录点击初始位置
            break;
        case MotionEvent.ACTION_MOVE:
            if (子控件需要处理滑动事件) {
                getParent().requestDisallowInterceptToucehEvent(true);
            } else {
                getParent().requestDisallowInterceptToucehEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.onDispatchTouchEvent(event);
}

如果parent.requestDisallowInterceptTouchEvent(true)传入参数为true,则parent就不会执行onInterceptTouchEvent方法,直接把事件交给子view处理。
requestDisallowInterceptTouchEvent()方法的逻辑:parent的dispatchTouchEvent()每次down事件,都会把它置为false,即拦截事件,走parent的onInterceptTouchEvent()方法,而在onInterceptTouchEvent()方法中,有一个属性mIsBeingDragged,当dy(滚动距离)>mTouchSlop的时候置为true,down事件和dy<mTouchSlop时为false,最后onInterceptTouchEvent()方法返回mIsBeingDragged,说明即使parent拦截了事件,但滚动距离比较小的时候,事件仍可以传递给子view,子view可以在onTouchEvent方法中调用parent.requestDisallowInterceptTouchEvent()方法

一个滑动冲突的场景

这里举一个很简单的例子
场景:ViewPager嵌套ViewPager的滑动冲突
解决思路:内部拦截法,当子ViewPager的position处于0且dx>0,或者子ViewPager的position处于adapter.count-1且dx<0时,把事件交给父ViewPager,其他时候都是子ViewPager处理即可
代码实现:

package study.self.zf.scrollconflict.widget;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class ChildViewPager extends ViewPager {
    private int mStartX;
    private int mStartY;

    public ChildViewPager(Context context) {
        super(context);
    }

    public ChildViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStartX = (int) ev.getX();
                mStartY = (int) ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = (int) getX() - mStartX;
                int dy = (int) getY() - mStartY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    int position = getCurrentItem();
                    int allCount = getAdapter().getCount();
                    boolean isInterceptByParent = (position == 0 && dx > 0) || ((position == allCount -1) && dx < 0);
                    getParent().requestDisallowInterceptTouchEvent(!isInterceptByParent);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

总结

从以上的讲解可以看出来,滑动冲突并不难,而且思路也很简单,无非就是从dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()、requestDisallowInterceptTouchEvent()方法入手,分析什么时候parent处理事件,什么时候子view处理事件即可。

参考文章:

https://www.jianshu.com/p/ff3b55441444
https://www.jianshu.com/p/7e92121814ed

写于2018.05.21下午18:00(位置:深圳南山)

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

推荐阅读更多精彩内容