View的事件分发

说明

本篇所讲的View不包括Viewgroup

事件分发在解决滑动冲突的时候很重要。如果想做出完美的自定义控件。事件分发的掌握是必须的。

例子

我们继承Button写一个自定义的CustomButton,复写事件分发相关的方法,并打上log

public class CustomButton extends Button {
    private static final String TAG = CustomButton.class.getSimpleName();

    public CustomButton(Context context) {
        super(context);
    }

    public CustomButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
}

然后添加到Activity中

public class Main3Activity extends AppCompatActivity{
    private static final String TAG = Main3Activity.class.getSimpleName();

    private CustomButton customButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main3);

        customButton = (CustomButton)findViewById(R.id.cbtn);

        customButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();

                switch (action){
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG, "onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG, "onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG, "onTouch ACTION_UP");
                        break;
                }
                return false;
            }
        });

    }
}

点击button并稍微移动一下可以看到如下log:

07-31 10:38:57.710 31587-31587/com.fcott.customview E/CustomButton: dispatchTouchEvent ACTION_DOWN
07-31 10:38:57.711 31587-31587/com.fcott.customview E/Main3Activity: onTouch ACTION_DOWN
07-31 10:38:57.711 31587-31587/com.fcott.customview E/CustomButton: onTouchEvent ACTION_DOWN
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: dispatchTouchEvent ACTION_MOVE
07-31 10:38:57.906 31587-31587/com.fcott.customview E/Main3Activity: onTouch ACTION_MOVE
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: onTouchEvent ACTION_MOVE
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: dispatchTouchEvent ACTION_UP
07-31 10:38:57.906 31587-31587/com.fcott.customview E/Main3Activity: onTouch ACTION_UP
07-31 10:38:57.906 31587-31587/com.fcott.customview E/CustomButton: onTouchEvent ACTION_UP

执行顺序一目了然,按照顺序来一一了解

dispatchTouchEvent

下面是dispatchTouchEvent方法的源码

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        //未被其他窗口遮盖 
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

我们只看重要部分,result 它用来标识接收事件的 view 是否消耗了当前事件。

//未被其他窗口遮盖 
if (onFilterTouchEventForSecurity(event)) {
            //判断控件是否可用以及当前这个事件是否为滚动条拖动,不必关心
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //这里的 li.mOnTouchListener就是我们调用setOnTouchListener设置进去的listener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

由上我们可以看出,onTouchEvent和我们设置的onTouch事件都是在dispatchTouchEvent内部调用的。
首先判断li和li.mOnTouchListener不为null(li是view管理所有监听事件的一个类),并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接result为true ; 也就是下面的onTouchEvent(event)不会被执行了;
也就是说:如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了
接下来看看onTouchEvent这东西

onTouchEvent

又是老长一段源码

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        //如果当前view处于不可用状态依然会消耗这个事件,只是不会有任何回应。
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == 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)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        //如果设置了代理,则执行代理的onTouchEvent事件。(代理的作用是可以扩大view的反应区域而不改变view本身大小)
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //重点,如果我们的View可以点击或者可以长按,则最终一定return true ;
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                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, x, y);
                       }

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

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

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

上面的关键部分我用注释解释了一下大概的作用,接下来着重看一下重点部分

MotionEvent.ACTION_DOWN

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

先设置mHasPerformedLongPress=false;表示长按事件还未触发;
然后判读isInScrollingContainer(),isInScrollingContainer的注释已经写得很明白了,就是遍历整个View树来判断当前的View是不是在一个滚动的容器中.因为对于触碰事件的处理,不能把滑动当成点击.所以先判断是不是在一个可滑动的容器中.

如果不在滑动容器中
执行

setPressed(true, x, y);
checkForLongClick(0, x, y);

说明:
PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动.
PFLAG_PRESSED 已经可以确定按下这一操作.

setPressed(true, x, y)的主要作用是给mPrivateFlags设置一个PRESSED的标识表示已经按下,并刷新背景
checkForLongClick(0, x, y)的作用是检测是否是长点击事件

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

发送一个延迟为ViewConfiguration.getTapTimeout() - delayOffset(500- 0ms)的延迟消息,到达延时时间后会执行CheckForLongPress()里面的run方法.

class CheckForLongPress implements Runnable {

        private int mOriginalWindowAttachCount;

        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }

        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }
    }

因为等待形成长按的过程中,界面可能发生变化如Activity的pause及restart,这个时候,长按应当失效.
View中提供了mWindowAttachCount来记录View的attach次数.当检查长按时的attach次数与长按到形成时.
的attach一样则处理,否则就不应该再当前长按. 所以在将检查长按的消息添加时队伍的时候,要记录下当前的windowAttachCount.

