25、遥控器按键:KeyEvent处理、自定义按键映射与长按事件
遥控器按键处理,说白了就是TV应用和用户交互的「最后一公里」。你想想看,手机应用靠触摸,TV应用全靠遥控器那十几个键。按键响应做不好,用户第一反应就是「这App卡死了」——其实只是你的KeyEvent没处理好。
我个人习惯把按键处理分成三个层次:基础拦截、按键映射、长按扩展。今天咱们一层层拆开讲。
25.1 KeyEvent 基础:从按键到响应
Android里每个遥控器按键都会生成一个KeyEvent对象。系统会把这个事件分发给当前焦点的View。你可以在Activity或View里重写onKeyDown()和onKeyUp()来拦截。
先看一个最基础的例子:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_UP:
// 处理上键
return true;
case KeyEvent.KEYCODE_DPAD_DOWN:
// 处理下键
return true;
case KeyEvent.KEYCODE_DPAD_CENTER:
// 处理确认键
return true;
default:
return super.onKeyDown(keyCode, event);
}
}
这里有个坑——返回值。返回true表示你消费了这个事件,系统不再往下传。返回false则事件会继续传递给父容器。我曾经在项目里忘了返回true,结果按键被上层ViewGroup又处理了一遍,导致焦点乱跳。嗯,调试了整整一个下午。
核心原则:消费了按键事件,一定要返回true。否则系统会认为你没处理,继续寻找下一个消费者。
25.2 按键映射:让遥控器「听懂」你的逻辑
不同品牌的遥控器,按键码可能不一样。比如某些国产盒子把「菜单键」映射成了KEYCODE_MENU,而另一些则用KEYCODE_BUTTON_MODE。这时候就需要做按键映射。
我建议的做法是建一个按键映射表:
public class KeyMapper {
private static final Map<Integer, Integer> KEY_MAP = new HashMap<>();
static {
// 统一映射到标准按键
KEY_MAP.put(KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_DPAD_CENTER);
KEY_MAP.put(KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_DPAD_CENTER);
KEY_MAP.put(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_DPAD_CENTER);
KEY_MAP.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_DPAD_CENTER);
}
public static int mapKey(int originalKeyCode) {
return KEY_MAP.getOrDefault(originalKeyCode, originalKeyCode);
}
}
然后在onKeyDown里先做映射:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
int mappedKey = KeyMapper.mapKey(keyCode);
// 后续逻辑都用 mappedKey
switch (mappedKey) {
case KeyEvent.KEYCODE_DPAD_CENTER:
// 统一处理确认
return true;
}
return super.onKeyDown(keyCode, event);
}
小技巧:映射表最好做成可配置的,比如从SharedPreferences或远程配置读取。这样线上出问题时,可以热修复按键映射,不用发版。
25.3 长按事件:别让用户「按到手指酸」
TV遥控器没有触摸长按的概念,但我们可以用KeyEvent的重复机制来实现。系统在按键按住不放时,会连续发送onKeyDown事件,并且event.getRepeatCount()会递增。
我一般这样处理长按:
private static final long LONG_PRESS_THRESHOLD = 500; // 毫秒
private long mDownTime = 0;
private boolean mIsLongPressed = false;
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getRepeatCount() == 0) {
// 第一次按下
mDownTime = System.currentTimeMillis();
mIsLongPressed = false;
} else {
// 重复事件,检查是否达到长按阈值
long elapsed = System.currentTimeMillis() - mDownTime;
if (elapsed >= LONG_PRESS_THRESHOLD && !mIsLongPressed) {
mIsLongPressed = true;
onLongPress(keyCode);
}
}
}
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (!mIsLongPressed) {
// 短按
onShortPress(keyCode);
}
mIsLongPressed = false;
return true;
}
private void onShortPress(int keyCode) {
// 处理短按逻辑
}
private void onLongPress(int keyCode) {
// 处理长按逻辑,比如快进、音量连续调节
}
注意:长按事件只在onKeyDown的重复事件里触发一次。如果你在onKeyDown里每次都执行长按逻辑,那用户按一次就会触发几十次——我见过有人这么写,结果用户反馈「按一下音量键,声音直接飙到最大」。
25.4 实战:一个完整的按键处理框架
下面是我在项目中常用的按键处理框架,把基础拦截、映射、长按整合在一起:
public abstract class BaseKeyHandler {
private static final long LONG_PRESS_THRESHOLD = 500;
private long mDownTime;
private boolean mIsLongPressed;
private int mLastKeyCode;
public boolean handleKeyEvent(KeyEvent event) {
int keyCode = KeyMapper.mapKey(event.getKeyCode());
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getRepeatCount() == 0) {
mDownTime = System.currentTimeMillis();
mIsLongPressed = false;
mLastKeyCode = keyCode;
onKeyDown(keyCode);
} else {
long elapsed = System.currentTimeMillis() - mDownTime;
if (elapsed >= LONG_PRESS_THRESHOLD && !mIsLongPressed) {
mIsLongPressed = true;
onLongPress(keyCode);
}
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
if (!mIsLongPressed) {
onKeyUp(keyCode);
}
mIsLongPressed = false;
return true;
}
return false;
}
protected abstract void onKeyDown(int keyCode);
protected abstract void onKeyUp(int keyCode);
protected abstract void onLongPress(int keyCode);
}
使用时继承这个基类,只需要实现三个抽象方法:
public class MyKeyHandler extends BaseKeyHandler {
@Override
protected void onKeyDown(int keyCode) {
// 按键按下时的反馈,比如改变焦点View的选中状态
}
@Override
protected void onKeyUp(int keyCode) {
// 短按释放,执行主要逻辑
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
// 确认操作
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
// 左移
break;
}
}
@Override
protected void onLongPress(int keyCode) {
// 长按操作,比如快速翻页
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
// 快进
}
}
}
我的习惯:把按键处理抽成独立的Handler类,而不是写在Activity或Fragment里。这样单元测试好写,也方便复用。我曾经在一个项目里,三个页面都需要同样的按键逻辑,直接复用同一个Handler,省了不少代码。
25.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
25.6 避坑指南
最后分享几个我踩过的坑:
- 重复事件别乱用:
getRepeatCount()只在按键按住不放时递增。如果你在onKeyDown里每次都执行逻辑,用户按一下就会触发多次。我建议只在onKeyUp里处理主要逻辑,onKeyDown只做状态记录。 - 别忘了处理
ACTION_UP:有些开发者只重写onKeyDown,忽略了onKeyUp。这会导致一个问题——用户按一下键,你的逻辑执行了,但松开时没有重置状态。下次按键就会出bug。 - 映射表要覆盖全:不同遥控器的按键码差异很大。我建议在测试阶段收集所有主流遥控器的按键码,把映射表做全。漏掉一个,用户就多一个「这个键按了没反应」的抱怨。
- 长按阈值别太短:500毫秒是我常用的值。太短容易误触,太长用户觉得「按了没反应」。你可以做成可配置的,让用户自己调。
好了,按键处理这块就讲到这里。记住一个核心:让遥控器的每个按键都「有反馈、有响应、不冲突」。做到这三点,你的TV应用交互体验就及格了。