Android面试老生常谈的 View 事件分发机制,看这一篇就够了

Posted gdutxiaoxu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android面试老生常谈的 View 事件分发机制,看这一篇就够了相关的知识,希望对你有一定的参考价值。

本文首发我的微信公众号:徐公,想成为一名优秀的 android 开发者,需要一份完备的 知识体系,在这里,让我们一起成长,变得更好~。

在 Android 开发当中,View 的事件分发机制是一块很重要的知识。不仅在开发当中经常需要用到,面试的时候也经常被问到。

如果你在面试的时候,能把这块讲清楚,对于校招生或者实习生来说,算是一块不错的加分项。对于工作几年的我们来说,这是必须掌握的,讲不明白,那你回去等通知吧,哈哈。

目录大概如下:

  1. View 事件分发机制简介
  2. View 常见滑动冲突解决
  3. View 双击,多击事件是怎么实现的
  4. 手势识别
  5. 小结

View 事件分发机制简介

View 触摸事件

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

1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件 2.ACTION_MOVE:手指在屏幕上移动时候产生该事件 3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件 4.ACTION_CANCEL 当前 View 的手势被打断,后续不会再收到任何事件

从 ACTION_DOWN 开始到 ACTION_UP/ACTION_CANCEL 结束我们称为一个事件序列

正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面 3 种 case。

  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事件

几个主要方法

我们知道,View 的事件分发机制主要涉及到以下几个方法

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

我们先来看一张图。

以下内容参考图解 Android 事件分发机制这一篇博客

  • 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
  • 事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 进行分发
  • 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。)
  • dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是如果方法返回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 再来开始这个事件的分发。

关于更多详细分析,请查看原博客图解 Android 事件分发机制,真心推荐,写得很好。


View 滑动事件冲突

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

常见的三种情况

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

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

第三种情况,上述两种情况的嵌套

解决思路

看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突。

那既然同一时刻只能由某一个 View 或者 ViewGroup 消费拦截,那我们就只需要 决定在某个时刻由这个 View 或者 ViewGroup 拦截事件,另外的 某个时刻由 另外一个 View 或者 ViewGroup 拦截事件,不就 OK了吗?

综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种解决方案

以下解决思路来自于 《Android开发艺术》 书籍

下面的两种方法针对第一种情况(滑动方向不同),父View是上下滑动,子View是左右滑动的情况。

外部解决法

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

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) 
    final float x = ev.getX();
    final float y = ev.getY();

    final int action = ev.getAction();
    switch (action) 
        case MotionEvent.ACTION_DOWN:
            mDownPosX = x;
            mDownPosY = y;

            break;
        case MotionEvent.ACTION_MOVE:
            final float deltaX = Math.abs(x - mDownPosX);
            final float deltaY = Math.abs(y - mDownPosY);
            // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
            if (deltaX > deltaY) 
                return false;
            
    

    return super.onInterceptTouchEvent(ev);

内部解决法

从子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事件
            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);
            // 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
            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);

更多细节,可以查看我的这篇文章,里面有详细介绍哦 ViewPager,ScrollView 嵌套ViewPager滑动冲突解决


View 双击,多击事件是怎么实现的

实现之前,我们首先来阐述一下思路,怎样实现双击事件,正所谓,授人以鱼不如授人以渔。

单击:用户点击一次之后,一段时间之内不再点击

双击;用户点击一次之后,一段时间之内再次点击

实现思路

  1. 我们监听 onTouch 事件,在 ACTION_DOWN 的时候,点击次数 clickCount +1;
  2. 同时,在 ACTION_DOWN 的时候,延时一段时间,执行相应的 Runnable 任务,这里我们用 handler 的 postDelayed 实现
  3. 在延时任务执行的时候,我们根据点击的次数,进行单击或者多级的回调,最后,记得重置点击次数,以及移除延时任务
open class MyDoubleTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener 

    private var clickCount = 0 //记录连续点击次数
    private val handler: Handler = Handler()

    interface MyClickCallBack 
        fun oneClick() //点击一次的回调
        fun doubleClick() //连续点击两次的回调
    

    override fun onTouch(v: View, event: MotionEvent): Boolean 
        if (event.action == MotionEvent.ACTION_DOWN) 
            clickCount++
            handler.postDelayed(
                if (clickCount == 1) 
                    myClickCallBack.oneClick()
                 else if (clickCount == 2) 
                    myClickCallBack.doubleClick()
                
                handler.removeCallbacksAndMessages(null)
                //清空handler延时,并防内存泄漏
                clickCount = 0 //计数清零
            , timeout.toLong()) //延时timeout后执行run方法中的代码
        
        return false //让点击事件继续传播,方便再给View添加其他事件监听
    

    companion object 
        private const val TAG = "MyClickListener"
        private val timeout = ViewConfiguration.getDoubleTapTimeout() //双击间四百毫秒延时

        init 
            Log.i(TAG, "timeout is $timeout ")
        
    


三击事件

三级事件呢,其实也很简单,我们直接判断在指定时间间隔内点击的次数即可

