Android U Input 系统:单按键手势功能

1,937 阅读8分钟

按键事件,在加入到 InputDispatcher 的 inbound queue 之前,会先询问策略是否截断,这个策略是由上层的 PhoneWindowManager 实现的。

一般来说,如果按键事件触发了单按键手势,例如,单击、长按,那么策略就一定会截断这个按键事件。否则,不截断。

本文就来分析下,策略是如何实现单按键手势。

单按键手势

策略是由上层的 PhoneWindowManager 实现的,当没有入队的按键事件到来时,它会处理按键手势功能,如下

// PhoneWindowManager.java

public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
    // ...

    // 系统是否处于交互状态
    final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
    
    // ...

    // 显示屏是否处于亮屏状态
    final boolean isDefaultDisplayOn = mDefaultDisplayPolicy.isAwake();
    final boolean interactiveAndOn = interactive && isDefaultDisplayOn;


    if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
        // 处理按键手势
        handleKeyGesture(event, interactiveAndOn);
    }

    // ...
}


private void handleKeyGesture(KeyEvent event, boolean interactive) {
    // 处理组合按键手势
    if (mKeyCombinationManager.interceptKey(event, interactive)) {
        mSingleKeyGestureDetector.reset();
        return;
    }

    // ...

    // 处理单按键手势
    mSingleKeyGestureDetector.interceptKey(event, interactive);
}

按键手势分为组合按键手势和单按键手势,本文分析单按键手势,下一篇文章分析组合按键手势。

// SingleKeyGestureDetector.java

void interceptKey(KeyEvent event, boolean interactive) {
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        // Store the non interactive state when first down.
        if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) {
            mBeganFromNonInteractive = !interactive;
        }
        interceptKeyDown(event);
    } else {
        interceptKeyUp(event);
    }
}

SingleKeyGestureDetector 分别对按键的 key down event 和 key up event 进行处理,从而实现单按键手势。下面分为三部分,分别看看按键的单击、多击、长按手势是如何实现的。

单击手势

按键按下,然后抬起,这是一次单击手势。首先看下按键按下时的 key down event 是如何处理的,如下

// SingleKeyGestureDetector.java

private void interceptKeyDown(KeyEvent event) {
    final int keyCode = event.getKeyCode();
    // ...
    
    // 1.保存按键按下的 key code
    mDownKeyCode = keyCode;

    // 2.根据 key code,获取一个有效的规则
    if (mActiveRule == null) {
        final int count = mRules.size();
        for (int index = 0; index < count; index++) {
            final SingleKeyRule rule = mRules.get(index);
            if (rule.shouldInterceptKey(keyCode)) {
                if (DEBUG) {
                    Log.i(TAG, "Intercept key by rule " + rule);
                }
                mActiveRule = rule;
                break;
            }
        }
        mLastDownTime = 0;
    }
    
    if (mActiveRule == null) {
        return;
    }

    // 这里是检测按键被按下多少次
    final long keyDownInterval = event.getDownTime() - mLastDownTime;
    mLastDownTime = event.getDownTime();
    if (keyDownInterval >= MULTI_PRESS_TIMEOUT) {
        mKeyPressCounter = 1;
    } else {
        // ...
    }

    if (mKeyPressCounter == 1) { // 按键首次按下
    
        // 处理规则支持按键长按功能
        if (mActiveRule.supportLongPress()) {
            // ...
        }
        
        // 处理规则支持非常时间长按功能
        if (mActiveRule.supportVeryLongPress()) {
            // ...
        }
    } else {
        // ...
    }
}

当按下按键时,SingleKeyGestureDetector 保存了按键的 key code,然后根据 key code 匹配一个规则,如下

// SingleKeyGestureDetector.java

abstract static class SingleKeyRule {
    private boolean shouldInterceptKey(int keyCode) {
        return keyCode == mKeyCode;
    }
}

再来看下按键抬起时的 key up event 是如何处理的,如下

// SingleKeyGestureDetector.java

