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

2019-07-08  本文已影响0人  imkobedroid

前言

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

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

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

事件传递机制

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

场景构建

我们这里做一个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 文章与代码有待改进!希望可以交流

上一篇下一篇

猜你喜欢

热点阅读