彻底理解View事件体系!

我的简书同步发布:彻底理解View事件体系!

转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】

View的事件体系整体上理解还是比较简单的,但是却有很多细节。这些细节很容易忘记,本文的目标是理解性的记忆,争取做到看完不忘。最近在复习,希望本文能对你也有所帮助。如果你已经对View事件体系有一定的了解,那么查漏补缺,看看你是不是已经掌握了以下内容呢?

1 View事件相关基础

在正式接触View事件体系之前,先看看相关基础部分。

1.1 View的坐标及宽高

在Android系统中,一个子View在ViewGroup中显示的区域由top、right、bottom、left四个属性确定。它们分别确定四条边,如下图所示:

这四个参数我们可以通过如下方法得到:

//假设v是个View实例
//View v=···;
int top = v.getTop();
int right = v.getRight();
int bottom = v.getBottom();
int left = v.getLeft();

拿到这四个参数后,我们也可以计算出宽高:

int width = right-left;
int height = bottom-top;

我们知道,在Android3.0(api 11)之前,是不能用属性动画的,只能用补间动画,而补间动画所做的动画效果只是将View的显示转为图片,然后再针对这个图片做透明度、平移、旋转、缩放等效果。这带来的问题是,View所在的区域并没有发生变化,变化的只是个“幻影”而已。也就是说,在Android 3.0之前,要想将View区域发生变化,就得改变topleftrightbottom。如果我们想让View的动画是实际的位置发生变化,并且要兼容3.0之前的软件,该怎么办呢?为了解决这个问题,从3.0开始,加了几个新的参数:xytranslationXtranslationY

x = left + translationX;
y = top + translationY;

这样,如果我们想要移动View,只需改变translationXtranslationY就可以了,top和left不会发生变化。也可以使用属性动画去改变translationXtranslationY

1.2 手势识别

(1)VelocityTracker 速度追踪

我们知道,很多ViewGroup中,假设手指滑动的距离相同,但是滑动速度不同,那么滑动速度越快,ViewGroup中内容滚动的距离越远。那么如何识别用户滑动的速度呢?当然了,你可以在onTouchEvent中不断的监听计算。但是那样的代码太臃肿了,而且容易算错。好在Android系统内置了速度追踪类VelocityTracker。有了它,妈妈再也不用担心如何计算速度追踪。先看看怎么用:

//event一般是通过onTouchEvent函数传递的MotionEvent对象
VelocityTracker vt=VelocityTracker.obtain();
vt.addMovement(event);

VelocityTracker.obtain();这句可以看出,这里是使用了享元模式,对享元模式不太熟悉的童鞋请参考我的另一篇文章《从Android代码中来记忆23种设计模式》。那么如何获取当前的移动速度呢?

vt.computeCurrentVelocity(1000);
int xv=(int) vt.getXVelocity();
int yv=(int) vt.getYVelocity();

在调用获取x和y方向的速度之前,先要调用computeCurrentVelocity函数,用于设定计算速度的时间间隔。很显然,速度的计算为(终端位置-起始位置)/间隔时间。

既然是享元模式,那肯定是需要回收的啦~我们看看如何回收VelocityTracker对象:

vt.clear();
vt.recycle();

(2)GestureDetector手势检测

同样,我们有时还需要检测用户的:单击、滑动、长按、双击等动作。懒得自己去计算时间来识别,直接用系统的GestureDector来监听这些事件,GestureDector的使用也非常简单:

GestureDetector.OnGestureListener listener=new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
        //手指出品按下的瞬间
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
        //手指触摸屏幕,并且尚未松开或拖动。与onDown的区别是,onShowPress强调没用松开和没有拖动
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        //手指离开屏幕(单击)
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //手指按下并拖动,当前正在拖动
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        //手指长按事件
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        //手指快速滑动
        return false;
    }
};
GestureDetector mGestureDetector = new GestureDetector(this,listener);

//防止长按后无法拖动的问题
mGestureDetector.setIsLongpressEnabled(false);

既然要让GestureDetector来识别各种动作事件,那么就得让GestureDetector来接管事件管理,即在onTouchEvent里面只写入如下代码:

return mGestureDetector.onTouchEvent(event);

我们看到,OnGestureListener 监听器包含了各种事件的监听。除了OnGestureListener以外,还有OnDoubleTapListener它主要是处理双击相关的事件,可以通过setOnDoubleTapListener将该监听器设置到GestureDetector中。

2 View事件分发机制

2.1 三个重要函数

