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

1,589 阅读9分钟

按键事件,在加入到 InputDispatcher 的 inbound queue 之前,会先经过策略处理。这个策略可能会截断按键事件,还会实现单按键手势(单击,长按,多击)。本文主要分析单按键手势是如何实现的。

单按键手势初始化

// PhoneWindowManager.java

    private void initSingleKeyGestureRules() {
        mSingleKeyGestureDetector = SingleKeyGestureDetector.get(mContext);
        mSingleKeyGestureDetector.addRule(new PowerKeyRule());
        if (hasLongPressOnBackBehavior()) {
            mSingleKeyGestureDetector.addRule(new BackKeyRule());
        }
        if (hasStemPrimaryBehavior()) {
            mSingleKeyGestureDetector.addRule(new StemPrimaryKeyRule());
        }
    }

SingleKeyGestureDetector 管理单按键手势的规则,这里看下 power 键的规则

    /**
     * Rule for single power key gesture.
     */
    private final class PowerKeyRule extends SingleKeyGestureDetector.SingleKeyRule {
        PowerKeyRule() {
            super(KEYCODE_POWER);
        }

        @Override
        boolean supportLongPress() {
            return hasLongPressOnPowerBehavior();
        }

        @Override
        boolean supportVeryLongPress() {
            return hasVeryLongPressOnPowerBehavior();
        }


        @Override
        int getMaxMultiPressCount() {
            return getMaxMultiPressPowerCount();
        }

        @Override
        void onPress(long downTime) {
            powerPress(downTime, 1 /*count*/,
                    mSingleKeyGestureDetector.beganFromNonInteractive());
        }

        @Override
        long getLongPressTimeoutMs() {
            if (getResolvedLongPressOnPowerBehavior() == LONG_PRESS_POWER_ASSISTANT) {
                return mLongPressOnPowerAssistantTimeoutMs;
            } else {
                return super.getLongPressTimeoutMs();
            }
        }

        @Override
        void onLongPress(long eventTime) {
            if (mSingleKeyGestureDetector.beganFromNonInteractive()
                    && !mSupportLongPressPowerWhenNonInteractive) {
                Slog.v(TAG, "Not support long press power when device is not interactive.");
                return;
            }

            powerLongPress(eventTime);
        }

        @Override
        void onVeryLongPress(long eventTime) {
            mActivityManagerInternal.prepareForPossibleShutdown();
            powerVeryLongPress();
        }

        @Override
        void onMultiPress(long downTime, int count) {
            powerPress(downTime, count, mSingleKeyGestureDetector.beganFromNonInteractive());
        }
    }

很直观,单击就调用 onPress(),多击就调用 onMultiPress(),长按调用 onLongPress(),但是这些都是由前提条件的,具体哪些条件满足,需要读者根据自己项目去判断。

单手势功能框架

当按键在加入到 InputDispatcher 的 inbound queue 之前,会先询问策略

// PhoneWindowManager.java

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

        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)) {
            // 组合键 ...
        }

        if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) {
            // 双击 power 打开 camera ...
        }

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

    void interceptKey(KeyEvent event, boolean interactive) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) {
                mBeganFromNonInteractive = !interactive;
            }
            interceptKeyDown(event);
        } else {
            interceptKeyUp(event);
        }
    }

SingleKeyGestureDetector 对 key down 和 key up 分别进行处理,从而实现单按键手势。

单击手势

来看下按键单击手势如何实现,首先看 SingleKeyGestureDetector 处理 key down

