Android按键事件传递流程(4)

1,952 阅读5分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

Android按键事件传递流程(4)

1.ViewGroup的dispatchKeyEvent

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 1);
    }
​
  
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
        == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
               == PFLAG_HAS_BOUNDS) {
        //递归传递到所有的布局和view中
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }
  // 这个判读主要是对按键事件进行一致性的检查,防止同样的错误会发生多次。
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
    }
    return false;
}

PFLAG_FOCUSED和PFLAG_HAS_BOUNDS分别确定了ViewGroup的焦点和大小边界。

mFocused表示ViewGroup中获得焦点的子View,如果该view或ViewGroup的焦点的大小边界已经确定,则调用该view或ViewGroup的dispatchKeyEvent。

假设Activity的布局如下图:

2021-11-10 10-26-18屏幕截图.png 如图焦点在C上面,则dispatchKeyEvent的传递流程是:ViewGroup -> A -> B -> C。

2.view的dispatchKeyEvent

public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 0);
    }
​
    // Give any attached key listener a first crack at the event.
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }

ListenerInfo类用于描述所有view相关监听器信息的类,例如OnFocusChangeListener,OnClickListener等。如果某个view设置了监听器则在对应的OnClickListener监听方法中会调用getListenerInfo创建ListenerInfo对象赋值给mListenerInfo;

//传递到KeyEvent  
if (event.dispatch(this, mAttachInfo != null
                       ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }
​
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

如果没有设置监听器或没有被监听器消耗掉,则该事件会继续想传递到KeyEvent的dispatch。

3.KeyEvent的dispatch

public final boolean dispatch(Callback receiver, DispatcherState state,Object target)

该方法中的参数Callback receiver既可能是Activity对象,有可能是View对象,因为是view的dispatchKeyEvent传过来的,所以目前分析的是View对象。

DispatcherState state View.java通过getKeyDispatcherState返回KeyEvent内部类DispatcherState对象,该对象用来进行高级别的按键事件处理,如长按事件等等;

public KeyEvent.DispatcherState getKeyDispatcherState() {
    //mAttachInfo是AttachInfo对象,当view关联到窗口时的一系列信息,AttachInfo类用来描述、跟踪这些信息,一般情况下不为空
    return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null;
}

下面继续分析KeyEvent的dispatch:

public final boolean dispatch(Callback receiver, DispatcherState state,
            Object target) {
    switch (mAction) {
        case ACTION_DOWN: {
            
            //清掉FLAG_START_TRACKING标记
            mFlags &= ~FLAG_START_TRACKING;
            if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
                             + ": " + this);
            //用view的onKeyDown方法
            boolean res = receiver.onKeyDown(mKeyCode, this);
            if (state != null) {
                //view的onKeyDown已经消耗掉,且是第一次按下,mFlags包含FLAG_START_TRACKING
                if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
                    if (DEBUG) Log.v(TAG, "  Start tracking!");
                    //跟踪按键事件
                    state.startTracking(this, target);
                    //如果是长按事件,且正在跟踪当前按键onKeyLongPress处理,用户可根据需求去重写该方法
                } else if (isLongPress() && state.isTracking(this)) {
                    try {
                        if (receiver.onKeyLongPress(mKeyCode, this)) {
                            if (DEBUG) Log.v(TAG, "  Clear from long press!");
                            state.performedLongPress(this);
                            res = true;
                        }
                    } catch (AbstractMethodError e) {
                    }
                }
            }
            return res;
        }
        case ACTION_UP:
            if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
                             + ": " + this);
            if (state != null) {
                state.handleUpEvent(this);
            }
            //回调view的onKeyUp方法
            return receiver.onKeyUp(mKeyCode, this);
        case ACTION_MULTIPLE:
            final int count = mRepeatCount;
            final int code = mKeyCode;
            if (receiver.onKeyMultiple(code, count, this)) {
                return true;
            }
            if (code != KeyEvent.KEYCODE_UNKNOWN) {
                mAction = ACTION_DOWN;
                mRepeatCount = 0;
                boolean handled = receiver.onKeyDown(code, this);
                if (handled) {
                    mAction = ACTION_UP;
                    receiver.onKeyUp(code, this);
                }
                mAction = ACTION_MULTIPLE;
                mRepeatCount = count;
                return handled;
            }
            return false;
    }
    return false;
}

dispatch主要作用是回调view的onKeyDown、onKeyUp

3.1 view的onKeyDown

