简介
Android事件分发机制,可以算是面试时的常客。可是很多人对其流程只有一个大致的了解,并未对各种情况进行跟踪观察,这也导致对触摸事件的应用,只处于低级阶段。
数学右手坐标系 计算机左手坐标系
Camera位置(0,0,-576)
将3D坐标系中心点设置为x,y
matrix.preTranslate(-x, -y)
matrix.postTranslate(x, y)
ACTION_DOWN消息总结:
- dispatchTouchEvent和onTouchEvent函数一旦返回true,将截断消息传递,后续节点都将无法收到ACTION_DOWN消息。
- onIterceptTouchEvent函数返回true,只会改变ACTION_DOWN消息的正常流向,消息会流向自己的onTouchEvent函数中,并不会截断消息。
- 当dispatchTouchEvent函数返回false时,同样只会改变ACTION_DOWN消息的正常流向,消息会直接流向其父控件的onTouchEvent函数中,同样不会截断消息。
- 一般我们在拦截消息时,都是使用onIterceptTouchEvent和onTouchEvent函数,通过onIterceptTouchEvent返回true,将消息拦截,发送给自己的onTouchEvent函数处理,同时onTouchEvent函数返回ture,表示自身消费了该事件。
ACTION_MOVE消息总结:
- 在dispatchTouchEvent函数中返回true拦截消息后,ACTION_MOVE消息的流向与ACTION_DOWN消息完全相同,消息会直接停止传递,后面的子控件都不会接收到这个消息。
- 无论ACTION_DOWN消息的流向是怎么样的,只要最终被某个控件的onTouchEvent消费了,那后续ACTION_MOVE消息会先流到该控件的dispatchTouchEvent函数中,然后直接流向该控件的onTouchEvent函数。
只拦截ACTION_MOVE消息
- 要想拦截ACTION_MOVE消息,必须让其或其子控件消费ACTION_DOWN消息。
- 当onIterceptTouchEvent函数中进行ACTION_MOVE消息拦截时,第一次ACTION_MOVE消息到达时,会给将ACTION_CANCLE事件发送给消费控件,后续ACTION_MOVE消息都将onIterceptTouchEvent->onTouchEvent(如果返回true,就不会向父类传递,否则会传递给父类的onTouchEvent)。
- dispatchTouchEvent拦截ACTION_MOVE消息(返回true),那么ACTION_MOVE消息将截断,而ACTION_UP事件不受影响。
- dispatchTouchEvent拦截ACTION_MOVE消息(返回false),那么ACTION_MOVE消息都将流向Activity的onTouchEvent的方法,而ACTION_UP事件不受影响。
禁用父类拦截消息
- 要想requestDisallowInterceptTouchEvent(true)生效。不能让父类拦截ACTION_DOWN消息。
- 只有在父控件在onIterceptTouchEvent函数中拦截ACTION_MOVE消息时,requestDisallowInterceptTouchEvent(true)才会有效。
- 跳过requestDisallowInterceptTouchEvent(true)禁止父控件拦截消息时,所有父控件的onIterceptTouchEvent函数都将被跳过。
view.getWidth() = view.getRight() - view.getLeft()
view.getHeight() = view.getBottom() - view.getTop()
view.getX() = view.getLeft() + view.getTranslationX()
view.getY() = view.getTop() + view.getTranslationY()
scrollTo和scrollBy只能移动View的内容,无法移动View自身位置和背景。
多点触控与ACTION_MOVE消息
- getActionIndex的有效性:
使用getActionIndex可以获得当前操作的手指索引指。不过请注意,getActionIndex只在手指按下和抬起时有效,移动时是无效的。
- getX与getX(pointerIndex)
1、getX:其实getX函数不仅可以用于单点触控,其实还可以在多点触控中用于获取当前活动手指的X坐标值。
2、getX(pointerIndex):该函数用于多点触控,因为它是通过指定手指的PointIndex指来获取特定手指位置的,所以无论在当前哪个消息中,都可所用。当然,在ACTION_MOVE消息中,只能用它获取手指位置。
ViewDragHelper
public abstract static class Callback {
/**
* Called when the drag state changes. See the <code>STATE_*</code> constants
* for more information.
*
* @param state The new drag state
*
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/
public void onViewDragStateChanged(int state) {}
/**
* Called when the captured view's position changes as the result of a drag or settle.
*
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/
public void onViewPositionChanged(@NonNull View changedView, int left, int top, @Px int dx,
@Px int dy) {
}
/**
* Called when a child view is captured for dragging or settling. The ID of the pointer
* currently dragging the captured view is supplied. If activePointerId is
* identified as {@link #INVALID_POINTER} the capture is programmatic instead of
* pointer-initiated.
*
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {}
/**
* Called when the child view is no longer being actively dragged.
* The fling velocity is also supplied, if relevant. The velocity values may
* be clamped to system minimums or maximums.
*
* <p>Calling code may decide to fling or otherwise release the view to let it
* settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
* one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
* and the view capture will not fully end until it comes to a complete stop.
* If neither of these methods is invoked before <code>onViewReleased</code> returns,
* the view will stop in place and the ViewDragHelper will return to
* {@link #STATE_IDLE}.</p>
*
* @param releasedChild The captured child view now being released
* @param xvel X velocity of the pointer as it left the screen in pixels per second.
* @param yvel Y velocity of the pointer as it left the screen in pixels per second.
*/
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {}
/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
*
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
/**
* Called to determine the Z-order of child views.
*
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/
public int getOrderedChildIndex(int index) {
return index;
}
/**
* Return the magnitude of a draggable child view's horizontal range of motion in pixels.
* This method should return 0 for views that cannot move horizontally.
*
* @param child Child view to check
* @return range of horizontal motion in pixels
*/
public int getViewHorizontalDragRange(@NonNull View child) {
return 0;
}
/**
* Return the magnitude of a draggable child view's vertical range of motion in pixels.
* This method should return 0 for views that cannot move vertically.
*
* @param child Child view to check
* @return range of vertical motion in pixels
*/
public int getViewVerticalDragRange(@NonNull View child) {
return 0;
}
/**
* Called when the user's input indicates that they want to capture the given child view
* with the pointer indicated by pointerId. The callback should return true if the user
* is permitted to drag the given view with the indicated pointer.
*
* <p>ViewDragHelper may call this method multiple times for the same view even if
* the view is already captured; this indicates that a new pointer is trying to take
* control of the view.</p>
*
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
*
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/
public abstract boolean tryCaptureView(@NonNull View child, int pointerId);
/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return 0;
}
/**
* Restrict the motion of the dragged child view along the vertical axis.
* The default implementation does not allow vertical motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return 0;
}
}
1、GestureDetector
概述 当用户触摸屏幕的时候,会产生许多手势,如down、up、 scroll fling 等。 我们知道,View 类有- - -个View.OnTouchListener内部接口,通过重写它的onTouch(View v, MotionEvent event)函数,可以处理一些touch 事件。但是这个函数太过简单,如果需要处理 一些复杂的手势,使用这个接口就会很麻烦。 Android SDK给我们提供了GestureDetector (手势检测)类,通过这个类可以识别很多手 势。在识别出手势之后,具体的事务处理则交由程序员自己来实现。 GestureDetector类对外提供了两个接口(OnGestureListener、 OnDouble TapListener)和一 个外部类(SimpleOnGestureListener)。 这个外部类其实是两个接口中所有函数的集成,它包 含了这两个接口里所有必须实现的函数,而且都已经被重写,但所有函数体都是空的。该类 是一个静态类,程序员可以在外部继承这个类,重写里面的手势处理函数。
这里重写了6个函数,这些函数在什么情况下才会被触发呢?
- onDown(MotionEvente): 用户按下屏幕就会触发该函数。 ●onShowPress(MotionEvent e):如果按下的时间超过瞬间,而且在按下的时候没有松开 或者是拖动的,该函数就会被触发。 ●onLongPress(MotionEvente): 长按触摸屏,超过一定时长,就会触发这个函数。 触发顺序: onDown→onShowPress→onLongPress ●onSingleTapUp(MotionEvent e):从名字中也可以看出,一次单独的轻击抬起操作,也 就是轻击一下屏幕,立刻抬起来,才会触发这个函数。当然,如果除down以外还有 其他操作,就不再算是单独操作了,也就不会触发这个函数了。 单击一下稍微慢一-点的(不滑动) Touchup, 触发顺序为: onDown→onShowPress-onSingleTapUp- onSingleTapConfi rmed ●onFling(MotionEvent el, MotionEvent e2, float velocityX,float velocityY) :滑屏,用户按 下触摸屏、快速移动后松开,由一个MotionEvent ACTION DOWN、 多个 ACTION MOVE、一个ACTION_ UP触发。 ●onScrol(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY):在屏幕上拖动 事件。无论是用手拖动View, 还是以抛的动作滚动,都会多次触发这个函数,在 ACTION_ MOVE动作发生时就会触发该函数。 滑屏,即手指触动屏幕后,稍微滑动后立即松开,触发顺序为: onDown→onScroll- + onScroll- +onScroll→...→onFling 拖动,触发顺序为: onDown→onScroll→onScroll- onFling 可见,无论是滑屏还是拖动,影响的只是中间onScroll 被触发的数量而已,最终都会触 发onFling事件。.
1、默认情况下事件传递流程:
1、无组件消费Down事件
dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回false
onTouchEvent返回false
2、有组件消费Down事件
dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回false
有组件onTouchEvent返回true
2、Down事件在dispatchTouchEvent返回true
有组件dispatchTouchEvent返回true
onInterceptTouchEvent返回false
onTouchEvent返回false
3、Down事件在dispatchTouchEvent返回false
1、无组件消费Down事件
有组件dispatchTouchEvent返回false onInterceptTouchEvent返回false onTouchEvent返回false
2、有组件消费Down事件
有组件dispatchTouchEvent返回false
onInterceptTouchEvent返回false
有组件onTouchEvent返回true
4、有控件拦截Down事件:
1、无组件消费Down事件
dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回true
onTouchEvent返回false
2、有组件消费Down事件
dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回true
有组件onTouchEvent返回true
5、ACTION_DOWN消息总结:
1、若在dispatchTouchEvent和onTouchEvent函数中返回true,将会截断消息,后续节点将不会收ACTION_DOWN 消息。
2、onInterceptTouchEvent函数只会改变ACTION_DOWN消息的正常流向,消息会直接流向自己的onTouchEvent函数中,并不会截断消息。
3、若在 dispatchTouchEvent函数中返回false拦截消息,同样会改变ACTION_DOWN消息的正常流向,消息会直接流向其父控件的onTouchEvent函数中,同样不会截断消息。
4、一般我们在拦截消息时,都是共同使用onInterceptTouchEvent和onTouchEvent函数的,通过在 onInterceptTouchEvent函数中返回 true,将 ACTION_DOWN消息流向自己的onTouchEvent函数中,然后在该onTouchEvent函数中返回 true拦截消息。
6、ACTION_MOVE消息总结:
1、在 dispatchTouchEvent 函数中返回 true拦截消息后,ACTION_MOVE 消息的流向与ACTION_DOWN消息的完全相同,消息会直接停止传递,后面的子控件都不会接收到这个消息。
2、无论ACTION_DOWN消息的流向是怎样的,只要最终流到onTouchEvent函数中就行。假设控件A最终在onTouchEvent 函数中消费了 ACTION_DOWN消息,那么ACTION_MOVE 消息的流向就是先流到控件A的 dispatchTouchEvent函数中,最终直接流到控件A的onTouchEvent 函数中,进而消息停止传递。
在ACTION_MOVE消息到来时拦截
为了在ACTION_MOVE消息到来时进行拦截事件,就必须让ACTION_DOWN消息被自身或子控件的onTouchEvent函数消费。
1、在onInterceptTouchEvent拦截Move事件
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case ACTION_MOVE:
return true;
}
return super.onInterceptTouchEvent(event);
}
上图看起来不容易理解,利用4条线来帮助大家理解。
红线:这条线表示ACTION_DOWN消息的传递流程,也就是上面讲解的在TextView的onTouchEvent函数中消费ACTION_DOWN 消息的传递流程。
绿线:这条线表示ACTION_MOVE消息第一次传递时的流向情况。本来消息依然会从Activity的dispatchTouchEvent函数流向子控件,但是在到达ViewGroupl的onInterceptTouchEvent 函数时,消息被拦截了。到这里,这次的ACTION_MOVE 消息就没有了,变成了ACTION_CANCEL消息继续向子控件传递,一直传递到ACTION_MOVE消息原本要传递的位置,通知所有被截断的子控件,它们的消息取消了,后面没有消息再传递过来。当我们收到ACTION_CANCEL消息时,就表示后续不会再获得消息,一般需要像处理ACTION_UP消息一样处理该消息,执行控件归位等操作。
蓝线:这条线表示消息被截断之后的ACTION_MOVE、ACTION_UP消息的流向。可以看到,这时候的ACTION_MOVE 消息的流向与正常情况下ViewGroup1的 dispatchTouchEvent函数拦截ACTION_DOWN消息时ACTION_MOVE消息的流向是完全相同的。在这里,我没有在ViewGroup1的onTouchEvent 函数中进行返回true的消息拦截,所以消息最终会流到Activity的onTouchEvent函数中。
需要特别注意的是,虽然ACTION_MOVE消息最终会流到Activity的onTouchEvent函数中,但后续的 ACTION_MOVE 消息并不会像正常处理流程一样(可以查看黑线,从 Activity的 dispatchTouchEvent 函数直接流到Activity 的 onTouchEvent函数中),而是每次ACTION_MOVE 消息的流向都与绿线保持一致,这就说明在这种情况下即使所有控件的onTouchEvent 函数都不拦截消息,ACTION_MOVE消息依然会走完全程。
其实这一点非常好理解,因为我们是在 ViewGroup1的 ACTION_MOVE消息到来时进行拦截的,而对于它的父控件,这里是Activity,并不知道这件事,它只会按照正常流程下ACTION_MOVE消息的流程来传递消息,所以每次ACTION_MOVE消息都会流到ViewGroup1中,只是ViewGroup1进行了拦截而已。
2、在dispatchTouchEvent拦截Move事件
2.1、返回true
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case ACTION_MOVE:
return true;
}
return super.dispatchTouchEvent(event);
}
红线:表示ACTION_DOWN消息传递流程。
绿线:表示ACTION_MOVE消息传递流程,在dispatchTouchEvent中被截断,子控件不会收到ACTION_CANCEL事件。
蓝线:表示ACTION_UP消息传递流程,这种情况下的ACTION_UP和正常情况下ACTION_UP消息传递流程相同。
2.2、返回false
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case ACTION_MOVE:
return false;
}
return super.dispatchTouchEvent(event);
}
如果dispatchTouchEvent拦截MOVE事件返回true时,事件将直接流到Activity的onTouchEvent中。
3、详解requestDisallowInterceptTouchEvent
requestDisallowInterceptTouchEvent主要用途是告诉父类,不要拦截消息,即不再调用onInterceptTouchEvent。
当我们通过requestDisallowInterceptTouchEvent,来禁止父控件拦截消息时,该控件的所有父控件的onInterceptTouchEvent函数都将被跳过。
常见滑动冲突场景
1、外层与内层的滑动方向不一致
根据方向决定滑动那个View
2、外层与内层的滑动方向一致
只有一种解决方法:根据业务需求,通过下面的拦截和禁止方法,决定在什么情况下滑动那个View。
1、外部拦截法
需要重写父控件的onInterceptTouchEvent函数。
2、内部拦截法
父控件不拦截任何消息,所有消息都传递给子控件,如果子控件需要此消息就直接消费掉,否则就交给父控件来处理。利用requestDisallowInterceptTouchEvent实现的。