// SingleKeyGestureDetector.java

    private void interceptKeyDown(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        // same key down.
        if (mDownKeyCode == keyCode) {
           // ...
            return;
        }

        // When a different key is pressed, stop processing gestures for the currently active key.
        if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN
                || (mActiveRule != null && !mActiveRule.shouldInterceptKey(keyCode))) {
            // ...
        }

        // 1. 保存 down key code
        mDownKeyCode = keyCode;

        // Picks a new rule, return if no rule picked.
        // 2. 找到一个 rule,保存到 mActiveRule
        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;
        }

        // 没有找到 rule ,直接退出
        if (mActiveRule == null) {
            return;
        }

        final long keyDownInterval = event.getDownTime() - mLastDownTime;
        mLastDownTime = event.getDownTime();
        if (keyDownInterval >= MULTI_PRESS_TIMEOUT) {
            // 记录按键按下的次数
            mKeyPressCounter = 1;
        } else {
            mKeyPressCounter++;
        }

        if (mKeyPressCounter == 1) {
            // 实现长按的逻辑
            if (mActiveRule.supportLongPress()) {
                
            }

            if (mActiveRule.supportVeryLongPress()) {
                
            }
        } else {
            // ...
        }
    }

当按键按下时,SingleKeyGestureDetector 使用 mDownKeyCode 保存按键按下的 key code,然后使用 mActiveRule 保存按键的规则,规则的寻找就是根据 key code 进行匹配。

然后看下 SingleKeyGestureDetector 处理 key up

// SingleKeyGestureDetector.java

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

        if (mActiveRule == null) {
            return false;
        }

        // 没有触发长按,移除长按消息
        if (!mHandledByLongPress) {
            
        }

        // 已经触发长按
        if (mHandledByLongPress) {
            
        }

        if (event.getKeyCode() == mActiveRule.mKeyCode) {
            // 最大点击数是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;
            }

            // 可能触发多点点击事件,因为发送一个延时消息等待
            // 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;
        }
        reset();
        return false;
    }


    private class KeyHandler extends Handler {
        KeyHandler() {
            super(Looper.myLooper());
        }

        @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_DELAYED_PRESS:
                    if (DEBUG) {
                        Log.i(TAG, "Detect press " + KeyEvent.keyCodeToString(keyCode)
                                + ", count " + pressCount);
                    }
                    if (pressCount == 1) {
                        // 就是执行 SingleKeyRule#onPress()
                        rule.onPress(mLastDownTime);
                    } else {
                        // ...
                    }
                    break;
            }
        }
    }    

如果规则支持的最大点击数是1,那么当 SingleKeyGestureDetector 检测到按键的 key up 时,就会立即执行规则的单击行为。

而如果规则支持的最大点击数大于1,那么需要发送一个延时消息,这个延时消息是为了检测用户是否多次点击按键,如果超时了,还没有检测到按键再次按下,那么就会触发规则的单击行为。

多击手势

根据单击的分析,在第一次按键抬起的时候,会发送一个延时消息,如果在超时时间内,按键第二次按下,看下处理 key down 流程

    private void interceptKeyDown(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        // same key down.
        if (mDownKeyCode == keyCode) {
            
        }

        // When a different key is pressed, stop processing gestures for the currently active key.
        if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN
                || (mActiveRule != null && !mActiveRule.shouldInterceptKey(keyCode))) {
            
        }

        mDownKeyCode = keyCode;

        // Picks a new rule, return if no rule picked.
        if (mActiveRule == null) {
            
        }

        if (mActiveRule == null) {
            return;
        }

        final long keyDownInterval = event.getDownTime() - mLastDownTime;
        mLastDownTime = event.getDownTime();
        if (keyDownInterval >= MULTI_PRESS_TIMEOUT) {
            
        } else {
            // 按键按下次数 + 1
            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()) {
                
            }
        }
    }

