Android View相关-事件分发机制详解-View

这篇文章我们来探究下Android中关于事件分发机制的一些细节和流程,由于这部分源码比较繁杂,拆开来讲,本文只探究View的事件分发流程,ViewGroup留到之后再说,在分析完这两者的事件分发机制之后我们来对Android的时间分发机制进行总结。那么本文就从View的子类Button来着手分析事件分发的流程,之后我们再从源码角度分析具体实现过程。

举个栗子

这里用一个很简单的小例子来演示View中dispatchTouchEvent、onTouchEvent、TouchListener的执行顺序,继承自Button的TestButton代码很简单,几个log:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        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);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        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);
}

Activity中设置onTouchListener,也是几个log:

testView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            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;
            default:
                break;
        }
        return false;
    }
});

接下来是执行流程,第一次点击按钮:

第二次点击后鼠标移动一下松开:

可以看到一次事件分发从ACTION_DOWN开始,到ACTION_UP结束,且其传递顺序是从dispatchTouchEvent –> onTouch –> onTouchEvent。下面我们来从源码中对这几个方法进行查看。

源码分析

我们先从View的dispatchTouchEvent开始吧:

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    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;
        }
    }
    ...
    return result;
}

我这里略去了部分代码,来看核心代码,可以看到第10行开始这里开始进行了判断,若同时附和这几个条件,则返回true,注意这里调用了OnTouchListener的onTouch方法,也就是说如果我们调用了View的setOnTouchListener方法那么在dispatchTouchEvent方法执行过程中,会调用OnTouchListener的onTouch方法,若onTouch方法返回true,则设置result为true,onTouchEvent不再执行,若onTouch方法返回为false,则第十行if语句不成立,result为改变,执行下一个判断语句,同时会执行onTouchEvent,若返回true则result为true,反之亦然。

我们继续来看一看onTouchEvent中的代码:

public boolean onTouchEvent(MotionEvent event) {
    //View状态为Disable并且可点击,返回true
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        return clickable;
    }
    //若TouchDelegate(触摸代理类)不为空,则调用其onTouchEvent方法并返回true
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //若可点击或者可长按以及长按出现ToolTip
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                break;
            case MotionEvent.ACTION_DOWN:
                ...
                break;
            case MotionEvent.ACTION_CANCEL:
                ...
                break;
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}

这段代码极长,有兴趣的朋友可以通读一下源码,我这里略去了一部分,前两个判断已经在注释里写清楚了,不多赘述,我们重要看下switch语句中的内容,接下来一个一个分析:

ACTION_DOWN

ACTION_DOWN是整个Touch流程的起点,代表触摸点按下操作。我们来看看onTouchEvent中的判断是怎样的:

case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;
    //判断是否为鼠标右键或者手写笔第一个按钮,若是,返回true后续代码不执行
    if (performButtonActionOnTouchDown(event)) {
        break;
    }
    //当前视图是否可滚动(例如:当前是ScrollView视图,返回true)
    boolean isInScrollingContainer = isInScrollingContainer();

    if (isInScrollingContainer) {
        // 滚动视图内,先不设置为按下状态,因为用户之后可能是滚动操作
        // 不是此次分析的重点,感兴趣可以自己了解下
        mPrivateFlags |= PREPRESSED;
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
        }
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    } else {
        // 不在滚动视图内,立即反馈为按下状态
        mPrivateFlags |= PRESSED;
        // 刷新为按下状态
        refreshDrawableState();
        //通过Handler发送一个延迟消息来判断是否是长按,500ms
        checkForLongClick(0);
    }
break;

各个步骤的注释已经写清楚了,下面分解一下各个方法:

首先是performButtonActionOnTouchDown

protected boolean performButtonActionOnTouchDown(MotionEvent event) {
    // 如果是鼠标右键,手写笔第一个按钮,看BUTTON_SECONDARY注释
    if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
        if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {
            return true;
        }
    }
    return false;
}

