Android笔记---事件分发和冲突解决

一、事件分发简介

1. View 触摸事件

对于屏幕的点击,滑动,抬起等一系的动作,其实都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:

(1)ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
(2)ACTION_MOVE:手指在屏幕上移动时候产生该事件
(3)ACTION_UP:手指从屏幕上松开的瞬间产生该事件
(4)ACTION_CANCEL 当前 View 的手势被打断,后续不会再收到任何事件从 ACTION_DOWN 开始到 ACTION_UP/ACTION_CANCEL 结束我们称为一个事件序列正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面 3 种情况。

1.点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP
2.点击后再风骚的滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
3.某些情况下,我们可能会没有收到 ACTION_UP 事件,是收到 ACTION_CANCEL 事件。

ACTION_CANCEL 一般是指 ChildView 原先拥有事件处理权,后面由于某些原因,该处理权需要交回给上层去处理,ChildView便会收到ACTION_CANCEL事件。对于一些复位或者重置操作,我们应该在 ACTION_UP 和 ACTION_CANCEL 里面同时进行处理。

代码逻辑上是:上层判断之前交给ChildView的事件处理权需要收回来了,便会做事件的拦截处理,拦截时给ChildView发一个ACTION_CANCEL事件。

2. 主要方法

View 的事件分发机制主要涉及到以下几个方法:

  • dispatchTouchEvent ,这个方法主要是用来分发事件的
  • onInterceptTouchEvent,这个方法主要是用来拦截事件的(需要注意的是 ViewGroup 才有这个方法,View 没有
    onInterceptTouchEvent 这个方法)
  • onTouchEvent 这个方法主要是用来处理事件的
  • requestDisallowInterceptTouchEvent(true),这个方法能够影响父View是否拦截事件,true 表示父
    View 不拦截事件,false 表示父 View 拦截事件

3.图解事件处理流程

在这里插入图片描述

  • 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
  • 事件从最上边的黑色虚线箭头开始,由 Activity 的 dispatchTouchEvent 进行分发
  • 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super
    的意思是调用父类实现。)
  • dispatchTouchEvent和
    onTouchEvent两个如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
  • 目前所有的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP我们最后做分析

当触摸事件发生时,首先 Activity 将 TouchEvent 传递给最顶层的 View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发,如果dispatchTouchEvent返回true 消费事件,事件终结。如果dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;如果dispatchTouchEvent返回super的话,默认会调用自己的onInterceptTouchEvent方法。

  • 默认的情况下onInterceptTouchEvent回调用super方法,super方法默认返回false,所以会交给子View的onDispatchTouchEvent方法处理
  • 如果 interceptTouchEvent 返回 true ,也就是拦截掉了,则交给它的 onTouchEvent 来处理,
  • 如果 interceptTouchEvent 返回 false ,那么就传递给子 view ,由子 view 的
    dispatchTouchEvent 再来开始这个事件的分发。

二、事件冲突解决

在开发当中,View 的滑动冲突时经常遇到的,比如 ViewPager 嵌套 ViewPager、ScrollView 嵌套 ViewPager、ViewPager嵌套RecyclerView和ScrollView嵌套RecyclerView等等,下面让我们一起来看看怎么解决。

1.常见的冲突情况

(1)第一种情况,滑动方向不同

Android知识库 > Android事件分发和冲突解决 > image2021-7-26_15-10-2.png

(2)第二种情况,滑动方向相同

Android知识库 > Android事件分发和冲突解决 > image2021-7-26_15-13-35.png

在这里插入图片描述

(3)第三种情况,上述两种情况的多级嵌套

Android知识库 > Android事件分发和冲突解决 > image2021-7-26_15-23-51.png

2.如何解决

看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突。那既然同一时刻只能由某一个 View 或者 ViewGroup 消费拦截,那我们就只需要决定在某个时刻由这个 View 或者 ViewGroup 拦截事件,另外的 某个时刻由 另外一个 View 或者 ViewGroup 拦截事件,不就 OK了吗?

这里针对第一种冲突情况介绍一下解决方法:

(1)第一种解决方法

