在我们实际的项目中,涉及到手势的操作很多,这就需要我们对Android的事件有一定的了解,才能设计出好的手势。本文就对Android中的事件分发做一些简单的介绍。
1、Android中事件的基本概念
1.1、事件分发的对象
事件分发的对象就是事件,当我们触摸屏幕的时候就会触发一系列的事件。
Touch事件相关细节(发生触摸的位置、时间、历史记录、手势动作等)被封装成MotionEvent对象。
1.2、事件的种类
Android中的事件分为4类:
- MotionEvent.ACTION_DOWN:按下View(所有事件的开始)
- MotionEvent.ACTION_MOVE:滑动View
- MotionEvent.ACTION_CANCEL:非人为原因结束本次事件(比如滑出手机屏幕外)
- MotionEvent.ACTION_UP:抬起View(与DOWN对应)
每次使用的时候都会组合触发这些事件,以down事件开始,以up或者cancel事件结束,如下图所示:
1.3、事件传递的对象
在Android中,事件是在Activty、ViewGroup、View之间相互传递的。
当我们点击屏幕,MotionEvent在这些对象之间传递,就叫做事件分发。
2、事件分发的过程
2.1、相关方法
在完整分析事件分发过程之前,先来看下事件分发的相关方法
- dispatchTouchEvent():Activity、ViewGroup、View中都存在该方法,是用来分发事件的,当触摸屏幕,就会触发Activity的此方法
- onInterceptTouchEvent():只存在于ViewGroup,是用来拦截事件的,在ViewGroup的dispatchTouchEvent()内部触发
- onTouchEvent(): Activity、ViewGroup、View中都存在该方法,是用来处理事件的,在dispatchTouchEvent内部触发
这些方法都会返回一个boolean变量,我们来对不同传递对象的各方法进行一个归纳总结:
Activity
dispatchTouchEvent()
- super:调用ViewGroup的dispatchTouchEvent()方法
- true/false:消费事件,结束当前事件的传递
onTouchEvent()
- 不论返回什么,都会结束
ViewGroup
dispatchTouchEvent()
- super:传递到当前ViewGroup的onInterceptTouchEvent()中
- false:不消费,事件向上传递,传递到Activity的onTouchEvent()
- true:消费事件,结束
onInterceptTouchEvent()
- super/false:不拦截事件,事件向下传递,传递到子View的dispatchTouchEvent()
- true:拦截事件,传递到当前ViewGroup的onTouchEvent()中
onTouchEvent()
- super/false:不消费事件,事件向上传递,传递到Activity的onTouchEvent()中,如果返回false,当前ViewGroup不再接收该事件队列的后续事件
- true:消费事件,结束
View
dispatchTouchEvent()
- super:传递到当前View的onTouchEvent()中
- false:不消费,事件向上传递,传递到ViewGroup的onTouchEvent()
- true:消费事件,结束
onTouchEvent
- super:这个针对不同的View,情况不太一样,如果当前View是clickable的(如Button),则消费事件,否则向上传递到ViewGroup的onTouchEvent()中
- false:不消费事件,向上传递到ViewGroup的onTouchEvent()中,该View不再接收该事件队列的后续事件
- true:消费事件,结束
具体看下表:
2.2、具体流程
注意看箭头的方向,默认实现并不一定是单纯的true或者false,是属于第三种情况,会调用到别的传递对象里的方法。
这里对上图作各个情况的梳理,见下面的流程图:
3、源码分析
这里简单做点源码分析,如果感兴趣的话,读者可以自行利用工具查看源码
一个事件首先触发的是Activity的dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//关注点1
//一般事件列开始都是DOWN,所以这里基本是true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//关注点2
onUserInteraction();
}
//关注点3
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
关注点1 一般事件列开始都是DOWN(按下按钮),所以这里返回true,执行onUserInteraction()
关注点2 该方法为空方法
从注释得知:当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
所以onUserInteraction()主要用于屏保
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
关注点3 getWindow().superDispatchTouchEvent(ev)
Window类是抽象类
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
PhoneWindow是Window类的唯一实现类,所以看一下PhoneWindow类中superDispatchTouchEvent()
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
//mDecor是DecorView的实例
//DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
}
接下来我们看mDecor.superDispatchTouchEvent(event):
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
//DecorView继承自FrameLayout
//那么它的父类就是ViewGroup
而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()
}
所以就走到了ViewGroup的dispatchTouchEvent()中,这样事件就从Activity传递到了ViewGroup,后面源码就不分析了,感兴趣可以自己查看。
4、应用
简单应用场景,viewpager嵌套webview,而webview中含有可以左右滑动的选项卡,这样viewpager的左右滑动势必会和webview左右滑动的选项卡冲突。解决思路是,重写viewpager的onInterceptTouchEvent方法,在move事件时判断,当在界面的边缘滑动,并且是左右滑动时(x的滑动绝对值大于y的滑动绝对值),将滑动事件给父view,否则滑动事件还交给子view处理。实现代码如下:
public class EdgeSwitchPageViewPager extends ViewPager{
private float startX;
private float startY;
public EdgeSwitchPageViewPager(Context context) {
super(context);
}
public EdgeSwitchPageViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float nowX = ev.getX();
float nowY = ev.getY();
int screenWidth = DeviceUtils.getScreenWidth(getContext());
//当按下时距离左边框或者右边框小于100px,且横向滑动幅度大于纵向滑动幅度时,将滑动焦点还给viewpager用于整个界面的切换
//当在内部滑动时将滑动焦点给子类,用于子类滑动操作
if((startX<100 && Math.abs(nowX - startX)>Math.abs(nowY - startY))
|| ((screenWidth - startX) <100 && Math.abs(nowX - startX)>Math.abs(nowY - startY))){
return true;
}else{
return false;
}
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(ev);
}
}
对于上面举例中的getX()做个简单的图解说明
类似的用法还有很多,设计一些简单手势(比如长按,右滑显示页面,当然对于手势的设计用GestureDetector更好),拦截down事件的后续事件啊,拦截click事件啊等等,相信读者看了本文已经知道怎么做了。