Android 事件分发总结

事件分发

事件分发就是用户触摸屏幕所产生的一系列事件的传递。


常见的事件类型
//MotionEvent.java
public static final int ACTION_DOWN             = 0; //手指按下
public static final int ACTION_UP               = 1; //手指松开
public static final int ACTION_MOVE             = 2; //手指移动
public static final int ACTION_POINTER_DOWN     = 5; //多指的按下(按下之前已经有手指在屏幕上)
public static final int ACTION_POINTER_UP       = 6; //多指的松开(松开之后仍然有手指在屏幕上)

/*
ACTION_CANCEL:告诉当前View你再也不会收到事件了,事件已经被某父布局拦截了。
当按下事件传递给某View处理了,那么后续的事件也会交由该View处理(因为事件分发通过传递按下事件来确认处理者)。
如果该View的某父布局在onInterceptTouchEvent对事件进行拦截,该View就会收到ACTION_CANCEL事件。
后续的所有事件都会被那拦截的父布局偷走。
*/
public static final int ACTION_CANCEL           = 3;

/*
ACTION_OUTSIDE:按下事件在View区域以外的地方发生就会收到这个事件,而且只有第一次点击才能收到。
注意:收到该事件是有前提的。必须设置两个LayoutParams flag
		 WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
		 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

PopupWindow点击其他区域,自动关闭的原理就是用这个实现的。
*/
public static final int ACTION_OUTSIDE          = 4;

MotionEvent常用的方法
getAction()	//获取事件类型。
getX()			//获得触摸点在当前 View 的 X 轴坐标。
getY()			//获得触摸点在当前 View 的 Y 轴坐标。
getRawX()		//获得触摸点在整个屏幕的 X 轴坐标。
getRawY()		//获得触摸点在整个屏幕的 Y 轴坐标。

//多指触控相关方法
getActionMasked()	//与 getAction() 类似,多点触控必须使用这个方法获取事件类型。
getActionIndex()	//获取产生事件的手指下标
getPointerCount()	//获取在屏幕上手指的个数
getPointerId(int pointerIndex)	//获取一个手指ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)	//通过手指ID获取手指下标,之后通过下标获取其他内容。
getX(int pointerIndex)	//获取某一个手指的X坐标
getY(int pointerIndex)	//获取某一个手指的Y坐标
  
/*
getAction() 与 getActionMasked() 的区别
多指触控下会产生大量事件,如何在获取事件类型的同时区分这些事件就是一个大问题。

谷歌工程师通过 int类型共32位(0x00000000),他们用最低8位(0x000000ff)表示事件类型,再往前的8位(0x0000ff00)表示事件编号

以手指按下为例:
ACTION_DOWN 的默认数值为 (0x00000000)
ACTION_POINTER_DOWN 的默认数值为 (0x00000005)
那么
第1个手指按下	ACTION_DOWN (0x00000000)
第2个手指按下	ACTION_POINTER_DOWN (0x00000105)
第3个手指按下	ACTION_POINTER_DOWN (0x00000205)
第4个手指按下	ACTION_POINTER_DOWN (0x00000305)

通过getActionMasked()方法除了能拿到事件外,还可以拿到手指下标

1、多点触控时必须使用 getActionMasked() 来获取事件类型。
2、单点触控时由于事件数值不变,使用 getAction() 和 getActionMasked() 两个方法都可以。
3、使用 getActionIndex() 可以获取到这个index数值。不过请注意,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。
*/

详情文章:MotionEvent详解


requestDisallowInterceptTouchEvent() - 阻止父布局拦截事件

子View为了防止父布局在onInterceptTouchEvent()中拦截掉事件。

可以通过在自己的dispatchTouchEvent()内调用getParent().requestDisallowInterceptTouchEvent(true);,来让父布局不再进入onInterceptTouchEvent()方法,从而阻止事件被父布局拦截。


事件分发机制

事件分发相关方法

dispatchTouchEvent()用来进行事件的分发,如果MotionEvent(点击事件)能够传递给该View,那么该方法一定会被调用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同决定。

返回值为true,则表示该点击事件被本身或者子View消耗。

返回值为false,则表示该ViewGroup没有子元素,或者子元素没有消耗该事件。

onInterceptTouchEvent():在dispatchTouchEvent()中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中不会再访问该方法。

onTouchEvent():在dispatchTouchEvent()中调用,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中View不会再次接收到事件。


事件分发流向

事件分发从上往下依次是Activity、ViewGroup、View。如果事件不被中断整个事件流向是一个类U型图

在这里插入图片描述


消费事件的总结:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

详情文章:图解 Android 事件分发机制


伪代码看事件分发方法的关系
// 点击事件产生后
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean consume = false; //代表 是否会消费事件

    // 步骤2:判断是否拦截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若拦截,则将该事件交给当前View进行处理
      // 即调用onTouchEvent()去处理点击事件
      consume = onTouchEvent (ev) ;

    } else {

      // b. 若不拦截,则将该事件传递到下层
      // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
      // 直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }

    // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
    return consume;

}

事件分发源码

直接看这文章简单易懂。大佬真的讲得很好,膜拜!!!

