菜鸟进阶之Android Touch事件传递(一)

罪过啊,已经有一个月没有写blog了(转载不算)。年末了各种加班,搞得身累心累的,也没有那心情写。但是春节临近,适逢今天放假,我再也不能抑制内心的那份冲动,必须得写几篇blog抒发一下新春佳节即将到来的这份愉快。

这几篇blog主要就是android的源码和自己的总结,有可能还有demo。我发现阅读android的源码是件很令人开心的事情,因为android源码够难,需要理清作者思路,揣测作者的意图,这是很有趣的事情,而且楼下还有一群大妈在跳舞助兴,一首不知道叫什么名的歌单曲循环了一个下午,我也是醉了,醉了一下午。

Android的Touch事件传递虽然代码量并不是很大,但是这块内容应该算是比较复杂的吧,尤其对我等菜鸟而言。鉴于我之前blog的内容表达不清除,有时我再看自己写的内容都不能理解。所以,我觉得要多附上代码,把我是怎么一步一步看代码的过程描述出来,少写一些我对代码的总结,如果写,也要斟酌再斟酌。

第一篇文章就写点简单的吧。1、OnTouchListener和onTouchEvent的关系。2、touch和click/longclick的关系(以前的blog好像写过他们之间的关系,只是那时的我还年轻,看到的是表象,现在从源码的角度来理解)。

PS:我用的是4.0的源码,不同版本的源码会有点去别。

OnTouchListener和onTouchEvent的关系

对于一个view来说,touch事件来了,最先到达的就是dispatchTouchEvent这个方法,那么我们来看这个方法(下面的代码记作代码A):

代码A:

public boolean  <span style="white-space:pre">dispatchTouchEvent</span>(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }
从代码A第10行的代码可以看到,现执行listener的onTouch方法,如果该方法返回true,那么onTouchEvent方法就不会被执行。他们的先后关系已经很明显。

touch和click/longclick的关系

onTouchEent方法,代码B:

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

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == 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));
        }

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

                        if (!mHasPerformedLongPress) {
                            // 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();
                    }
                    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();
                        }
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    break;

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

                    // 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;
    }
代码B的4-12行,如果该view是DISABLED的,那么基本不做处理,结束该方法。代码B的20行开始,如果该view是CLICKABLE或者LONG_CLICKABLE,后面的代码是对该投产事件进行处理的。根据touch的action来分别进行处理。

a、down(代码B的74-97行)

在第75行把mHasPerformedLongPress设置为false,这个量的作用从名字就可以看出来。代码82到96行,根据isInScrollingContaine这个变量不同处理,isInScrollingContaine值为false,执行94行和95行,94行设置该view的Pressed状态为true。我们先把95行这个方法搁置,来看isInScrollingContaine为true的时候,执行的内容是:把CheckForTap放入队列,在150毫秒后执行。来看的内容:

代码C:

private final class CheckForTap implements Runnable {
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true);
            checkForLongClick(ViewConfiguration.getTapTimeout());
        }
    }
可以看出来执行的方法和isInScrollingContaine值为false的时候代码基本一样,只是延迟了150毫秒执行。

那么我们来看checkForLongClick这个方法:

代码D:

private void checkForLongClick(int delayOffset) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.rememberWindowAttachCount();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
代码D第6行用到CheckForLongPress,从代码可以看到就是把CheckForLongPress的一个对象加载到队列里,在500毫秒之后(如果isInScrollingContaine值为true时,延迟350毫秒)执行这个CheckForLongPress。那么我们来看CheckForLongPress的内容。

代码E:

class CheckForLongPress implements Runnable {

        private int mOriginalWindowAttachCount;

        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }
可以看到CheckForLongPress的核心内容就是第8行的执行performLongClick这个方法。

down的总结:dwon事件处理,设置该view的longclick还没有被处理、修改该view显示的内容、在Runnable的队列里布置一个Runnable,再500毫秒后处理longclick事件。

b、move事件(代码B的105-120行)

110行,判断该touch是否还在这个view里面,这个方法里牵扯到一个mTouchSlop这里就不介绍它了(其实mTouchSlop我也还没弄明白)。112行取消掉代码B的91行布置的那个Runnable,第115行取消掉代码D第9行布置的那个Runnble。

c、cancel事件(代码B的100-103行)

取消之前布置的Runnable。

d、up事件(代码B的24-72行)

第24行代码,获得prepressed,prepressed值为true代表现在事件是在touch down之后的150毫秒之内,prepressed值为true代表touch down事件已经发生了超过150毫秒。至于这是怎么来的,通过跟踪mPrivateFlags与PFLAG_PREPRESSED之间的运算可得出,这里不方面演示出来。

第25行的判断,意思是说当前view已经被按下,两种情况:1.左边的很显然.2.当前view处在touch down之后的150毫秒以内,虽然(mPrivateFlags & PFLAG_PRESSED) != 0为true,但是该view在事实上已经被按下来。

第28-31行,改变焦点状态。第33-39行,注释很清楚。

第41-57行,如果该view没有被处理onLongClick,即touch down后不到500毫秒,那么:取消布置在队列里的Runnable,第50-55行代码,注释很清楚,就是执行performClick。

第59-69行,设置该view的Pressed状态为false。第79行,取消布置的Runnable。

总结

代码就看到这里,基本已经明了了。梳理一下处理过程:

1.touch down时间后会布置一个名为mPendingCheckForLongPress的Runnable,让该Runnable在500毫秒后执行。会根据该view是否在能滚动的组件里面,如果该view的父view(递归父view)存在可滚动的,那么还会布置一个名为mPendingCheckForTap的Runnable,该Runnable在150毫秒后执行。

2.touch cancel 取消touch down布置的Runnable。

3.touch move 根据情况是否取消touch down布置的Runnable。

4.touch up 改变焦点状态、根据情况是否取消touch down布置的名为mPendingCheckForLongPress的Runnable、执行performClick()、取消touch down布置的名为mPendingCheckForTap的Runnable。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值