前面做了基础热身之后,我们现在开始学习View的事件分发机制。View的事件分发主要是由3个函数决定:dispatchTouchEventonInterceptTouchEvent 以及 onTouchEvent。一个触摸事件,如果事件坐标处于ViewGroup所“管辖范围”,首先调用的是该ViewGroupdispatchTouchEvent函数,dispatchTouchEvent函数内部调用onInterceptTouchEvent函数,用于判断是否拦截该事件,如果拦截,则调用ViewGrouponTouchEvent。否则调用子ViewdispatchTouchEvent函数,可以参考如下图:

注意,上述图中,只是描述事件从ViewGroup往下传递过程,没有考虑子ViewonTouchEvent的返回值,即没有考虑事件从子View往上回传的过程。后面再介绍事件回传的过程。ViewGroup是否拦截事件,是通过onTnterceptTouchEvent返回值来确定,当返回true时,表示拦截该事件,那么该系列事件全部传递给ViewGrouponTouchEvent,如果返回false,则表示不拦截该系列事件,该系列事件全部交给子View来处理。为什么我们说是“该系列事件”,而不是说“该事件”呢?注意,View的事件体系中,从down->move->……->move->up。这一个过程为同一个事件系列,当不拦截该系列事件是,该系列事件的所有的事件都不会拦截。

2.2 事件来源

我们知道,我们直接通过onTouchEvent里面的形参就可以拿到事件对象,可是事件对象时从哪里产生的?又是经历过哪些曲折的道路才到达目的地的?

首先,Activity拿到事件对象,Activity把事件对象传递给PhoneWindowPhoneWindow再传递给DecorViewDecorView通过遍历再传递到我们的ViewGroup。那么Activity又是从哪里得到事件对象的呢?这里面就涉及的比较底层了,感兴趣的童鞋参考任玉刚的《 Android中MotionEvent的来源和ViewRootImpl 》这篇文章。

2.3 从onTouch、onClick、onTouchEvent优先级开始

当一个View处理触摸事件时,如果同时设置了OnTouchListener(内含onTouch抽象方法)、OnClickListener(内含onClick抽象方法).那么到底哪个函数先执行?我们做一个实验,自定义一个View,重写onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            Log.d("--> down ", "onTouchEvent");
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d("--> move ", "onTouchEvent");
            break;
        }
        case MotionEvent.ACTION_UP: {
            Log.d("--> up ", "onTouchEvent");
            break;
        }

    }
    return true;
}

并在MainActivity设置OnTouchListenerOnClickListener

myView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
       switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d("--> down", "onTouch");
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                Log.d("--> move", "onTouch");
                break;
            }
            case MotionEvent.ACTION_UP: {
                Log.d("--> up", "onTouch");
                break;
            }

        }
        return false;
    }
});

myView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("-->", "onClick");
    }
});

点击后,打印的日志信息如下:

06-27 00:36:56.756 2407-2407/? D/--> down: onTouch
06-27 00:36:56.756 2407-2407/? D/--> down: onTouchEvent
06-27 00:36:56.848 2407-2407/? D/--> up: onTouch
06-27 00:36:56.849 2407-2407/? D/--> up: onTouchEvent

注意到,首先执行的是onTouch然后再执行onTouchEvent,由此可见,onTouchonTouchEvent优先级高。代码中,onTouch返回的是false,表示不消耗事件,因此,触摸事件能顺利的从onTouch传递到onTouchEvent,现在我们把onTouch返回值改为true,表示消耗触摸事件,看看会打印什么日志:

06-27 00:42:09.783 2499-2499/? D/--> down: onTouch
06-27 00:42:09.863 2499-2499/? D/--> up: onTouch

正如我们所猜想的那样,并没有执行onTouchEvent。我们看到,onClick并没有执行。这是为什么呢?仔细看看onTouchEvent的返回值,我们看到,onTouchEvent返回的是true,表示消耗触摸事件,而此时onClick就没执行了。是不是可以猜想:onTouchEvent优先级比onClick高。我们把onTouchEvent返回值改为false,看看日志信息(确保onTouch返回值也是false,否则onTouchEvent连触摸事件都拿不到,更别谈是否消耗触摸事件的问题了):

06-27 00:48:22.214 2947-2947/? D/--> down: onTouch
06-27 00:48:22.214 2947-2947/? D/--> down: onTouchEvent

