Android Touch事件分发响应机制

概述

在Android中,事件包括了点按、长按、拖拽、滑动等,有了这些事件才能让Android响应用户的各种操作。但是归根结底,所有的这些事件都是以如下三个部分作为基础的:

  1. ACTION_DOWN(按下)
  2. ACTION_MOVE(移动)
  3. ACTION_UP(抬起)

所有的操作事件首先必须执行ACTION_DOWN(按下)操作,之后所有的操作都是以按下操作为前提,当按下操作完成后,接下来可能是一段ACTION_MOVE然后ACTION_UP,或者直接ACTION_UP。

所有操作事件的执行顺序必须是:ACTION_DOWN -> ACTION_MOVE -> ACTION_UP。(是否包含ACTION_MOVE取决于用户手势中是否包含了移动操作)

本文基于Android2.2.3版本进行分析(主要是因为:2.2.3版本代码清晰简单一些,而且原理是通用的)


Activity事件传递

Touch事件的产生涉及到硬件和Linux内核部分,虽然我在硬件组,但是我也不想去深究这块内容。目前只需要知道Touch事件产生后,最先响应它的是ActivitydispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

其中,onUserInteraction()是一个空方法,开发者可以根据自己的需求override这个方法,这个方法在一个Touch事件的周期中肯定是第一个被调用。

接着分析getWindow().superDispatchTouchEvent(ev),这个方法其实最终是调用了ViewGroup的dispatchTouchEvent方法。

小结:

通过上面的分析,我们至少可以知道,一个Touch事件首先经过Activity的dispatchTouchEvent方法处理,然后分配给了ViewGroup的dispatchTouchEvent方法。


ViewGroup响应Touch事件

重点就是分析dispatchTouchEvent()方法,2.2.3版本这个方法还算简单,添加中文注释的源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    // mScrollX代表视图起始坐标x轴方向偏移量
    // mScrollY代表视图起始坐标y轴方法偏移量
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;

    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    // 处理初始的ACTION_DOWN事件
    if (action == MotionEvent.ACTION_DOWN) {
        // 当行为是ACTION_DOWN时,应该将mMotionTarget置为null
        // 因为,ACTION_DOWN代表一个新的Touch事件
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }

        // 判断当前ViewGroup是否对touch事件进行拦截
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            // 从ViewGroup的子View中找到应该处理该touch事件的控件
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i --) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    // 获取当前子控件的矩形区域坐标
                    child.getHitRect(frame);
                    // 判断当前的Touch事件是否位于child的矩形区域中
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        // 获取Touch事件相对于View控件的偏移
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        // 调用child view的dispatchTouchEvent方法对Touch事件进行处理
                        if (child.dispatchTouchEvent(ev)) {
                            // 设置touch事件之后的处理View
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }

    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // 之前处理ACTION_DOWN的时候,已经确定了需要处理后续touch事件的子View
    final View target = mMotionTarget;

    // 这里说明点击的是ViewGroup的空白区域,这时的Touch事件就需要由ViewGroup自己来处理了
    if (target == null) {
        ev.setLocation(xf, yf);
        // ViewGroup的父类是View,所以这里其实也是调用了View的dispatchTouchEvent方法
        return super.dispatchTouchEvent(ev);
    }

    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        // 交给子View来处理Touch事件
        if (!target.dispatchTouchEvent(ev)) {
        }
        mMotionTarget = null;
        return true;
    }

    if (isUpOrCancel) {
        mMotionTarget = null;
    }

    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }

    return target.dispatchTouchEvent(ev);
}

由于ViewGroup默认的onInterceptTouchEvent的返回值为false,所以ViewGroup并不对TouchEvent进行拦截。

从代码中,我们可以看到,如果ViewGroup本身不对Touch事件进行拦截,则Touch事件最终是交给相应的子View去处理的。


View响应Touch事件

从ViewGroup响应Touch事件的源码分析来看,ViewGroup在不拦截Touch事件的前提下,是将Touch事件分发给Child View实现的。假设这里的Child View是一个Button,那我们来研究一下Touch事件在View里的传递规则。

查看Button的源码,我们是找不到dispatchTouchEvent方法实现的。但是,由于android.widget.Button继承自android.widget.TextView->android.view.View,最终我们发现dispatchTouchEvent方法是在View类中实现的。源码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
        mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}

Android2.2.3版本,superDispatchTouchEvent方法实现非常简单,但是已经足够说明问题。接下来,我们逐个分析if语句中的代码实现。

第一,mOnTouchListener是如何赋值的?

解答:从View中我们能找到如下方法:

public void setOnTouchListener(OnTouchListener l) {
    mOnTouchListener = l;
}

这个方法我们应该是非常熟悉,当开发者给控件注册touch事件响应对象的时候,mOnTouchListener就已经被赋值了。

第二,mOnTouchListener.onTouch()算是回调方法吗?

解答:必须算啊,我们在对控件注册onTouch事件对象的时候,都会重写onTouch方法,这里就是触发我们注册的ouTouch方法。并且,我们可以看到,如果我们在重写的onTouch方法中返回true,则该Touch事件就已经被处理完成,否则,就继续调用View的onTouchEvent()方法。

那接下来,我们就继续分析一下View的onTouchEvent()方法,源码如下:

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    // 被disable的View不响应Touch事件
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return ((viewFlags & CLICKABLE) == CLICKABLE || 
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if ((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_UP:
            boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
            if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (!mHasPerformedLongPress) {
                    removeLongPressCallback();

                    if (!focusTaken) {
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    mUnsetPressedState.run();
                }
                removeTapCallback();
            }
            break;

            case MotionEvent.ACTION_DOWN:
            // 主要是响应一下按下事件,例如修改控件的颜色等
            if (mPendingCheckForTap == null) {
                mPendingCheckForTap = new CheckForTap();
            }
            mPrivateFlags |= PREPRESSED;
            mHasPerformedLongPress = false;
            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
            break;

            case MotionEvent.ACTION_CANCEL:
            break;

            case MotionEvent.ACTION_MOVE:
            final int x = (int) event.getX();
            final int y = (int) event.getY();

            // 判断是否越界
            int slop = mTouchSlop;
            if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                (y < 0 - slop) || (y >= getHeight() + slop)) {
                removeTapCallback();

            }
            break;
        }
    }
}

onTouchEvent()方法看起来还是挺复杂的。这里,我们关注重点,特别是对ACTION_UP的处理。其中,对ACTION_UP的处理最终会调用到PerformClick类的实例化。接下来,我们看一下该类是如何实例化的:

private final class PerformClick implements Runnable {
    public void run() {
        performClick();
    }
}

public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    if (mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        mOnClickListener.onClick(this);
        return true;
    }
    return false;
}

之前我们分析过mOnTouchListener是在onTouch事件赋值的,那mOnClickListener肯定是在onClick事件进行赋值的,所以这里是在ACTION_UP事件中回调了onClick方法。

“`


参考资料

  1. Android Deeper-Touch事件分发响应机制
  2. Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
  3. Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值