private boolean interceptKeyUp(KeyEvent event) {
    // 重置 mDownKeyCode
    mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    
    if (mActiveRule == null) {
        return false;
    }

    // ...

    if (event.getKeyCode() == mActiveRule.mKeyCode) {
        // 1. Directly trigger short press when max count is 1.
        if (mActiveRule.getMaxMultiPressCount() == 1) {
            if (DEBUG) {
                Log.i(TAG, "press key " + KeyEvent.keyCodeToString(event.getKeyCode()));
            }
            Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                    1, mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessage(msg);
            mActiveRule = null;
            return true;
        }

        // 2. This could be a multi-press.  Wait a little bit longer to confirm.
        if (mKeyPressCounter < mActiveRule.getMaxMultiPressCount()) {
            Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                    mKeyPressCounter, mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessageDelayed(msg, MULTI_PRESS_TIMEOUT);
        }
        return true;
    }
    
    // ...
}

当按键抬起时,如果规则只支持单击,那么立即触发单击功能。而如果规则支持多击,那么需要发送一个延时消息来确认是触发单击还是要触发多击。如果在超时的时间内,按键没有再次按下,那么表示触发的就是单击功能,否则要触发多击功能。

处理单击功能的消息,其实就是让规则执行单击动作,如下

// SingleKeyGestureDetector.java

private class KeyHandler extends Handler {
    // ...

    @Override
    public void handleMessage(Message msg) {
        final SingleKeyRule rule = (SingleKeyRule) msg.obj;
        final int keyCode = msg.arg1;
        final int pressCount = msg.arg2;
        switch(msg.what) {
            // ...
            
            case MSG_KEY_DELAYED_PRESS:
                if (DEBUG) {
                    Log.i(TAG, "Detect press " + KeyEvent.keyCodeToString(keyCode)
                            + ", count " + pressCount);
                }
                if (pressCount == 1) {
                    // 让规则执行单击功能
                    rule.onPress(mLastDownTime);
                } else {
                    // ...
                }
                break;
        }
    }
}

多击手势

多击手势,以双击为例进行分析。

根据单击手势的分析,在按键抬起时,如果在一个超时时间内,按键再次按下,那么会触发多击功能,如下

// SingleKeyGestureDetector.java

private boolean interceptKeyUp(KeyEvent event) {
    // 重置 mDownKeyCode
    mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    
    // ...

    if (event.getKeyCode() == mActiveRule.mKeyCode) {
        // ...

        // This could be a multi-press.  Wait a little bit longer to confirm.
        if (mKeyPressCounter < mActiveRule.getMaxMultiPressCount()) {
            Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                    mKeyPressCounter, mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessageDelayed(msg, MULTI_PRESS_TIMEOUT);
        }
        return true;
    }
    
    // ...
}

假如在超时时间内,按键再次按下,就要移除单击手势的超时消息,避免触发单击手势,如下

// SingleKeyGestureDetector.java

private void interceptKeyDown(KeyEvent event) {
    final int keyCode = event.getKeyCode();
    
    // ...
    
    mDownKeyCode = keyCode;

    // ...

    final long keyDownInterval = event.getDownTime() - mLastDownTime;
    mLastDownTime = event.getDownTime();
    // 由于是在超时时间内再次按下按键,因此 keyDownInterval 要小于 MULTI_PRESS_TIMEOUT
    if (keyDownInterval >= MULTI_PRESS_TIMEOUT) {
        // ...
    } else {
        // 记录按键按下的次数
        mKeyPressCounter++;
    }

    if (mKeyPressCounter == 1) {
        // ...
    } else {
        // ...
        
        // 此时要触发多击,因此移除单击的消息
        mHandler.removeMessages(MSG_KEY_DELAYED_PRESS);

        // 如果达到规则最大的多击次数,那么直接触发多击功能
        // Trigger multi press immediately when reach max count.( > 1)
        if (mActiveRule.getMaxMultiPressCount() > 1
                && mKeyPressCounter == mActiveRule.getMaxMultiPressCount()) {
            if (DEBUG) {
                Log.i(TAG, "Trigger multi press " + mActiveRule.toString() + " for it"
                        + " reached the max count " + mKeyPressCounter);
            }
            final Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, keyCode,
                    mKeyPressCounter, mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessage(msg);
        }
    }
}

