您的浏览器不支持CSS3,建议使用Firfox、Chrome等浏览器,以取得最佳显示效果

Android事件分发知识点整理

开发技术 197℃ 0 1个月前 (01-17)
摘要:

本文梳理Android事件分发涉及到的各种知识点,包括MotionEvent、事件分发流程、ViewConfiguration、VelocityTracker等,并给出了几个典型的实际案例。

了解MotionEvent

MotionEvent.class自身并不实际包含Action相关的信息,只包含了一个mNativePtr,指向JNI层实际包含所有Event信息的Object。

Pointer

  • Pointer指多点触控中的一个点,通常就是一根手指。
  • 一般第一个按下的Pointer即为Primary Pointer,其他的为Non-Primary Pointer。
  • PointerIndex,即Pointer的Index,用于区分不同的Pointer。

Action

View在响应一次用户操作时,会接收到一个事件流,以ACTION_DOWN开始,中间有若干个ACTION_MOVE、ACTION_POINTER_DOWN、ACTION_POINTER_UP,最后以ACTION_UP或者ACTION_CANCEL结束。

4个基本事件如下:

  • ACTION_DOWN:第一个Pointer按下。获取到的是最初始状态下的信息。此时View一般会设置一些标志位,表明自己被按下了,开始处理用户事件。如果有设置selector,此时selector也会变成pressed状态。
  • ACTION_MOVE:有一个Pointer移动。由于事件分发有时间间隔(包括可能发生卡顿),实际获取到的数据是离当前最近的时间点的数据。通常在ACTION_MOVE事件发生时处理滑动操作。
  • ACTION_UP:最后一个Pointer释放。通常在此时处理点击操作,或者作为滑动操作的终止条件,并开始执行惯性滑动(Fling)过程。
  • ACTION_CANCEL:操作被取消,通常是因为事件被其他组件处理了。收到ACTION_CANCEL的组件应该及时清除按下状态,且不作出事件响应。具体场景可以参考后文的实际案例。

多点控触新增的Action:

  • ACTION_POINTER_DOWN:一个Non-Primary Pointer按下
  • ACTION_POINTER_UP:一个Non-Primary Pointer释放

事件API以及单点、多点触控的兼容问题

1、早期Android只支持单点触控,View中通过getAction()获取int型的事件(Action),然后用switch-case判断即可。

// MotionEvent event
switch(event.getAction()) {
  case MotionEvent.ACTION_DOWN:
  case MotionEvent.ACTION_MOVE:
  case MotionEvent.ACTION_UP:
}

2、高版本Android系统中增加了多点触控机制,如果直接用getAction()获取int型action值,其中最低8位即0-7位用于存储原有的Action,8-15位用于在ACTION_POINTER_DOWN/UP事件发生时存储PointerIndex。系统提供了新的API,一般应该使用getActionMasked()获取低8位的Action部分,通过getActionIndex()获取8-15位的PointerIndex

3、多点触控机制可以兼容单点触控的View组件。也就是说,在高版本Android系统提供了多点触控机制之后,原有的单点触控组件仍然能正常运行。原因是原有的4个基本事件发生时,8-15位全为0,并没有PointerIndex,用getAction()getActionMasked()得到的值相同。

坐标、压力、大小

  • getX(),getY()可获取MotionEvent相对当前View的坐标
  • getRawX(),getRawY()可获取MotionEvent相对屏幕的坐标
  • getPressure(),getSize()等可获取压力、大小等信息,需要硬件支持

Pointer相关API

  • 通过MotionEvent.getPointerCount(),可以获取当前有几个Pointer处于按下状态。
  • 通过getPointerId(int pointerIndex)findPointerIndex(int pointerId)可以互相转换PointerIndex和PointerId。
  • getX(int pointerIndex),getY(int pointerIndex)等方法可以获取指定PointerIndex的坐标、压力、大小等信息。

生成精确的MotionEvent用于调试

开发过程中,常需要调试交互组件,但手工操作比较麻烦,耗时耗力,精度低,可能满足不了需要。这时就可以考虑使用代码自动产生精确的MotionEvent序列用于调试。

例如可以用下面的方法,产生精确的滑动事件序列,直接调用Activity.dispatchTouchEvent注入事件,观察UI界面的响应。

private static Handler handler = new Handler(Looper.getMainLooper());

