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

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

作者: imkobedroid | 来源:发表于2019-07-08 17:40 被阅读0次

    前言

    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 文章与代码有待改进!希望可以交流

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/ohkahctx.html