很简单的判断,如果成功就返回true,不成功就false。

接下来是isInScrollingContainer

public boolean isInScrollingContainer() {
    ViewParent p = getParent();
    while (p != null && p instanceof ViewGroup) {
        if (((ViewGroup) p).shouldDelayChildPressedState()) {
            return true;
        }
        p = p.getParent();
    }
    return false;
}

这里获取到了当前View的父控件,而后一层一层向上判断是否处于滚动容器中(shouldDelayChildPressedState返回true),如果是,则返回true

接下来是checkForLongClick

private void checkForLongClick(int delayOffset) {
    // 当前视图可以执行长按操作
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.rememberWindowAttachCount();
        // 延迟一段时间(500ms)把runnable添加到消息队列
        postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}

ACTION_DOWN中的逻辑大概就是这样

ACTION_MOVE

ACTION_MOVE代表触摸点发生滑动

case MotionEvent.ACTION_MOVE:
    //这个方法暂时不用关注
    if (clickable) {
        drawableHotspotChanged(x, y);
    }

    // Be lenient about moving outside of buttons
    if (!pointInView(x, y, mTouchSlop)) {
        // Outside button
        // Remove any future long press/tap checks
        removeTapCallback();
        removeLongPressCallback();
        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    }
break;

我们先来看看pointInView方法里做了什么:

public boolean pointInView(float localX, float localY, float slop) {
    return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
        localY < ((mBottom - mTop) + slop);
}

这个方法是判断是否划出控件可视区域,为了保证触摸点发生及轻微变化就导致ACTION_MOVE被执行,这里加入了一个slop的边界值,即在视图上下左右扩大slop

接下来是两个remove方法,这里我们放在一起讲:

private void removeTapCallback() {
    if (mPendingCheckForTap != null) {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        removeCallbacks(mPendingCheckForTap);
    }
}
private void removeLongPressCallback() {
    if (mPendingCheckForLongPress != null) {
        removeCallbacks(mPendingCheckForLongPress);
    }
}

这两个方法主要是删除触摸和长按回调(还记得之前按下后发送的长按延时消息吗)

ACTION_CANCLE

ACTION_CANCLE代表取消触摸操作,触摸流程结束

if (clickable) {
    setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

这里代码很简单,清除View状态

ACTION_UP

ACTION_UP代表触摸点抬起操作,同样是一个触摸流程的结束

case MotionEvent.ACTION_UP:    
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    if ((viewFlags & TOOLTIP) == TOOLTIP) {
        handleTooltipUp();//popWindow之类相关的,不必关心
    }
    //清除触摸状态以及触摸回调和长按的回调
    if (!clickable) {
        removeTapCallback();
        removeLongPressCallback();
        mInContextButtonPress = false;
        mHasPerformedLongPress = false;
        mIgnoreNextUpEvent = false;
        break;
    }
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
        // 当前视图处于预按下或者按下状态,如果失去焦点,获取焦点状态
        boolean focusTaken = false;
        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
            focusTaken = requestFocus();
        }

        if (prepressed) {
            //重设按下状态
            setPressed(true, x, y);
        }
        //长按未触发
        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
            //移除长按回调
            removeLongPressCallback();

            //按下状态执行点击事件
            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)) {
            //执行失败的话,保证视图不会永远处于按下状态
            //直接执行一次
            mUnsetPressedState.run();
        }
        // 清除轻触回调
        removeTapCallback();
    }
    mIgnoreNextUpEvent = false;
break;

这短代码是触摸流程里最重要的一部分。代码中已经注释清除,不必全部理解,理解流程即可。

到此,我们整个View的触摸流程就结束了,光看这部分还是很绕的,下一篇文章我们会详细讲讲ViewGroup中事件分发流程,并且和本篇做对照,可能会理解的更透彻。

enjoy~

我的个人博客

猜你喜欢

转载自blog.csdn.net/xiaomi987/article/details/80163317