// 向Activity分发移动事件 ACTION_DOWN --> MOVE --> MOVE --> ... --> MOVE --> UP
public static void moveAction(final Activity activity, int startX, int startY, int stepX, int stepY, int stepT, int eventCount) {
    if (eventCount < 3) return;
    final long downTime = System.currentTimeMillis();

    int offsetTime = 0;
    for (int i = 0; i < eventCount; i++) {
        final int action;
        if (i == 0) {
            action = MotionEvent.ACTION_DOWN;
        } else if (i == eventCount - 1) {
            action = MotionEvent.ACTION_UP;
        } else {
            action = MotionEvent.ACTION_MOVE;
        }
        final long eventTime = downTime + offsetTime;
        final int x = startX;
        final int y = startY;
        final int index = i;
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i("Event", String.format("----> send event(%d): xy = (%d, %d)", index, x, y));
                activity.dispatchTouchEvent(MotionEvent.obtain(downTime, eventTime, action, x, y, 0));
            }
        }, offsetTime);
        startX += stepX;
        startY += stepY;
        offsetTime += stepT;
    }
}

事件分发流程

事件分发到View前经过的几个关键方法:

ViewRootImpl.ViewPostImeInputStage.processPointerEvent
PhoneWindow.DecoreView.dispatchTouchEvent
Activity.dispatchTouchEvent
ViewGroup.dispatchTouchEvent

View中的关键函数调用关系如下:

ViewGroup.dispatchTouchEvent {
	ViewGroup.onInterceptTouchEvent
    ViewGroup.dispatchTransformedTouchEvent {
        child.dispatchTouchEvent
    	super/View.dispatchTouchEvent {
            View.OnTouchListener.onTouch
            View.onTouchEvent {
	            View.TouchDelegate.onTouchEvent
                View.performClick
            }
        }
    }
}

requestDisallowInterceptTouchEvent

当子View不希望父View拦截事件时,可以调用父View的requestDisallowInterceptTouchEvent方法。

DescendantFocusability

DescendantFocusability用于设置嵌套View的焦点获取优先顺序,通常只有获取的焦点的View才能响应点击事件。

  • blocksDescendants
  • beforeDescendants
  • afterDescendants

AbsListView嵌套AbsListView、CheckBox、Button、ImageButton、设置了OnClickListener的View,焦点默认分发给子View,导致OnItemClickListener无效。此时可以将AbsListView设置成blocksDescendants。

TouchDelegate

TouchDelegate可扩大一个View的触摸响应范围,使其可以响应超过自身大小范围的事件。

子View在onLayout获取尺寸后,给Parent设置一个TouchDelegate,即可扩展自身点击区域。这个区域不能超过Parent的点击区域;且每个Parent只能设置一个TouchDelegate,指定给一个子View扩展点击区域。

更复杂的需要,可以通过覆写TouchDelegate,或者覆写Parent的事件处理方法实现。

VelocityTracker

VelocityTracker用于计算滑动速度。几个关键方法:

// 静态方法,创建实例
VelocityTracker.obtain();
// 回收对象
VelocityTracker.recycle();

// 清除事件
VelocityTracker.clear();
// 添加一个MotionEvent
VelocityTracker.addMovement(MotionEvent ev);
// 计算速度,units通常取1000,表示计算每秒移动多少个像素点
VelocityTracker.computeCurrentVelocity(int units, float maxVelocity);
// 获取X、Y方向滑动速度
VelocityTracker.getXVelocity();
VelocityTracker.getYVelocity();

ViewConfiguration

ViewConfiguration接口中定义了一系列和View操作相关的阈值。

TouchSlop

当用户点击屏幕上的一个View时,除了ACTION_DOWNACTION_UP,一般情况下,由于手指操作并不十分精确,中间可能会产生若干个ACTION_MOVE,且ACTION_DOWNACTION_UP事件的坐标通常并不完全相同。

如何区分用户是点击还是滑动操作呢?Android中使用TouchSlop来解决这个问题。当移动的距离小于等于TouchSlop时,视为点击操作;大于TouchSlop,则视为滑动操作。这里的距离,可能是X方向、Y方向,视具体的View组件而定。

在Android源码中,一般TouchSlop被定义为8dp对应的像素点,实际根据不同手机ROM而定。

PagingTouchSlop

和TouchSlop类似的还有一个PagingTouchSlop,这个通常用于ViewPager中判断用户滑动距离是否可以视为翻页操作。

其他

ViewConfiguration还定义了其他一些值,例如按下多长时间可以认为是长按操作,速度达到多大可以视为快速滑动(Fling)等。

GestureDetector

GestureDetector提供了一套比较简单的接口,实现常用手势检测。

案例一:自己实现一个OnClickListener

覆写View的onTouchEvent,或设置OnTouchListener。