open class MyMultiTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener 

    private var clickCount = 0 //记录连续点击次数
    private val handler: Handler = Handler()

    interface MyClickCallBack 
        fun oneClick() //点击一次的回调
        fun doubleClick() //连续点击两次的回调
        fun threeClick() // 连续点击三次的回调
    

    override fun onTouch(v: View, event: MotionEvent): Boolean 
        if (event.action == MotionEvent.ACTION_DOWN) 
            clickCount++
            handler.postDelayed(
                if (clickCount == 1) 
                    myClickCallBack.oneClick()
                 else if (clickCount == 2) 
                    myClickCallBack.doubleClick()
                 else if (clickCount == 3) 
                    myClickCallBack.threeClick()
                
                handler.removeCallbacksAndMessages(null)
                //清空handler延时,并防内存泄漏
                clickCount = 0 //计数清零
            , timeout.toLong()) //延时timeout后执行run方法中的代码
        
        return false //让点击事件继续传播,方便再给View添加其他事件监听
    

    companion object 
        private const val TAG = "MyClickListener"
        private val timeout = 600 //双击间四百毫秒延时

        init 
            Log.i(TAG, "timeout is $timeout ")
        
    


手势识别

在 Android 开发当中,几乎所有的事件都会与用户进行交互,而我们用得的最多的就是手势了。

我们知道当我们触摸屏幕的时候,会产生很多事件,比如 down,move,up, fling 事件等等。一些简单的处理,我们可以直接重写 View 的 onTouchEvent 方法,根据 View 的 MotionEvent 事件进行处理。

而 Google 为了方便开发者方便接入,提供了几个默认处理类,那就是 GestureDetector 和 ScaleGestureDetector。

GestureDetector这个类对外提供了两个接口和一个外部类 。 接口:OnGestureListener,OnDoubleTapListener

内部类:SimpleOnGestureListener,同时实现了 OnGestureListener,OnDoubleTapListener 接口,如果只想使用接口里面的某个方法,可以直接使用它,方便快捷。

讲解之前,我们向来看一下怎么使用

GestureDetector(Context context, GestureDetector.OnGestureListener listener)

GestureDetector 基本使用

第一步,初始化 GestureDetector 对象

 mDetector = GestureDetectorCompat(this, MyGestureListener())

可以看到有两个参数,第一个参数 context,第二个参数 OnGestureListener,我们可以直接实现 OnGestureListener 接口,也可以直接使用 GestureDetector.SimpleOnGestureListener

    private class MyGestureListener : GestureDetector.OnGestureListener 

        private val TAG = "GestureDemoActivity"

        override fun onShowPress(e: MotionEvent?) 
            Log.d(TAG, "onShowPress: e is $e")
        

        override fun onSingleTapUp(e: MotionEvent?): Boolean 
            Log.d(TAG, "onSingleTapUp: e is $e")
            return false
        

        override fun onDown(event: MotionEvent): Boolean 
            Log.d(TAG, "onDown: $event")
            return true
        

        override fun onFling(
                event1: MotionEvent,
                event2: MotionEvent,
                velocityX: Float,
                velocityY: Float
        ): Boolean 
            Log.d(TAG, "onFling: $event1 $event2")
            return false
        

        override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean 
            Log.d(TAG, "onScroll: distanceX is $distanceX,distanceY is $distanceY ")
            return false
        

        override fun onLongPress(e: MotionEvent?) 
            Log.d(TAG, "onLongPress: e is $e")
        
    

第二步:设置双击监听


// 设置双击监听
mDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener 
            override fun onDoubleTap(e: MotionEvent?): Boolean 
                Log.d(TAG, "onDoubleTap: e is e")
                return false
            

            override fun onDoubleTapEvent(e: MotionEvent?): Boolean 
                Log.d(TAG, "onDoubleTapEvent: e is e")
                return false
            

            override fun onSingleTapConfirmed(e: MotionEvent?): Boolean 
                Log.d(TAG, "onSingleTapConfirmed: e is e")
                return false
            

        )

最后,重写 Activity 或者 View 的 onTouchEvent ,将事件交给 mDetector 处理。

通常会有两种写法,第一种是如果手势处理器处理了,直接返回 true,进行消费。否则,进行默认处理

override fun onTouchEvent(event: MotionEvent): Boolean 
        return if (mDetector.onTouchEvent(event)) 
            true
         else 
            super.onTouchEvent(event)
        
    

第二种写法是直接在 onTouchEvent 方法中,直接调用 mDetector.onTouchEvent(event) 方法

override fun onTouchEvent(event: MotionEvent): Boolean 
        mDetector.onTouchEvent(event)
        return super.onTouchEvent(event)
    

第二种写法,一般不会影响当前 View 或者 Activity 事件的传递,在开发当中,有时候为了减少一些触摸事件的冲突,经常这样写。

ScaleGestureDetector 这里暂时不展开描述了了,写着写着,发现好多呀,一个周末就这样过去,贼快,觉得对你有帮助的,请来个三连,点赞,收藏,转发😆


小结

这篇文章,其实不难。主要是将 View 的事件分发机制,滑动冲突,以及开发当中经常用到的一些知识点,总结一下。

参考博客:

图解 Android 事件分发机制

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

Demo 地址

如果觉得对你有所帮助的话,可以关注我的微信公众号程序员徐公

  1. 公众号程序员徐公回复黑马,获取 Android 学习视频
  2. 公众号程序员徐公回复徐公666,获取简历模板,教你如何优化简历,走进大厂
  3. 公众号程序员徐公回复面试,可以获得面试常见算法,剑指 offer 题解
  4. 公众号程序员徐公回复马士兵,可以获得马士兵学习视频一份

以上是关于Android面试老生常谈的 View 事件分发机制,看这一篇就够了的主要内容,如果未能解决你的问题,请参考以下文章

Android面试老生常谈的 View 事件分发机制,看这一篇就够了

Android面试View的事件分发

Android View 事件分发机制

Android事件分发机制之ACTION_DOWN

2016年未面试题汇总

Android事件分发机制五:面试官你坐啊