前言
产品有个需求是两个tab页面可以左右切换,当时立马想到我用viewPager+fragment,但是我们知道viewPager默认是可以左右滑动的,而我的需求是只可点击不可滑动,于是我就翻了一下viewpager的API发现并没有可以设置是否可以滑动的相关方法。于是我就想是否可以通过事件的分发机制去拦截它左右滑动的touch。果不其然!
public class myViewpager extends ViewPager {
private boolean canTouch = false;//设置默认不滑动
public myViewpager(@NonNull Context context) {
super(context);
}
public myViewpager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canTouch;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return canTouch;
}
public void setCanTouch(boolean canTouch) {//提供设置是否可以点击方法默认不可以
this.canTouch = canTouch;
}
}
可以看到这里重写了onTnterceptTouchEvent、onTouchEvent并且返回false事情就这样实现了!
事件分发机制
但是它的事件是如何传递的呢?这就得从android的事件分发机制说起:
说到它必须排上三个方法:
dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()
字面意思其实就是事件的分发、拦截、触摸的处理。
我们知道,安卓中的控件千千万,也就是说每一个view都有自己的事件分发,那我怎么能找到他们的规律呢?总不能以偏概全吧?是的,我们Java的三大特性是啥?封装、继承、多态我们看它的父类不难发现所有的自定义控件都是继承view或者是viewgroup,还有一个特殊的就是我们的activity。activity是一个可视化窗口(我是这么理解的)你把所有的控件摆在上面,当然它也能处理一些事件。因此我们事件的作用对象可以大致分为:
View的事件分发机制
ViewGroup的事件分发机制
Activity的事件分发机制
产生一个事件
事件是如何产生并进行分发的?
当屏幕被触摸时(view或者viewGroup),将会产生点击事件touch,touch是事件的具体实现像:UP(抬起)、DOWN(按下)、MOVE(移动)、CANCEL(取消)。
听着听着是不是感觉这个不就是咱们的MotionEvent,还就是,我们的touch细节就被封装在MotionEvent方法里面而我们的MotionEvent方法在哪调用了?
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(ev);
}
是的就是我们前面排出的三大方法。想搞懂事件分发机制我们先看看这个方法。
MotionEvent
MotionEvent常用四种类型:
MotionEvent.ACTION_DOWN 按下View(所有事件的开始)
MotionEvent.ACTION_UP 抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE 滑动View
MotionEvent.ACTION_CANCEL 结束事件
当然你打开MotionEvent会发现里面有很多touch的类型方法如:ACTION_HOVER_MOVE、ACTION_POINTER_UP
总结一下:一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事
事件分发机制流程
上面咱们看了一个重要的方法MotionEvent(处理事件的类型为事件分发做准备),到这我们看一下事件分发的流程。
事件分发的传递顺序是:Activity -> ViewGroup -> View
即:点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View
这里说一下View和ViewGroup区别:大白话说,我们去自定义控件的时候可能这个控件不像Button一样,是一个简单的绘制的一个距形框上绘制字体,比如LineaLayout,我们自带的可以包含子布局的控件,ViewGroup相比较View多一个onLayout方法,也就是设置子布局摆放位置的相关方法。但是viewGroup最终调用的还是View,所以说viewGroup也是view多一个子类,但是它是一个特殊的view因为view可以分为两类:我本身只是个view,我不会再容下其他View。而我们的特殊View(ViewGroup)我这个view还可以放其他view,我可以约束你,让你规范。(好难表达)?
事件分发过程
方法 | 作用 | 调用时机 |
---|---|---|
dispatchTouchEvent() | 分发点击事件 | 当事件传递给view时(按下屏幕时机) |
onTouchevent () | 处理点击事件 | 在dispatchTouchEvent内部事件没有被父类消耗掉是调用 |
onInterceptTouchEvent() | 判断是否拦截某个点击事件,View无改方法 | 在ViewGroup的dispatchTouchEvent内部调用 |
这里可以看到对于view、viewGroup、Activity他们的事件分发机制是不同的,说到这最难的时候来了,空说无凭啊,他们源码到底是如何实现的呢?
Activity的事件分发
在Activity中分别调用者三个方法(注意包名)
activity是:package android.app;
View、ViewGroup是:package android.view;
this.dispatchTouchEvent();
view.dispatchTouchEvent()
viewGroup.dispatchTouchEvent()
点开this.dispatchTouchEvent();
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();//方法1
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;//方法2
}
return onTouchEvent(ev);//方法3
}
方法1:
看到的是如下的空方法:大概意思是你需要自己去实现,处理通知栏相关的,比如你下滑、上推通知栏,也就是用户实现交互的方式。
public void onUserInteraction() {
}
方法2:
getWindow().superDispatchTouchEvent(ev)
mDecor.superDispatchTouchEvent(event)
属于顶层View(DecorView)
a. DecorView类是PhoneWindow类的一个内部类
b. DecorView继承自FrameLayout,是所有界面的父类
c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
*/
@return boolean Return true if this event was consumed
这个是官方的注释,若getWindow().superDispatchTouchEvent(ev)的返回true表示该事件停止往下传递,被消耗掉。
方法3:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
这个是对view的边界处理,点击事件在边界外返回true,否则返回false,而这个mWindow.shouldCloseOnTouch(this, event)内部实现的是对边界判断。
同时我们也发现Activity是没有onInterceptTouchEvent方法的。这是因为Android里面只有可以作为双亲的视图才会有onInterceptTouchEvent,因此也只有ViewGroup才有这个方法,用来截取触摸事件
这里简单总结一下:
这便是Activity的事件传递
viewGroup事件分发
嗯,乍一看完全看不懂,要比Activity的dispatchTouchEvent要复杂的多,但是,我们还是可以看懂一点的。我们知道viewgroup是有onInterceptTouchEvent这里是对事件是否拦截做的处理,源码里也有体现
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
!onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
// a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
// b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
这里是对内部view循环判断处理,找到当前点击对view
// 条件判断的内部调用了该View的dispatchTouchEvent()
// 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
所以有以下事件分发的流程图
#### View的事件分发机制
view和ViewGroup是在同一个类。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
// 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
// 1. mOnTouchListener != null
// 2. (mViewFlags & ENABLED_MASK) == ENABLED
// 3. mOnTouchListener.onTouch(this, event)
// 下面对这3个条件逐个分析
/**
* 条件1:mOnTouchListener != null
* 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
// 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
}
/**
* 条件2:(mViewFlags & ENABLED_MASK) == ENABLED
* 说明:
* a. 该条件是判断当前点击的控件是否enable
* b. 由于很多View默认enable,故该条件恒定为true
*/
/**
* 条件3:mOnTouchListener.onTouch(this, event)
* 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
// 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
// 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)