方案一:

  1. ACTION_DOWN时记录下初始坐标mStartX,mStartY
  2. ACTION_UP时获取事件坐标x,y,分别判断x、y两个方向相对初始坐标mStartX,mStartY的位移均没有超过TouchSlop,则可视为点击事件,此时即可触发OnClickListener。如果超过了TouchSlop则视为滑动事件,不会触发点击事件。

方案二:

方案一有个小问题,如果用户手指在View组件上滑动了一段距离然后又滑动回来然后释放,且ACTION_DOWN和ACTION_UP的坐标恰好接近,此时还是会被视为点击操作。改进的方案二如下:

  1. ACTION_DOWN时记录下初始坐标mStartX,mStartY,并设置标志位mIsBeingDragged为false。
  2. 每个ACTION_MOVEACTION_UP事件生成时判断位移相对初始坐标是否超出TouchSlop,如果超过了则设置标志位mIsBeingDragged为true。
  3. ACTION_UP时判断标志位mIsBeingDragged如果为false,则视为点击事件。

案例二:ScrollView嵌套Button的事件分发

ScrollView嵌套一个Button。

场景1

  • 当手指按下按钮时,按钮处于按下状态;
  • 手指释放,Button被点击。

场景2

  • 当手指按下按钮时,按钮处于按下状态;
  • 手指开始进行滑动,可以观察到,按钮的按下状态消失,ScrollView开始滚动。

这个两个场景中的事件分发过程如下:

  1. ACTION_DOWN被传递给ScrollView.dispatchTouchEvent,然后调用ScrollView.onInterceptTouchEvent,返回值为false。于是ScrollView会把ACTION_DOWN分发给Button。
  2. Button接收到事件后,设置自己处于按下状态,于是selector会展示成selected状态。
  3. 手指开始滚动,连续产生多个ACTION_MOVE,其垂直方向相对ACTION_DOWN的位移没有达到TouchSlop,事件经过ScrollView.dispatchTouchEventScrollView.onInterceptTouchEvent,然后被分发给Button。
  4. 对于场景1,在ACTION_MOVE后,产生了一个ACTION_UP事件,并被分发给Button,于是Button.performClick被执行,从而OnClickListener被调用。流程结束。
  5. 对于场景2,手指继续滑动直到某一次ACTION_MOVE位移达到TouchSlop,ScrollView.onInterceptTouchEvent会设置mIsBeingDragged=true并返回true,此时ScrollView.dispatchTouchEvent判断发现之前已经把事件分发给Button了,于是给Button分发一个ACTION_CANCEL事件,同时把这个ACTION_MOVE分发给自己的ScrollView.onTouchEvent
  6. Button接收到ACTION_CANCEL事件后,取消按下状态,且不触发OnClickListener
  7. ScrollView.onTouchEvent接收到ACTION_MOVE事件后,开始滚动。
  8. 由于mIsBeingDragged==true,之后的ACTION_MOVEACTION_UP都会被发送给ScrollView.onTouchEvent
  9. ACTION_UP事件发生时,ScrollView会获取VelocityTracker记录的滚动速度,然后利用Scroller执行Fling过程,即惯性滚动。流程结束。

场景2输出的Log如下:

event______: ACTION_DOWN, xy = (370.5, 1147.1)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onInterceptTouchEvent
m_LogScroll:     onInterceptTouchEvent return false
m_LogButton:     dispatchTouchEvent
m_LogButton:         onTouchEvent
m_LogButton:         onTouchEvent return true
m_LogButton:     dispatchTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (370.5, 1140.4)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onInterceptTouchEvent
m_LogScroll:     onInterceptTouchEvent return false
m_LogButton:     dispatchTouchEvent
m_LogButton:         onTouchEvent
m_LogButton:         onTouchEvent return true
m_LogButton:     dispatchTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (370.5, 1124.0)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onInterceptTouchEvent
m_LogScroll:     onInterceptTouchEvent return true
event______: ACTION_CANCEL, xy = (370.5, 1124.0)
m_LogButton:     dispatchTouchEvent
m_LogButton:         onTouchEvent
m_LogButton:         onTouchEvent return true
m_LogButton:     dispatchTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (372.5, 1088.7)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (375.5, 1056.0)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (378.2, 1038.7)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (377.5, 1025.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (377.5, 1017.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (377.5, 1017.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_UP, xy = (377.5, 1017.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll:     onTouchEvent
m_LogScroll:     onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:

最后,欢迎扫码关注微信公众号,也可以加我微信 jzj2015 交流(注明来自博客)。

本文由原创,转载请注明来源:http://www.paincker.com/android-motion-event
(标注了原文链接的文章除外)

0

暂无评论

评论前:需填写以下信息,或 登录

用户登录

忘记密码?