什么?!!!,为什么还是没有执行onClick?仔细观察会发现连up事件也没了~。为什么up事件没有了呢?主要是,onTouchEvent返回false,表示对此系列的事件不处理(不消耗),那么该系列事件又会返回到ViewGrouponTouchEvent。后续的moveup事件也不会再交给子ViewonTouchEvent了。这个过程我们暂时先放一放,回到我们前面所说的,为什么onClick不执行?注意!什么是点击?其实,点击包含downup,因此我们需要判断downup是否都是在当前View区域内,我们当然就没办法只根据一个事件来判断是否需要执行onClick。因此,onTouchEvent的返回值不能用于决定是否把事件传递给onClick。如果想把事件传递到onClick函数,我们需要在onTouchEvent里做判断,并显式调用OnClickListener实例对象的onClick。当然了,你可以不用自己写,直接在你的onTouchEvent中的最后一句改为:

return super.onTouchEvent(event);

View在onTouchEvent函数中,根据触摸事件判断,显式的调用了OnClickListener实例对象的onClick。调用过程封装到performClick函数中,看看performClick源码:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

因此可以得出结论,执行的顺序是:onTouch->onTouchEvent->onClick。当onTouch返回false时,onTouchEvent才会执行,当onTouchEvent显式调用onClick时,onClick才会执行。

2.4 事件的回传过程

我们知道,在ViewGroup中,事件是dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent。由onInterceptTouchEvent决定是否将事件传递给子View。如果传递给子View,但是子View并不想处理这个系列的事件(子View的onTouchEvent返回false),该怎么处理这个系列事件呢?难道就抛弃这个系列的触摸事件不管了吗?当然不是!我们先看一段测试代码:

自定义的ViewGroup,重新如下函数:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    print(ev, "ViewGroup dispatchTouchEvent");
    return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    print(ev, "ViewGroup onInterceptTouchEvent");
    //不拦截,将事件往子View传递
    return false;
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    print(event, "ViewGroup onTouchEvent");
    return true;

}

为了减少重复代码,我们定义了print函数:

private void print(MotionEvent event, String msg) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            Log.d("--> down ", msg);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            Log.d("--> move ", msg);
            break;
        }
        case MotionEvent.ACTION_UP: {
            Log.d("--> up ", msg);
            break;
        }

    }

}

自定义View,重写如下函数:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    print(event, "childView dispatchTouchEvent");
    return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    print(event, "childView onTouchEvent");
    //子View不处理该系列事件
    return false;
}

触摸子View后,打印如下信息:

06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: childView dispatchTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: childView onTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:25:38.589 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:25:38.589 3666-3666/? D/--> up: ViewGroup onTouchEvent

看到,当子ViewonTouchEvent返回的是false,那么该系列的事件会回到ViewGrouponTouchEvent。注意,down事件先到达子View的onTouchEvent,如果子View不消耗,则down事件及其后续的事件会传到ViewGrouponTouchEvent。而ViewGrouponTouchEvent也是一样,如果ViewGroup不处理该系列事件,又会继续回传到ViewGroup的父View的onTouchEvent。如下图所示:

我们以上讨论的点击位置都是子View所处的区域,即如下如所示。

如果点击不是子View所处的区域,事件的传递会是怎么样的呢?我们看看日志信息:

06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup onTouchEvent

可以看到,子View并没有调用任何函数。这很容易理解,因为压根就跟子View没有半毛钱关系,要是点击任意区域子View都会有事件传递过去那才奇怪呢!因此,可以看出,ViewGroup在传递触摸事件时,会遍历子View,判断触摸点是否在各个子View中,如果在,则触发调用相关函数。如果点击的位置没有子View,那么不管onIntercepTouchEvent返回的是什么,ViewGroup的onTouchEvent都会执行!

最后,有几点必须要知道的:

  • 如果View只消耗down事件,而不消耗其他事件,那么其他事件不会回传给ViewGroup,而是默默的消逝掉。我们知道,一旦消耗down时间,接下来的该系列所有的事件都会交给这个View,因此,如果不处理down以外的事件,这些事件就会被“遗弃”。
  • 如果ViewGroup决定拦截,那么这个系列事件都只能由它处理,并且onInterceptTouchEvent不会再被调用。
  • 某个View,在onTouchEvent中,如果针对最开始的down事件都返回false,那么接下来的事件系列都不会交给这个View
  • ViewGroup默认不拦截事件,即onInterceptTouchEvent默认返回false
  • ViewonTouchEvent默认返回true,即消耗事件。
  • View没有onInterceptTouchEvent方法。
时间: 2024-10-10 08:24:52

彻底理解View事件体系!的相关文章

Android艺术开发探索第三章————View的事件体系(下)

Android艺术开发探索第三章----View的事件体系(下) 在这里就能学习到很多,主要还是对View的事件分发做一个体系的了解 一.View的事件分发 上篇大致的说了一下View的基础知识和滑动,现在我们再来聊聊一个比较核心的知识点,那就是事件分发了,而且他还是一个难点,我们更加应该掌握,View的滑动冲突一直都是很苦恼的,这里,我们就来一起探索一下 1.点击事件的传递规则 我们分析的点击事件可不是View.OnClickListener,而是我们MotionEvent,即点击事件,关于M

