Android事件分发机制

545 阅读13分钟

简介

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函数都将被跳过。

d43d656bf05b849f698148b1cab3079.jpg

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;
    }
}

image.png

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事件。.

image.png image.png

image.png

1、默认情况下事件传递流程:

1、无组件消费Down事件

dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回false
onTouchEvent返回false

image.png

2、有组件消费Down事件

dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回false
有组件onTouchEvent返回true

image.png

2、Down事件在dispatchTouchEvent返回true

有组件dispatchTouchEvent返回true
onInterceptTouchEvent返回false
onTouchEvent返回false

image.png

3、Down事件在dispatchTouchEvent返回false

1、无组件消费Down事件

有组件dispatchTouchEvent返回false onInterceptTouchEvent返回false onTouchEvent返回false

image.png

2、有组件消费Down事件

有组件dispatchTouchEvent返回false
onInterceptTouchEvent返回false
有组件onTouchEvent返回true

image.png

4、有控件拦截Down事件:

1、无组件消费Down事件

dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回true
onTouchEvent返回false

image.png

2、有组件消费Down事件

dispatchTouchEvent返回super.dispatchTouchEvent()
onInterceptTouchEvent返回true
有组件onTouchEvent返回true

image.png

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函数消费。

image.png

1、在onInterceptTouchEvent拦截Move事件

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case ACTION_MOVE:
            return true;
    }
    return super.onInterceptTouchEvent(event);
}

image.png

上图看起来不容易理解,利用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);
}

image.png

红线:表示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);
}

image.png

如果dispatchTouchEvent拦截MOVE事件返回true时,事件将直接流到Activity的onTouchEvent中。

3、详解requestDisallowInterceptTouchEvent

requestDisallowInterceptTouchEvent主要用途是告诉父类,不要拦截消息,即不再调用onInterceptTouchEvent。

当我们通过requestDisallowInterceptTouchEvent,来禁止父控件拦截消息时,该控件的所有父控件的onInterceptTouchEvent函数都将被跳过。

常见滑动冲突场景

1、外层与内层的滑动方向不一致

根据方向决定滑动那个View

2、外层与内层的滑动方向一致

只有一种解决方法:根据业务需求,通过下面的拦截和禁止方法,决定在什么情况下滑动那个View。

1、外部拦截法

需要重写父控件的onInterceptTouchEvent函数。

2、内部拦截法

父控件不拦截任何消息,所有消息都传递给子控件,如果子控件需要此消息就直接消费掉,否则就交给父控件来处理。利用requestDisallowInterceptTouchEvent实现的。