Android事件传递分析-滑动冲突解决(内部处理原理)

前言

Android事件传递分析-传递日志分析

Android事件传递分析-OnTouchListener、onTouchEvent、OnClickListener关系

这是事件传递分析的第三章,前两篇我们对事件的传递和消费进行了大致的分析,看过的读者应该能大致明白android事件机制了,现在我们开始处理滑动冲突的问题,其实写到这里很多人都会说滑动冲突解决网上一大把,但是我看了很多文章发现滑动冲突是能解决,但是并没有详细的讲解原理,这篇文章就对滑动冲突内部解决原理做一个简单的分析

事件传递机制

由于我总结的事件传递机制跟网上很多是相似的,所以这里借鉴 放码过来的总结,先做一个预热!

  • 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束
  • 正常情况下,一个事件序列只能被一个View拦截且消耗。因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  • 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  • ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouch-Event方法默认返回false。
  • View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  • View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
  • View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  • onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
  • 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

场景构建

我们这里做一个viewPager包含了HorizontalScrollView的场景,两个都是横向滑动所以会产生一个滑动冲突,我们重写HorizontalScrollView进行内部拦截处理这个滑动冲突,我们让HorizontalScrollView滑动到最右边或者最左边才触发viewpager的滑动事件

布局代码

其中viewpager中的fragment的布局文件如下

 <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
                android:gravity="center"
                android:layout_height="match_parent">

    <com.android.base.weight.MyHorizontalScrollView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:ignore="UselessParent">
        <TextView android:layout_width="match_parent"
                  android:textSize="20sp"
                  android:background="@color/colorPrimary"
                  android:textColor="@color/colorAccent"
                  android:maxLines="1"
                  android:gravity="center"
                  android:text="滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!滑动冲突解决~!"
                  android:layout_height="wrap_content"
                  android:paddingTop="100dp"
                  android:paddingBottom="100dp"/>

    </com.android.base.weight.MyHorizontalScrollView>

</RelativeLayout>

viewpager的代码很常规!就是设置适配器,这里就不多写代码了

运行时界面如下:

image

解决方案

因为横向滑动的viewpager与横向滑动的HorizontalScrollView是有冲突的,我们这里重写HorizontalScrollView来进行内部拦截方法进行处理滑动冲突,我们这里采用的是requestDisallowInterceptTouchEvent方式来通知父组件是否进行onInterceptTouchEvent拦截处理!

整个代码如下:

public class MyHorizontalScrollView extends HorizontalScrollView {
    public MyHorizontalScrollView(Context context) {
        super(context);
    }
    public MyHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                /**
                 *  在dispatchTouchEvent这方法中ACTION_DOWN的时候设置requestDisallowInterceptTouchEvent为true或者false都没得影响,
                 *  因为ACTION_DOWN事件只要父类不手动拦截都会传入他的子view,这里设置为true子view自己处理这个down,设置为false让父组件
                 *  可以进行拦截,但是父组件也不会拦截所以也是传递下来子view处理,所以这里设置false或者true并没有影响,关键是看onTouchEvent中对这个
                 *  down事件的返回值才是关键,因为onTouchEvent返回值直接影响后续事件需不需要这个子view处理
                 *
                 */
                getParent().requestDisallowInterceptTouchEvent(true);

                break;
            case MotionEvent.ACTION_MOVE:
                if (isScrollToRight() || isScrollToLeft()) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                getParent().requestDisallowInterceptTouchEvent(false);
            default:

        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 是否已经滑到了最右边
     */
    private boolean isScrollToRight() {
        return getChildAt(getChildCount() - 1).getRight() == getScrollX() + getWidth();
    }

    /**
     * 是否已经滑到了最左边
     */
    private boolean isScrollToLeft() {
        return getScrollX() == 0;
    }
}

这里代码很简单,就是在dispatchTouchEvent的时候调用根据事件的类型来调用requestDisallowInterceptTouchEvent进行处理,因为我们在滑动到最左边最右边的时候触发viewpager来滑动,所以这里我们做了一个判断

     if (isScrollToRight() || isScrollToLeft()) {
         getParent().requestDisallowInterceptTouchEvent(false);
               }

这里设置成false就是通知父组件这个事件你可以根据你的规则进行拦截,但是其他的事件都默认是true表示我当前自己来处理!这样就很简单的完成了滑动的处理,因为我们继承的是HorizontalScrollView所以这里控件已经帮我们做了onTouchEvent的处理了!这里看不出什么问题,因为只是做了一个简单的通知父组件的操作,想看清楚详细的操作,那我们自己定义一个横向滑动的自定义view吧

自定义横向滑动的view

自定义的具体过程不多讲述,我们这里给出一个界面可以详细的看出我们自定义的样子就行!

image

这里我们做的是拖动小圆球进行左右移动,因为是左右移动所以跟viewpager是产生了滑动冲突的,我们照样重写dispatchTouchEvent方法来进行通知父组件拦截处理

  @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            default:
                getParent().requestDisallowInterceptTouchEvent(false);
        }
        return super.dispatchTouchEvent(event);
    }

