按键事件,在加入到 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 键没有被用于其他功能,那么灭屏状态下,会立即亮屏。