当按键再次抬起时,就需要触发双击,如下

// SingleKeyGestureDetector.java

private boolean interceptKeyUp(KeyEvent event) {
    // 重置 mDownKeyCode
    mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    
    // ...

    if (event.getKeyCode() == mActiveRule.mKeyCode) {
        // ...

        // This could be a multi-press.  Wait a little bit longer to confirm.
        if (mKeyPressCounter < mActiveRule.getMaxMultiPressCount()) {
            Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                    mKeyPressCounter, mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessageDelayed(msg, MULTI_PRESS_TIMEOUT);
        }
        return true;
    }
    
    // ...
}

如果按键按下的次数,没有达到规则的最大点击次数,那么需要发送一个延时消息,来确认是否执行多击。

这里假设在超时时间内,按键没有第三次按下,那么就会触发双击功能,其实就是触发规则的多击功能,如下

// SingleKeyGestureDetector.java

private class KeyHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        final SingleKeyRule rule = (SingleKeyRule) msg.obj;
        final int keyCode = msg.arg1;
        final int pressCount = msg.arg2;
        switch(msg.what) {
            // ...
            
            case MSG_KEY_DELAYED_PRESS:
                if (DEBUG) {
                    Log.i(TAG, "Detect press " + KeyEvent.keyCodeToString(keyCode)
                            + ", count " + pressCount);
                }
                if (pressCount == 1) {
                    // ...
                } else {
                    // 触发规则的多击功能
                    rule.onMultiPress(mLastDownTime, pressCount);
                }
                break;
        }
    }
}

长按手势

长按手势,根据底层是否支持按键长按功能,有两种不同实现。

如果底层支持按键长按功能,那么当按键首次按下,并且一定时间内不抬起时,底层会再次上报一次 key down event,InputDispatcher 会认为是一次长按事件,如下


bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
                                        DropReason* dropReason, nsecs_t* nextWakeupTime) {
    if (!entry->dispatchInProgress) {
        if (entry->repeatCount == 0 && entry->action == AKEY_EVENT_ACTION_DOWN &&
            (entry->policyFlags & POLICY_FLAG_TRUSTED) &&
            (!(entry->policyFlags & POLICY_FLAG_DISABLE_KEY_REPEAT))) {
            if (mKeyRepeatState.lastKeyEntry &&
                mKeyRepeatState.lastKeyEntry->keyCode == entry->keyCode &&
                mKeyRepeatState.lastKeyEntry->deviceId == entry->deviceId) {
                
                // 2. 如果再次收到 key down event,那么entry->repeatCount 记录按键重复按下的次数
                entry->repeatCount = mKeyRepeatState.lastKeyEntry->repeatCount + 1;
                resetKeyRepeatLocked();
                mKeyRepeatState.nextRepeatTime = LLONG_MAX; // don't generate repeats ourselves
            } else {
                // Not a repeat.  Save key down state in case we do see a repeat later.
                resetKeyRepeatLocked();
                mKeyRepeatState.nextRepeatTime = entry->eventTime + mConfig.keyRepeatTimeout;
            }
            
            // 1. 按键首次按下时,保存 KetEvent
            mKeyRepeatState.lastKeyEntry = entry;
        } else if (entry->action == AKEY_EVENT_ACTION_UP && mKeyRepeatState.lastKeyEntry &&
                   mKeyRepeatState.lastKeyEntry->deviceId != entry->deviceId) {
            // ...
        } else if (!entry->syntheticRepeat) {
            // ..
        }

        // 3. 只在 repeat count 为 1 时,在 KeyEvent::flgas 中添加 AKEY_EVENT_FLAG_LONG_PRESS
        if (entry->repeatCount == 1) {
            entry->flags |= AKEY_EVENT_FLAG_LONG_PRESS;
        } else {
            entry->flags &= ~AKEY_EVENT_FLAG_LONG_PRESS;
        }

        // ...
    }
    
    // ...
 }