当第二次按下按键时,会移除单击手势的延时消息,这样就无法触发单击手势功能。我们假设此时按键按下的次数,并没有达到规则的最大点击次数,因此接着看第二次按键抬起的处理

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

        if (!mHandledByLongPress) {
            
        }

        if (mHandledByLongPress) {
            
        }

        if (event.getKeyCode() == mActiveRule.mKeyCode) {
            // Directly trigger short press when max count is 1.
            if (mActiveRule.getMaxMultiPressCount() == 1) {
                
            }

            // This could be a multi-press.  Wait a little bit longer to confirm.
            if (mKeyPressCounter < mActiveRule.getMaxMultiPressCount()) {
                // mKeyPressCounter 表示按键按下的次数
                Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                        mKeyPressCounter, mActiveRule);
                msg.setAsynchronous(true);
                mHandler.sendMessageDelayed(msg, MULTI_PRESS_TIMEOUT);
            }
            return true;
        }
        reset();
        return false;
    }

    private class KeyHandler extends Handler {
        KeyHandler() {
            super(Looper.myLooper());
        }

        @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_DELAYED_PRESS:
                    if (DEBUG) {
                        Log.i(TAG, "Detect press " + KeyEvent.keyCodeToString(keyCode)
                                + ", count " + pressCount);
                    }
                    if (pressCount == 1) {
                        
                    } else {
                        // 触发多点击
                        rule.onMultiPress(mLastDownTime, pressCount);
                    }
                    break;
            }
        }
    }    

在没有达到规则的最大点击次数之前,仍然需要发送一个超时消息。如果在超时时间内,用户没有第三次点击按键,那么当发生超时时,就会触发双击手势功能。

而如果在超时时间内,用户第三次按下,那么继续执行前面流程,直到检测到按键按下次数等于规则的最大点击次数,就会立即执行规则的多击功能,如下

// PhoneWindowManager.java

    private void interceptKeyDown(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        // same key down.
        if (mDownKeyCode == keyCode) {
            
        }

        // When a different key is pressed, stop processing gestures for the currently active key.
        if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN
                || (mActiveRule != null && !mActiveRule.shouldInterceptKey(keyCode))) {
            
        }

        mDownKeyCode = keyCode;

        // Picks a new rule, return if no rule picked.
        if (mActiveRule == null) {
            
        }

        if (mActiveRule == null) {
            return;
        }

        final long keyDownInterval = event.getDownTime() - mLastDownTime;
        mLastDownTime = event.getDownTime();
        if (keyDownInterval >= MULTI_PRESS_TIMEOUT) {
            
        } else {
            // 按键按下次数 + 1
            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);
            }
        }
    }

长按手势

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

        if (mDownKeyCode == keyCode) {

        }

        if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN
                || (mActiveRule != null && !mActiveRule.shouldInterceptKey(keyCode))) {
            
        }

        // 保存 down key code
        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
            mKeyPressCounter = 1;
        } else {
            
        }

        if (mKeyPressCounter == 1) {
            // 按键首次按下,发送一个延时消息,如果在超时时间内,没有检测到 key up,就认为是长按
            if (mActiveRule.supportLongPress()) {
                final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, keyCode, 0,
                        mActiveRule);
                msg.setAsynchronous(true);
                mHandler.sendMessageDelayed(msg, mActiveRule.getLongPressTimeoutMs());
            }

            if (mActiveRule.supportVeryLongPress()) {

            }
        } else {
            
        }
    }

    private class KeyHandler extends Handler {
        KeyHandler() {
            super(Looper.myLooper());
        }

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


                // ...
            }
        }
    }    

当按键首次按下时,会发送一个超时的消息,如果在超时时间内没有收到 key up 事件,那么就会触发规则的长按。

而如果在超时时间内,收到 key up 事件,那么会取消长按,如下

// PhoneWindowManager.java

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

        if (!mHandledByLongPress) {
            final long eventTime = event.getEventTime();
            // 长按超时时间内,检测到 key up,移除长按超时消息,这样就无法触发长按手势功能
            if (eventTime < mLastDownTime + mActiveRule.getLongPressTimeoutMs()) {
                mHandler.removeMessages(MSG_KEY_LONG_PRESS);
            } else {
                
            }

            if (eventTime < mLastDownTime + mActiveRule.getVeryLongPressTimeoutMs()) {
                mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
            } else {
                
            }
        }
    }

其实还有一种实现长按的方式,如下