疑问解决:为什么我们不做MotionEvent.ACTION_DOWN的requestDisallowInterceptTouchEvent处理呢?

答:跟上边解释的一样,在dispatchTouchEvent这方法中ACTION_DOWN的时候设置requestDisallowInterceptTouchEvent为true或者false都没得影响,因为ACTION_DOWN事件只要父类不手动拦截都会传入他的子view,这里设置为true子view自己处理这个down,设置为false让父组件可以进行拦截,但是父组件也不会拦截所以也是传递下来子view处理,所以这里设置false或者true并没有影响,关键是看onTouchEvent中对这个 down事件的返回值才是关键,因为onTouchEvent返回值直接影响后续事件需不需要这个子view处理

重要的onTouchEvent

因为我们自定义view是继承自viwe的

public class CustomView5 extends View {

    ............省略............
}

这里的自定义view不像上边的继承自HorizontalScrollView,因为继承HorizontalScrollView里面已经处理好了onTouchEvent事件,但是我们这里需要自己手动处理!

我们继续分析,因为前面自定义view前面已经处理好了dispatchTouchEvent,这里我们开始正式我们滑动事件消费的处理,因为消费是在onTouchEvent中,所这里我们要分类型

 public boolean onTouchEvent(MotionEvent event) {
        float oldX;
        switch (event.getAction()) {
            //问题1
            case MotionEvent.ACTION_DOWN:
                //拖动小圆球滑动的处理
                oldX = event.getX();
                setProgressIndex(oldX);
                break;
            case MotionEvent.ACTION_MOVE:
                //拖动小圆球滑动的处理
                setProgressIndex(event.getX());
               //问题2
                return false;
            default:
        }
        return super.onTouchEvent(event);
    }

看到这里应该很多人都会说这个跟正常的onTouchEvent有什么大的区别吗?却是没什么大的区别。但是这里隐藏了很多原理

问题1

因为onTouchEvent方法中只返回了一个super.onTouchEvent,因为大家都知道down事件如果返回为false,那么后续事件就接收不到了,拖动小圆球就不会起作用了,怎么保证我能接受到后面的事件呢?

答:问到这里却是是这样的,但是我们进入super.onTouchEvent的源码里面可以分析:

  /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

         。。。省略代码。。。


        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
              。。。省略代码。。。
            }

            return true;
        }

        return false;
    }

为了方便起见,我这里省略了很多代码,只看返回值的条件

clickable || (viewFlags & TOOLTIP) == TOOLTIP

因为是||所以很简单,满足一个为true即可,我们看到clickable是不是很熟悉?加上前面两片文章我有介绍onTouchEvent会调用OnClickListener来消耗事件,所以为了让down的时候返回为true只需要设置点击事件即可。所以

 view.CustomView5.setOnClickListener {  }

问题解决!
我们再来看下HorizontalScrollView的onTouchEvent源码会发现,里面返回的也是true!

 @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);

        final int action = ev.getAction();

        switch (action & MotionEvent.ACTION_MASK) {
            。。。。。省略。。。。。
        }
        return true;
    }

问题2

前面我们解决了down事件返回为true,后续事件可以接受到,但是为什么在ACTION_MOVE的时候返回为false呢?返回为false那这个事件没有处理咋办?

答:这里我只是做了一个代码的埋点,就是让大家看到这里我返回的是fasle,返回true表示消费了这里就不多说了,因为事件传递的过程中,如果子view接受了down事件,那么后续的事件都该由此view来处理完成,但是这里我们在move的时候返回为false不处理,按照道理他应该返回给父组件来处理,但是这里是有问题的,他并不会给viewpager来处理,所以这里返回false也不会触发viewpager的onTouchEvent而导致滑动冲突,他会直接返回到顶层的容器里面处理或者忽略掉,这是很关键的一点,并且下面的up事件也是一样,所以我这里值返回一个super.onTouchEvent(event)来满足down事件,至于后续事件返回true或者false都不影响我的拖动操作!当然个别情况下是move的返回值的话 要自己手动处理!

总结

滑动事件处理只要懂原理什么都可以迎刃而解,外部拦截是最简单的操作,只需要重写onInterceptTouchEvent根绝自己的情况来判断返回为true拦截即可!内部拦截的话一般requestDisallowInterceptTouchEvent是写在dispatchTouchEvent里面进行分发的操作,然后在onTouchEvent里面进行消耗操作,需要注意的是down事件一定要消耗,至于move或者up事件看自己业务需求而定,像我上边拖动小圆球操作就没得什么特别的要求,只需要走那段代码就行,不需要必须消耗掉那个move事件!冲突解决就写到这里!

需要源码的朋友可以发送请求到邮箱 imkobedroid@gmail.com 文章与代码有待改进!希望可以交流

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

推荐阅读更多精彩内容