Android事件分发机制详解:史上最全面、最易懂


View的OnTouchListener解析

没用重写View或者ViewGroup的话,可以通过setOnTouchListener()方法来监听触摸事件。

OnTouchListener优先于内部onTouchEvent执行,并且通过onTouch回调用通过返回值来控制事件传递,让View的onTouchEvent不执行。

//View#dispatchTouchEvent中那么一段逻辑
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
		&& (mViewFlags & ENABLED_MASK) == ENABLED
		&& li.mOnTouchListener.onTouch(this, event)) { //1、当onTouch返回true的话
				result = true;
}

if (!result && onTouchEvent(event)) { //2、那么View#onTouchEvent是不会执行的
		result = true;
}
...
return result;

滑动冲突与解决

解决滑动冲突的方法只有两个,总结就是外敷内服


例子:父布局的左右滑动与子视图的左右滑动起冲突,最明显的现象是在子视图区域进行滑动,父布局跟着发生了滑动。


外敷 - 外部拦截

解决冲突,从父布局入手。核心:找出冲突区域按需分配

上面例子用外部拦截的方案解决的话。父布局中要重写onInterceptTouchEventonTouchEvent方法,在按下事件时获取子视图区域。如果手指在子视图区域内,那么就不拦截事件传给子视图并且禁止自己的滑动逻辑的执行。反之手指不在子视图区域直接拦截事件给自己。

		private boolean isIntercept;
    private Rect childViewRect;

    private Rect getChildViewRectFromId(@IdRes int id){
        View view = this.findViewById(id);
        if (view != null){
            Rect rect = new Rect();
            view.getGlobalVisibleRect(rect); //返回该view的全局矩阵
            return rect;
        }
        return null;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                childViewRect = getChildViewRectFromId(R.id.a_view);
                break;
            case MotionEvent.ACTION_MOVE:
                if (childViewRect != null){
                    //手指在子视图区域内,就不拦截
                    isIntercept = !childViewRect.contains((int) event.getRawX(), (int) event.getRawY());
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                childViewRect = null;
                break;
        }
        return isIntercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!isIntercept) return true; //防止子视图onTouchEvent没有消费掉事件,回传回父布局
        
      	//滑动逻辑...
    }

内服 - 内部拦截

解决冲突,从子视图入手。核心:阻止事件拦截,让事件给我消费

上面例子用内部拦截的方案解决的话。当事件分发到给自己的时候,调用requestDisallowInterceptTouchEvent方法,阻止父布局拦截事件,然后事件让自己消费掉。

 		@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        super.dispatchTouchEvent(ev);
        requestDisallowInterceptTouchEvent(true); //在dispatchTouchEvent中调用,阻止父布局拦截事件
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        return true;
    }

实战 - ViewPage2 + DrawerLayout + RecyclerView冲突惨案

上下翻页的全屏ViewPage2,ViewPage2的item内有个从右往左拉出的DrawerLayout,DrawerLayout内有个上下滑动的RecycleView。

冲突1:拉出DrawerLayout会经常触发到ViewPage2的翻页

冲突2:RecycleView上下滑也会经常触发ViewPage2的翻页

解决方法:因为ViewPage2是final的无法继承。所以引入中间布局,专门处理这三者的冲突。通过禁止ViewPage2的滚动来解决冲突。

public class ListenTouchLayout extends LinearLayout {
        private float touchDownX, touchDownY;
        private Rect childViewRect;

        @Setter
        private ViewPager2 viewPager;

        public ListenTouchLayout(Context context) {this(context, null, -1);}

        public ListenTouchLayout(Context context, @Nullable AttributeSet attrs) {this(context, attrs, -1);}

        public ListenTouchLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }

  			//是否允许ViewPager滑动	
        private void setViewPagerEnable(boolean isEnable){
            if (viewPager != null){
                viewPager.setUserInputEnabled(isEnable);
            }
        }
				
        private Rect getChildViewRectFromId(@IdRes int id){
            View view = this.findViewById(id);
            if (view != null){
                Rect rect = new Rect();
                view.getGlobalVisibleRect(rect); //返回该view的全局矩阵
                return rect;
            }
            return null;
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    touchDownX = event.getRawX();
                    touchDownY = event.getRawY();
                    childViewRect = getChildViewRectFromId(R.id.recycler_view);
                    break;
                case MotionEvent.ACTION_MOVE:
                    float moveX = event.getRawX();
                    float moveY = event.getRawY();
                		//左右滑动趋势,禁止Viewpager滑动
                    if (Math.abs(touchDownX - moveX) > Math.abs(touchDownY - moveY)){ //左右滑动趋势
                        setViewPagerEnable(false);
                    }else { //上下滑动趋势
                        if (childViewRect != null){
                          	//在RecyclerView区域内,也禁止Viewpager滑动
                            setViewPagerEnable(!childViewRect.contains((int)moveX, (int)moveY));
                        }else {
                            setViewPagerEnable(true);
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    setViewPagerEnable(true);
                    touchDownX = 0;
                    touchDownY = 0;
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    }

将这个ListenTouchLayout放到ViewPager2 item布局中作为根布局就好了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值