// SingleKeyGestureDetector.java

    private void interceptKeyDown(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        // same key down.
        if (mDownKeyCode == keyCode) { // 在超时时间内,没有收到 key up ,而是又收到同一个按键的 key down

            // key down 事件还携带了一个长按的标志位,那么认为这个事件,就是长按事件
            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;
        }

根据前面文章 InputReader 处理按键事件的分析,这个标志位应该是在 key layout file 把 scan code 转换为 key code 的过程,但是具体什么情况下设置长按标志位,我这里就不深入探讨了。

power 键的亮灭屏

power 键灭屏是在规则中实现的,如下

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

    // 参数 count 为 1
    // beganFromNonInteractive: 如果使用 power 键亮屏,值为 true,如果使用 power 灭屏,值为 false
    private void powerPress(long eventTime, int count, boolean beganFromNonInteractive) {
        // ...

        // 处于亮屏流程中
        // mDefaultDisplayPolicy.isScreenOnEarly() 表示刚开始亮屏
        // mDefaultDisplayPolicy.isScreenOnFully() 表示完全亮屏
        if (mDefaultDisplayPolicy.isScreenOnEarly() && !mDefaultDisplayPolicy.isScreenOnFully()) {
            Slog.i(TAG, "Suppressed redundant power key press while "
                    + "already in the process of turning the screen on.");
            return;
        }

        final boolean interactive = mDefaultDisplayPolicy.isAwake();

        // 注意,这个log可以代表 power 灭屏时间点
        Slog.d(TAG, "powerPress: eventTime=" + eventTime + " interactive=" + interactive
                + " count=" + count + " beganFromNonInteractive=" + beganFromNonInteractive
                + " mShortPressOnPowerBehavior=" + mShortPressOnPowerBehavior);

        if (count == 2) {
            
        } else if (count == 3) {
            
        } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
            
        } 
        // 从条件看,这里只实现了在亮屏情况下,单击 power 规则
        else if (count == 1 && interactive && !beganFromNonInteractive) {

            switch (mShortPressOnPowerBehavior) {
                // ...

                case SHORT_PRESS_POWER_GO_TO_SLEEP:
                    sleepDefaultDisplayFromPowerButton(eventTime, 0);
                    break;
                // ...
            }
        }
    }    

但是power亮屏并不是在规则中实现的,而是在策略中实现,如下

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

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

        // ...

        switch (keyCode) {
            // ...

            case KeyEvent.KEYCODE_POWER: {
                // 注意,这个 event log 可以作为 power 键亮屏时间点
                EventLogTags.writeInterceptPower(
                        KeyEvent.actionToString(event.getAction()),
                        mPowerKeyHandled ? 1 : 0,
                        mSingleKeyGestureDetector.getKeyPressCounter(KeyEvent.KEYCODE_POWER));
                
                // power 按键事件是不会发送给窗口的
                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) {
        // ....

        TelecomManager telecomManager = getTelecommService();
        boolean hungUp = false;
        if (telecomManager != null) {
            if (telecomManager.isRinging()) { 
                // 来电静音
                telecomManager.silenceRinger();
            } else if ((mIncallPowerBehavior
                    & Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP) != 0
                    && telecomManager.isInCall() && interactive) {
                // 通话挂断
                hungUp = telecomManager.endCall();
            }
        }

        // 如果距离传感器让显示屏出于灭屏状态,那么当按power键时,会通知 DMS 忽略距离传感器,这样屏幕就会亮屏
        final boolean handledByPowerManager = mPowerManagerInternal.interceptPowerKeyDown(event);

        sendSystemKeyToStatusBarAsync(event);

        // 这里检测 power 键是否已经被处理
        mPowerKeyHandled = mPowerKeyHandled || hungUp
                || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();

        if (!mPowerKeyHandled) { // power 键没有被处理
            if (!interactive) { // 处于非交互状态(一般是灭屏)
                // 亮屏
                wakeUpFromPowerKey(event.getDownTime());
            }
        } else {
            
        }
    }    

当 power 键按下时,如果 power 键没有被用于其他功能,那么灭屏状态下,会立即亮屏。