安卓开发艺术探索-View的事件体系-笔记

第三章:View的事件体系

View的位置参数:
  • View的位置参数left, top, right, bottom是相对于VIew的父容器来说的,因此是一种相对坐标
  • 宽高和坐标的关系:
    • width = right - left
    • height = bottom - top
  • 这四个参数对应于源码的mLeft, mRight, mTop, mBottom这四个成员变量,获取方式:
    • left = getLeft()
    • right = getRight()
    • Top = getTop()
    • Bottom = 个体Bottom()
MotionEvent和TouchSlop
  • 通过MotionEvent我们可以得到点击事件发生的坐标。
  • getX和getY得到的是相对于当前View的坐标
  • getRawX和getRawY是相对于屏幕的坐标
  • TouchSlop是系统所能识别出的被认为是滑动的最小距离,下面是获取的方式:
    • ViewConfiguration.get(getContext()).getScaledTouchSlop();
VeloccityTracker和GuestureDetector和Scroller
  • VelocityTracker:速度追踪,用于追踪手指在滑动中的速度
  • 使用:首先在View的onTouchEvent方法中追踪当前事件的速度:
1
2
3
4
5
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int xv = (int) velocityTracker.getXVelocity();
int yv = (int) velocityTracker.getYVelocity();
  • 注意:获取速度之前必须计算速度;第二点:这里的速度指的是一段时间内手指所划过的像素数
  • 最后不用的时候,应当调用clear和recycle来回收
GestureDetector
  • 使用:
1
2
3
4
5
GestureDetector detector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
detector.setIsLongpressEnabled(false);
boolean resume = detector.onTouchEvent(event);
return resume;
  • 首先创建一个对象,并实现其监听接口,接着在接管View的onTouchEvent方法中,接管该事件
Scroller
  • 实现View的滑动有三种方式:
    • View自身提供的scrollerTo/scrollerBy
    • 通过动画平移
    • 改变View的layoutParams
  • scrollerTo/scrollerBy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
  • scrollTo是绝对滑动
  • scrollBy是相对于自身位置滑动
  • 在滑动过程中,mScrollX的值等于View左边缘和View内容左边缘的水平距离,而mscrollY的值总是等于View上边缘和内容边缘在竖直方向上的距离
  • 使用这两种方法不能使得View本身滑动,只能是内容滑动
  • 使用动画:
  • 改变布局参数:通过改变Margin来滑动
1
2
3
4
5
6
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mFriends.getLayoutParams();
params.width += 10;
params.leftMargin += 10;
mFriends.requestLayout();
  • 总结:

    • scrollTo/scrollBy:操作简单,适合对View内容的滑动
    • 动画:操作简单。主要适合用于没有交互的View和实现复杂的动画效果
    • 改变布局参数:操作稍微复杂,用于有交互的View
实现弹性滑动
  • 使用Scroller
  • 用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Scroller mScroller = new Scroller(this);
private void smoothScrollTo(int desX, int desY) {
int scrollX = getScrollX();
int deltaX = desX - scrollX;
mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
mFriends.invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()) {
mFriends.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
mFriends.postInvalidate();
}
}
  • 首先构造一个Scroller对象,接着调用它的startScroll,但是该方法内部知识保存了几个传递的参数
  • 让View是实现弹性滑动的是invalidate(),该方法或导致View的重绘,在View的draw中又会去调用computeScroll,而computeScroll是我们自己实现的一个方法,在computeScroll中又会去获得当前的scrollX, scrollY,然后通过scrollTo实现滑动,接着又调用postInvalidate再进行重绘,如此反复,直到滑动完成
  • computeScrollOffset:该方法通过计算时间的流逝来计算出当前scrollX和scrollY的值,类似插值器的工作原理
总结:
  • Scroller本身并不能够实现View的滑动,它需要配合View的computerScroll来实现弹性滑动,它不断地让View重绘,而每次重绘距滑动起始时间会有一个间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo来实现滑动。就这样,每次View的重绘都会导致View进行小幅度的滑动,而多次的小滑动就组成了View的弹性滑动
  • 使用动画:
  • 使用延时策略:通过使用Handler来实现