Android开发艺术探索读书(三)-View的事件体系

移动手持客户端作为目前最受欢迎的智能设备,拥有着最为广大的体验用户群体.因此,作为软件开发商,要紧紧抓住用户的胃口,不仅要向用户提供合适的服务项目,也应该更为注重与用户的交互体验.而作为感觉型的用户,应用操作是否流畅,界面内容是不是足够精致,是判断该应用是不是一个好应用的硬性标准.那么,要如何去强化与用户的交互体验呢?这就涉及了本章所讲的内容:View的事件体系 提纲: 一.什么是View 二.View的位置参数 三.几个相关的View知识点 四.View的滑动深入 五.view的事件分发 六.

Android艺术开发探索第三章——View的事件体系(上)

Android艺术开发探索第三章----View的事件体系(上) 我们继续来看这本书,因为有点长,所以又分了上下,你在本片中将学习到 View基础知识 什么是View View的位置参数 MotionEvent和TouchSlop VelocityTracker,GestureDetector和Scroller View的滑动 使用scrollTo/scrollBy 使用动画 改变布局参数 各种滑动方式的对比 弹性滑动 使用Scroller 通过动画' 使用延时策略 这章的概念偏自定义View方

深入理解 View 的事件传递机制

引言:现在 GitHub 上酷炫的 Android 控件越来越多,一方面我们可以让 App 各美观,另一方面我们这些开发者也可以从中学习到各种知识.写下这篇博文主要是记录研究自定义控件源码过程中接触到的知识盲区,帮助自己巩固知识的同时,也和大家交流学习,一起进步. Demo源码 废话不多说,进入正题: 一.概述 View 事件传递机制 用户通过点击.滑动屏幕与 App 产生交互是移动互联网时代的交互基础,那么在 Android 中,用户的点击.滑动是怎么和 Android 系统产生交互的呢? 在

Android View 的事件体系

android 系统虽然提供了很多基本的控件,如Button.TextView等,但是很多时候系统提供的view不能满足我们的需求,此时就需要我们根据自己的需求进行自定义控件.这些控件都是继承自View的. 一.android 控件架构 android 中的控件在界面上都会占一块巨型区域,主要分为两类:ViewGroup和View控件.ViewGroup作为父控件可以包含多个View控件,并管理他们,但其也是继承自View.通过Viewgroup,整个控件界面形成了View的控件树,如图1所示.

5.深入理解输入事件的派发1

6.5深入理解输入事件的派发 控件树中的输入事件派发是由ViewRootImpl为起点,沿着控件树一层一层传递给目标控件,最终再回到ViewRootImpl的一个环形过程.这一过程发生在创建ViewRootImpl的主线程之上,但是却独立于ViewRootImpl.performTraversals()之外,就是说输入事件的派发并不依赖于ViewRootImpl的"心跳"作为动力,而是有它自己的动力源泉.经过第5章的学习可以知道,这一动力源泉来自用于构建InputEventReceiv

Unity3d开发(十七)UGUI 事件体系分析

div#cpcontent2 {height:215px;width:215px;float:left;} div#cpmenu {height:200px;float:left;} div#cpcontent {height:200px;width:150px;float:left;} 文章作者:松阳 原文链接:http://blog.csdn.net/fansongy/article/details/52778862 很多Unity3D项目都使用了UGUI,但并不是所有人都研究过它的内部结构

Android View 事件分发机制源码详解(View篇)

前言 在Android View 事件分发机制源码详解(ViewGroup篇)一文中,主要对ViewGroup#dispatchTouchEvent的源码做了相应的解析,其中说到在ViewGroup把事件传递给子View的时候,会调用子View的dispatchTouchEvent,这时分两种情况,如果子View也是一个ViewGroup那么再执行同样的流程继续把事件分发下去,即调用ViewGroup#dispatchTouchEvent:如果子View只是单纯的一个View,那么调用的是Vie

Android View事件分发机制

最近在开发中遇到view滑动冲突的问题,由于一开始就知道这个问题与view事件分发有关,之后在网上看了几篇关于事件分发的资料后,开发中遇到的问题很快便得到解决. 在这里总结一下我对view事件分发的理解. 首先,看下事件分发流程图: Button事件演示 在对view的事件分发机制进行分析前,我们可以通过一个demo看看Button的事件处理的流程. 在布局文件中添加一个button控件,然后在代码中实现Button的setOnClickListener和setOnTouchListener方法