当前位置: 代码迷 >> Android >> touch和click/longclick的关系
  详细解决方案

touch和click/longclick的关系

热度:90   发布时间:2016-04-28 02:21:42.0
菜鸟进阶之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。


  相关解决方案