View的事件分发机制
  • 相关的三个重要的方法:
    • public boolean dispatchTouchEvent(MotionEvent event):用于进行事件的分发。如果时间能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent()方法的影响,表示是否消费当前事件
    • public boolean onInterceptHoverEvent(MotionEvent event):在上述方法内部调用,用于判断是不是要拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被调用,返回结果表示是否拦截当前事件。
    • public boolean onTouchEvent(MotionEvent event):在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否拦截当前事件,如果不消耗,那么在同一个事件序列当中,当前View将无法接受到事件
  • 概述:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent会被调用,如果这个ViewGroup的onInterceptHoverEvent返回true表示它要拦截当前事件,接着该事件就会交给这个ViewGroup处理,即它的onTouchEvent会被调用;如果返回false表示不拦截此事件,这时该事件会传给它的子View,接着子View的dispatchTouchEvent会被调用,如此直到该事件被最终处理
源码分析:

1.Activity对点击事件的分发过程

  • 当一个点击事件发生时,事件最先传递给当前的Activity,由该Activity的dispatchTouchEvent来进行事件派发,具体的是又Activity内部的Window来完成的。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(framelayout),通过Activity.getWindow().getDecorView()可以获得。 下面是Activity的dispatchTouchEvent的源码:
1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
  • 首先事件开始交给Activity所依附的Window进行分发,如果返回true的话,整个事件循环就结束了,返回false的话意味着事件没有人处理,所有的View都返回onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用。
  • 接下来看Window是怎么样传给ViewGroup对象的,首先Window是一个抽象类,superDispatchTouchEvent方法也是抽象方法,其具体实现类是PhoneWindow,那么我们来看看其superDispatchTouchEvent:
1
2
3
4
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
  • PhoneWindow将事件直接传递给DecorView,我们来看看DecorView是什么?
  • 我们知道,通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecorView显然就是getWindow().getDecorView()返回的View,而我们通过setContentView()就是它的一个子View
  • 在这里开始,事件已经传递到顶级View了,即在Activity中通过setContentView所设置的View,另外顶级View也叫根View,顶级View一般说的是ViewGroup
顶级View对点击事件的分发过程
  • 首先看看其dispatchTouchEvent:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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;
}
  • 从上面的代码可以看出,在两种情况下ViewGroup会拦截此事件:事件类型为:ACTION_DOWN或者mFirstTouchTarget != null。当是事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向该子元素。
  • 也就是说,当ViewGroup不拦截事件并将事件交给子元素处理时,mFirstTouchTarget != null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget != null就不成立。
  • 那么当MOVE和UP事件传递过来的时候,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理
  • 还有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent来设置的,一旦被设置后,ViewGroup将无法拦截除了DOWN事件以外的点击事件。这时因此ViewGroup在分发事件时,如果是DOWN事件的话就会重置这个标记位,将导致子View中设置的标记无效,面对DOWN事件,ViewGroup总会询问自己是不是要拦截:
  • 小结:当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理,并且不再调用onInterceptTouchEvent。FLAG_DISALLOW_INTERCEPT这个标记的作用是让ViewGroup不再拦截事件,
  • 接着看ViewGroup不拦截事件的时候,事件会向下分发交给它的子View进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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;
  • 首先遍历ViewGroup所有子View,然后判断子View是不是能够接收点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是不是落在子元素的区域。如果子View满足两个条件的话,那么事件将交给它处理,
  • 实际调用子元素的dispatchTouchEvent是在dispatchTransformedTouchEvent中,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
  • 如果传递的字View不为null的话,就会调用子View的dispatchTouchEvent,这样事件就交给子View处理了
  • 如果子View的dispatchTouchEvent返回true的话,那么mFirstTouchTarget就会被赋值并跳出循环:
1
2
3
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
  • 上面的代码完成了对mFirstTouchTarget赋值并终止了对子View的遍历。
  • 如果子View返回false的话,ViewGroup会将事件分给下一个子View(还有下一个子 View的话)
  • 如果遍历所有子View后事件都没有被合适处理,这包含两种情况:
    • ViewGroup没有子View
    • 子元素处理了点击事件,但是在dispatchTouchEvent中返回false。
  • 上面这两种情况下ViewGroup会自己处理点击事件
1
2
3
4
5
6
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
  • 上面的代码中第三个参数为null,显然调用super.dispatchTouchEvent
View对点击事件的处理过程
  • 先看看其dispatchTouchEvent:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
  • 可以看到,首先会判断有没有设置OnTouchEventListener,如果OnTouchEventListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便外界处理点击事件
  • 接下来分析onTouchEvent:
1
2
3
4
5
6
7
8
9
10
11
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
  • 上面是View在不可用的状态下点击事件的处理过程,显然不可用的View照样会消费事件,只是没有为点击做出回应
  • 下面看看onTouchEvent对点击事件的具体处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
  • 只要View的CLICKABLE和LONG——CLICKABLE有一个为true的话,那么它就会消费这个事件,即onTouchEvent返回true,不管状态是不是DISABLE状态。然后就是当ACTION_UP事件发生时,就会出发performClick,如果View设置了OnClickListener的话,performClick内部就会调用onClick
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*/
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;
}
  • View的LONG_CLICKABLE默认设置为false,而CLICKABLE则要看具体的View,可点击的View的CLICKABLE为true,不可点击的为false。
  • setOnClickListener会自动将View的CLICKABLE设置为true,setOnLongClickListener也会将View的LONG_CLICKABLE设置为true,在源码可以找到,这里就不贴代码了。

  • 结论:

    • 正常情况下,一个事件序列只能被一个View拦截且消费,因为一旦一个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一事件序列中的事件不能分别由两个View同时处理,但是通过特殊的手段可以实现,比如一个View将本该自己处理的事件通过onTouchEvent返回false,强行传给其他View
    • 某个View一旦决定拦截,那么一个事件序列都只能有它处理,并且onInterceptTouchEvent不会被调用。
    • 某个View一旦开始处理事件,如果不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会交给它处理,并且事件将重新交给父元素去处理。
    • 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件由Activity来处理。
    • ViewGroup内部默认不拦截事件
    • View中不能拦截事件,一旦有事件交给它,它的onTouchEvent就会被调用
    • View的onTouchEvent都默认消费事件,除非它是不可点击的。
    • View的enable不影响onTouchEvent的默认返回值。
    • onClick的发生前提时当前View可点击,并且它收到了down和up事件
    • 事件传递过程是由外向内的,即事件总是先传递给父元素,容纳后再由父元素进行分发,通过requestDisalowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发,但是ACTION_DOWN除外
滑动冲突
  • 在界面中,只要内外两层同时可以滑动,这个时候就会产生滑动冲突
常见的滑动冲突场景:
  • 外部滑动和内部滑动方向不一致;
  • 外部滑动方向和内部滑动方向一致;
  • 上面两种情况的嵌套。
滑动冲突的处理规则
  • 对于场景一,处理的规则是:当用户左右(上下)滑动时,需要让外部的View拦截点击事件,当用户上下(左右)滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。
  • 对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。
  • 场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。
滑动冲突的解决方式
  • 外部拦截法:所谓的外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。下面是伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前事件){
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept=x;
mLastYIntercept=y;
return intercepted;
}

  • 内部拦截法:内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public boolean dispatchTouchEvent(MotionEvent even) {
int x = (int) even.getX();
int y = (int) even.getY();
switch (even.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowIntercepttouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if(父容器需要此类点击事件) {
parent.requestDisallowIntercepttouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(even);
}
}
  • 除了子元素需要做处理外,父元素也要默认拦截ACTION_DOWN以外的事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需要的事件,因此,父元素需要修改:
    1
    2
    3
    4
    5
    6
    7
    8
    public boolean onInterceptTouchEvent(MotionEvent even) {
    int action = even.getAction();
    if(about == MotionEvent.ACTION_DOWN) {
    return false;
    } else {
    return true;
    }
    }
分享到