当 SingleKeyGestureDetector 检测到带有 AKEY_EVENT_FLAG_LONG_PRESS 标志位的 key down event 时,就会触发长按手势功能,如下

// SingleKeyGestureDetector.java

private void interceptKeyDown(KeyEvent event) {
    final int keyCode = event.getKeyCode();
    // same key down.
    if (mDownKeyCode == keyCode) {
        if (mActiveRule != null && (event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0
                && mActiveRule.supportLongPress() && !mHandledByLongPress) {
            if (DEBUG) {
                Log.i(TAG, "Long press key " + KeyEvent.keyCodeToString(keyCode));
            }
            mHandledByLongPress = true;
            mHandler.removeMessages(MSG_KEY_LONG_PRESS);
            mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
            final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, keyCode, 0,
                    mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessage(msg);
        }
        return;
    }
    
    // ...
}

而如果底层不支持按键长按功能,当按键首次按下时,SingleKeyGestureDetector 会通过一个延时消息来确认是否触发长按,如下

// SingleKeyGestureDetector.java

private void interceptKeyDown(KeyEvent event) {
    final int keyCode = event.getKeyCode();
    // ...
    
    mDownKeyCode = keyCode;

    // 匹配一个规则 
    if (mActiveRule == null) {
        final int count = mRules.size();
        for (int index = 0; index < count; index++) {
            final SingleKeyRule rule = mRules.get(index);
            if (rule.shouldInterceptKey(keyCode)) {
                if (DEBUG) {
                    Log.i(TAG, "Intercept key by rule " + rule);
                }
                mActiveRule = rule;
                break;
            }
        }
        mLastDownTime = 0;
    }
    if (mActiveRule == null) {
        return;
    }

    final long keyDownInterval = event.getDownTime() - mLastDownTime;
    mLastDownTime = event.getDownTime();
    if (keyDownInterval >= MULTI_PRESS_TIMEOUT) {
        mKeyPressCounter = 1;
    } else {
        // ...
    }

    if (mKeyPressCounter == 1) {
        // 如果规则支持长按,发送一个延时消息,来确认是否触发长按
        if (mActiveRule.supportLongPress()) {
            final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, keyCode, 0,
                    mActiveRule);
            msg.setAsynchronous(true);
            mHandler.sendMessageDelayed(msg, mActiveRule.getLongPressTimeoutMs());
        }

        // ...
    } else {
        // ...
    }
}

如果在超时时间内,按键没有抬起,那么会执行消息,即触发规则的按键长按功能,如下

// SingleKeyGestureDetector.java

private class KeyHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
        final SingleKeyRule rule = (SingleKeyRule) msg.obj;
        if (rule == null) {
            Log.wtf(TAG, "No active rule.");
            return;
        }

        final int keyCode = msg.arg1;
        final int pressCount = msg.arg2;
        switch(msg.what) {
            case MSG_KEY_LONG_PRESS:
                if (DEBUG) {
                    Log.i(TAG, "Detect long press " + KeyEvent.keyCodeToString(keyCode));
                }
                rule.onLongPress(mLastDownTime);
                break;
            
            // ...
        }
    }
}

电源键手势

系统为电源键添加一个单按键手势规则,如下

// PhoneWindowManager.java

private void initSingleKeyGestureRules() {
    mSingleKeyGestureDetector = SingleKeyGestureDetector.get(mContext);
    mSingleKeyGestureDetector.addRule(new PowerKeyRule());
    
    // ...
}

看看它的单击功能是如何实现的

// PhoneWindowManager.java

private final class PowerKeyRule extends SingleKeyGestureDetector.SingleKeyRule {
    // ...
    
    @Override
    void onPress(long downTime) {
        powerPress(downTime, 1 /*count*/,
                mSingleKeyGestureDetector.beganFromNonInteractive());
    }

    // ...
}