public boolean onKeyDown(int keyCode, KeyEvent event) {
    //主要针对KEYCODE_DPAD_CENTER、KEYCODE_ENTER事件进行处理:
    if (KeyEvent.isConfirmKey(keyCode)) {
        //如果当前按键事件包含KEYCODE_DPAD_CENTER、KEYCODE_ENTER,并且view设置了DISABLED属性,直接返回true,说明该view已经被按下
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return true;
        }
​
        if (event.getRepeatCount() == 0) {
            // Long clickable items don't necessarily have to be clickable.
            final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE
                || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
            if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) {
                // For the purposes of menu anchoring and drawable hotspots,
                // key events are considered to be at the center of the view.
                final float x = getWidth() / 2f;
                final float y = getHeight() / 2f;
                if (clickable) {
                    //如果view设置了单击CLICKABLE或长按状态LONG_CLICKABLE,调用setPressed把该view设置为PRESSED状态
                    setPressed(true, x, y);
                }
                
                checkForLongClick(
                    ViewConfiguration.getLongPressTimeout(),
                    x,
                    y,
                    // This is not a touch gesture -- do not classify it as one.
                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION);
                return true;
            }
        }
    }
​
    return false;
}

checkForLongClick方法的主要工作:

如果仅是长按状态,系统在500秒后执行下面几种情形:

a. 如果有长按监听器OnLongClickListener,就回调onLongClick,如果成功,系统会发出一个触觉反馈

b. 如果没有长按监听器,就显示一个菜单。

如果按键事件不包含KEYCODE_DPAD_CENTER、KEYCODE_ENTER,onKeyDown不作任何处理,直接return false

3.2 view的onKeyUp

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (KeyEvent.isConfirmKey(keyCode)) {
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return true;
        }
        if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
            //setPressed(false)去除view的pressed状态,同时更新其绘制状态(显示状态)
            setPressed(false);
​
            //在onKeyDown中没有处理长按事件false
            if (!mHasPerformedLongPress) {
                // This is a tap, so remove the longpress check
                //把长按事件对象从消息的队列中移除
                removeLongPressCallback();
                if (!event.isCanceled()) {
                    return performClickInternal();//当遥控器按键(KEYCODE_DPAD_CENTER、KEYCODE_ENTER)松开时,performClick()如果设置了OnClickListener监听器,会调用onClick方法
                }
            }
        }
    }
    return false;
}

4. KeyEvent的dispatch——Activity

如果Activity里面的任何view或ViewGroup都没有处理按键,就会传递到Activity的onKeyDown,onKeyUp。

4.1 activity的onKeyDown

if (keyCode == KeyEvent.KEYCODE_BACK) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.ECLAIR) {
 event.startTracking();
} else {
 onBackPressed();
}
    return true;
}

只有松开BACK按键时才会退出Activity,如果不松开不会退出Activity。随后根据按键模式mDefaultKeyMode决定做哪些事情......

4.2 activity的onKeyUp

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getApplicationInfo().targetSdkVersion
        >= Build.VERSION_CODES.ECLAIR) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
            && !event.isCanceled()) {
            //返回退出activity
            onBackPressed();
            return true;
        }
    }
    return false;
}

如果onKeyDown,onKeyUp没有消耗掉按键事件,就逆向返回到KeyEvent的dispatch中处理,如果仍然没有被消耗,就返回到Activity —-> DecorView —-> PhoneWindow,进入到 PhoneWindow中处理,下面对其进行讲解。

5. PhoneWindow的onKeyDown、onKeyUp

上述方法处理后,如果仍然返回false,则表明没有被消耗掉,按键事件传递到了当前窗口的window处理DecorView中返回isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event);

onKeyDown、onKeyUp主要针对当前获取焦点的窗口的一些特殊按键的处理,例如音量键+/-。

6.总结

  1. 当按键事件传递到View树根DecorView后会分为两步:

    1. view树内
    2. 外部(获得焦点的窗口)如果没有被view树内消耗就会传递到view树外,传递给获得焦点的窗口的keydown和keyup事件。
  2. view树内的传递一般先传递到当前的Activity

  3. Activity对象内部分发给ViewGroup,viewGroup如果本身有焦点就传递给其父类view,如果没有就继续传递给获取焦点的子View

    1. 如果子view是LinearLayout等常见布局,就递归传递给获得焦点的view视图;
    2. 如果子view就是view视图,就传递给该视图;
  4. view视图内部,如果设置了OnKeyListener监听器,就传递给OnKey;如果没有OnKeyListener监听器,就分发给KeyEvent的dispatch,dispatch主要回调view的onKeyDown/onKeyUp;

  5. 在view的onKeyDown/onKeyUp中,如果是KEYCODE_DPAD_CENTER,KEYCODE_ENTER,直接return true;详细看第二部分