如果在滑动容器中
1.给mPrivateFlags设置一个PREPRESSED的标识
2.发送一个延迟为ViewConfiguration.getTapTimeout()(100毫秒)的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法.
说明:
在给定的tapTimeout(100ms)时间之内,用户的触摸没有移动,就当作用户是想点击,而不是滑动.
具体的做法是,将 CheckForTap的实例mPendingCheckForTap添加时消息队例中,延迟执行.
如果在这tagTimeout之间用户触摸移动了,则删除此消息.否则:执行按下状态.然后检查长按.

CheckForTap

private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }

在run方法里面取消mPrivateFlags的PREPRESSED,执行setPressed(true, x, y);方法和checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
和上面不在滑动容器中的情况一样,唯一的区别是这里的delayOffset时间不是0ms,而是100ms,因为在判断滑动还是按下的时候已经消耗了100ms.

我们来总结一下上面的分析,可以看到,当用户按下,首先会判断是否在滑动容器中,如果在滑动容器中则设置标识为PREPRESSED,如果100ms后,没有抬起,会将View的PREPRESSED标识去掉并且设置为PRESSED,然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - 100ms,这个100ms刚好时检测额PREPRESSED时间;也就是用户从DOWN触发开始算起,如果500ms内没有抬起则认为触发了长按事件:
1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;
2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;

MotionEvent.ACTION_MOVE

case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

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

判断手指是否移出view,如果移出则:
1、执行removeTapCallback();
2、然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();
3、最后把mPrivateFlags中PRESSED标识去除,刷新背景;

MotionEvent.ACTION_UP

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;
                        //看是否需要获得焦点及用变量focusTaken设置是否获得了焦点.
                        //如果我们还没有获得焦点,但是我们在触控屏下又可以获得焦点,那么则请求获得焦点.
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        //如果手指触摸屏幕不到100ms就放开,设置为按下
                        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, x, y);
                       }
                        //  如果mHasPerformedLongPress 返回false,并且不忽略本次up事件
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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.
                                //如果mPerformClick为null,初始化一个实例
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //立即通过handler添加到消息队列尾部,如果添加失败则直接执行 performClick()
                                //总之无论如都会执行performClick();
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        //如果触摸未满100ms就放开,则延迟64ms执行mUnsetPressedState
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        }  
                          //如果触摸满100ms,立即通过handler添加到消息队列尾部
                          //如果添加失败则直接执行 mUnsetPressedState.run();     
                          else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

上面关键部分已经用注释说明。需要特别说明的是:
1.performClick()

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

可以看到,performClick()执行的就是我们调用setOnclickListener设置的点击监听。说明onClick是在MotionEvent.ACTION_UP里执行的。

2.mUnsetPressedState

private final class UnsetPressedState implements Runnable {
        @Override
        public void run() {
            setPressed(false);
        }
    }

只有一句代码,把我们的mPrivateFlags中的PRESSED取消,然后刷新背景,把setPress转发下去。
3.mHasPerformedLongPress
我们在MotionEvent.ACTION_DOWN的时候获取了一个变量mHasPerformedLongPress,上面没讲这个东西是干嘛的。这里可以看到:

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 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.
                                //如果mPerformClick为null,初始化一个实例
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //立即通过handler添加到消息队列尾部,如果添加失败则直接执行 performClick()
                                //总之无论如都会执行performClick();
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

只有当mHasPerformedLongPress 为false的时候,我们才能触发performClick()事件。
也就是说,如果设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
如果没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;

总结

1、整个View的事件转发流程是:
View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
2、onTouchEvent中的DOWN,MOVE,UP
DOWN时(只讲在滑动容器内,如果不在滑动容器内,则跳过a部分即可):
a、首先设置mHasPerformedLongPress=false;然后设置标志为PREPRESSED ,最后发出一个100ms后的mPendingCheckForTap;
b、如果100ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-100ms的,检测长按任务消息;
c、如果按下500ms后(从DOWN触发开始算),则会触发LongClickListener:
此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;
MOVE时:
主要就是检测用户是否划出控件,如果划出了:
100ms内,直接移除mPendingCheckForTap;
100ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();
UP时:
a、如果100ms内,触发UP,此时标志为PREPRESSED,先设置setPressed(true, x, y)确定按下操作,再执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;
b、如果是100ms-500ms间(mHasPerformedLongPress一定为false),即长按还未发生,则首先移除长按检测,执行onClick回调;
c、如果是500ms以后,那么有两种情况:
i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
ii.没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容