private void powerPress(long eventTime, int count, boolean beganFromNonInteractive) {
    // ....

    // 这个变量命名错误,其实它代表屏幕是否处理亮屏状态
    final boolean interactive = mDefaultDisplayPolicy.isAwake();

    // 触发电源键单击功能的log
    // 注意,这个log在实际工作中有很大用处
    Slog.d(TAG, "powerPress: eventTime=" + eventTime + " interactive=" + interactive
            + " count=" + count + " beganFromNonInteractive=" + beganFromNonInteractive
            + " mShortPressOnPowerBehavior=" + mShortPressOnPowerBehavior);

    // 此时 count 为 1
    if (count == 2) {
        // ...
    } else if (count == 3) {
        // ...
    } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
        // ...
    } else if (count == 1 && interactive && !beganFromNonInteractive) { // 亮屏
        // ...
        
        // mShortPressOnPowerBehavior 是从配置中获取的
        switch (mShortPressOnPowerBehavior) {
            case SHORT_PRESS_POWER_NOTHING:
                break;
            case SHORT_PRESS_POWER_GO_TO_SLEEP:
                sleepDefaultDisplayFromPowerButton(eventTime, 0);
                break;
            case SHORT_PRESS_POWER_REALLY_GO_TO_SLEEP:
                sleepDefaultDisplayFromPowerButton(eventTime,
                        PowerManager.GO_TO_SLEEP_FLAG_NO_DOZE);
                break;
            
            // ...
        }
    }
}

很奇怪吧,电源键的规则中,对于单击功能,只实现灭屏的功能。

而电源键单击亮屏功能,是在策略 PhoneWindowManager 中实现的,如下

// PhoneWindowManager.java

public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
    // ...

    // Handle special keys.
    switch (keyCode) {
        // ...
        
        final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
        
        // ...
        
        final boolean isDefaultDisplayOn = mDefaultDisplayPolicy.isAwake();
        final boolean interactiveAndOn = interactive && isDefaultDisplayOn;
        
        // ...

        case KeyEvent.KEYCODE_POWER: {
            // 电源键 event log
            // 注意,在实际中有很大用处
            EventLogTags.writeInterceptPower(
                    KeyEvent.actionToString(event.getAction()),
                    mPowerKeyHandled ? 1 : 0,
                    mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER));

            // 电源键的 Key Event,不分发给窗口
            result &= ~ACTION_PASS_TO_USER;

            isWakeKey = false; // wake-up will be handled separately

            if (down) {
                interceptPowerKeyDown(event, interactiveAndOn);
            } else {
                // ...
            }
            break;
        }

        // ...
    }

    // ...

    return result;
}


private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
    // ...
    
    // Stop ringing or end call if configured to do so when power is pressed.
    TelecomManager telecomManager = getTelecommService();
    boolean hungUp = false;
    if (telecomManager != null) {
        if (telecomManager.isRinging()) { // 来电静音
            // Pressing Power while there's a ringing incoming
            // call should silence the ringer.
            telecomManager.silenceRinger();
        } else if ((mIncallPowerBehavior
                & Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP) != 0
                && telecomManager.isInCall() && interactive) { // 通话挂断
            // Otherwise, if "Power button ends call" is enabled,
            // the Power button will hang up any current active call.
            hungUp = telecomManager.endCall();
        }
    }

    // 跟 sensor 灭屏有关
    final boolean handledByPowerManager = mPowerManagerInternal.interceptPowerKeyDown(event);

    // Inform the StatusBar; but do not allow it to consume the event.
    sendSystemKeyToStatusBarAsync(event);

    // If the power key has still not yet been handled, then detect short
    // press, long press, or multi press and decide what to do.
    mPowerKeyHandled = mPowerKeyHandled || hungUp
            || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();
            
    if (!mPowerKeyHandled) { // 没有被用于其实用途
        if (!interactive) { // 系统处于非交互状态,例如灭屏
            // 亮屏
            wakeUpFromPowerKey(event.getDownTime());
        }
    } else {
        // ...
    }
}