从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不要的时候返回false,代码大概如下

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
	//当前滑动x位置
    final float x = ev.getX();
    //当前滑动y位置
	final float y = ev.getY();

    final int action = ev.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN:
			//按下时x位置
            mDownPosX = x;
			//按下时y位置
            mDownPosY = y;

            break;
        case MotionEvent.ACTION_MOVE:
			//计算x滑动距离
            final float deltaX = Math.abs(x - mDownPosX);
			//计算y滑动距离
            final float deltaY = Math.abs(y - mDownPosY);
            // 这里可以判断是否是左右滑动来控制父View是否拦截此事件
            if (deltaX > deltaY) {
				//若x方向的滑动距离大于y方向的滑动距离则此次滑动判断为左右滑动,父View可以拦截此事件消费
                return true;
            }else {
				// 这一步可以不写,父View 返回false和return super.onInterceptTouchEvent(ev)的效果是一样的
				return false;
			}
    }

    return super.onInterceptTouchEvent(ev);
}

(2)第二种解决方法

从子View着手,父View先不要拦截任何事件,所有的事件传递给 子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View处理。实现思路 如下,重写子 View的dispatchTouchEvent方法,在ACTION_DOWN动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父 View不要拦截事件,这样保证子 View 能够接受到ACTION_MOVE事件,再在ACTION_MOVE动作中根据自己的逻辑是否要拦截事件,不需要拦截事件的话再交给 父 View 处理。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    int x = (int) ev.getRawX();
    int y = (int) ev.getRawY();
    int dealtX = 0;
    int dealtY = 0;

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            dealtX = 0;
            dealtY = 0;
            // 保证子View能够接收到Action_move事件,不让父View拦截事件
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            dealtX += Math.abs(x - lastX);
            dealtY += Math.abs(y - lastY);
            Log.i(TAG, "dealtX:=" + dealtX);
            Log.i(TAG, "dealtY:=" + dealtY);
            // 判断是否是上下滑动,如是上下滑动则在子View消费事件
            if (dealtX >= dealtY) {
                getParent().requestDisallowInterceptTouchEvent(true);
            } else {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_CANCEL:
            break;
        case MotionEvent.ACTION_UP:
            break;

    }
    return super.dispatchTouchEvent(ev);
}

(3)项目中遇到滑动冲突的解决方法

如下面这张设计图,想要的效果是两个页面来回切换,页面的Item可以侧滑拉出删除按钮,这里的布局是由ViewPager+Fragment+RecyclerView设计的,但是做出来的效果,Recyclerview侧滑和ViewPager侧滑会产生冲突,导致RecyclerView的Item侧滑卡顿甚至滑不出来。

Android知识库 > Android事件分发和冲突解决 > image2021-7-27_14-20-24.png

这种情况的解决思路可以从内部子View即RecyclerView入手,首先判断是否在RecyclerView中进行滑动,在判断删除按钮是否已经滑出,进而将滑动事件交由父ViewGroup处理。

主要解决代码如下:

/**
 * 重写RecyclerView的dispatchTouchEvent 解决ViewPager滑动冲突  导致侧滑删除按钮滑动卡顿的问题
 *
 * @param ev MotionEvent
 * @return boolean
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    initViewParent();
    if (viewParent == null) {
        return super.dispatchTouchEvent(ev);
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = (int) ev.getX();
            startY = (int) ev.getY();
            //recyclerview中按下后交由recyclerview处理
            viewParent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            int endX = (int) ev.getX();
            int endY = (int) ev.getY();
            int scrollX = endX - startX;
            int disX = Math.abs(scrollX);
            int disY = Math.abs(endY - startY);
            if (disX < disY) {
                //如果是横向滑动,告知父布局不进行事件拦截,交由子布局消费, requestDisallowInterceptTouchEvent(true)
                viewParent.requestDisallowInterceptTouchEvent(canScrollHorizontally(startX - endX));
            }
            //这里判断左滑且删除按钮已关闭的情况  滑动事件交由父布局处理
            if (scrollX > 0 && mFlingView.getScrollX() == 0) {
                viewParent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            viewParent.requestDisallowInterceptTouchEvent(false);
            break;
    }
    return super.dispatchTouchEvent(ev);
}

/**
 * 初始化viewParent
 */
private void initViewParent() {
    viewParent = getViewParent();
}

/**
 * 递归查找ViewPager父布局  没有就返回空
 *
 * @return ViewParent
 */
private ViewParent getViewParent() {
    if (viewParent == null) {
        viewParent = getParent();
    } else {
        viewParent = viewParent.getParent();
    }
    if (viewParent == null) {
        return null;
    }
    if (viewParent instanceof ViewPager) {
        return viewParent;
    } else {
        return getParent();
    }
}


参考:
View 事件分发机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值