TV中的遥控器事件
KeyEvent.KEYCODE_DPAD_UP:遥控器上键
KeyEvent.KEYCODE_DPAD_DOWN:遥控器下键
keyEvent.KEYCODE_DPAD_LEFT:遥控器左键
keyEvent.KEYCODE_DPAD_RIGHT:遥控器右键
keyEvent.KEYCODE_DPAD_CENTER:遥控器ok确认键
keyEvent.KEYCODE_DPAD_x:x表示数字0-9,遥控器数字键
KeyEvent.KEYCODE_BACK:遥控器返回键
KeyEvent.KEYCODE_HOME:遥控器主页键
KeyEvent.KEYCODE_x:x表示字母A-Z,遥控器字母键
KeyEvent.KEYCODE_MENU:遥控器菜单键
使用adb命令模拟遥控器按键
adb shell
input keyevent 1 // 遥控器menu键
input keyevent 3 // 遥控器Home键
input keyevent 4 // 遥控器Back键
input keyevent 19 // 遥控器上键
input keyevent 20 // 遥控器下键
input keyevent 21 // 遥控器左键
input keyevent 22 // 遥控器右键
input keyevent 23 // 遥控器ok确认键
焦点拦截事件分发
手机端拦截事件分发:
-
dispatchTouchEvent()
-
onInterceptTouchEvent()
-
onTouchEvent()
TV端拦截事件分发:
-
dispatchKeyEvent()
-
onKeyDown()/onKeyUp()
-
onKeyListener()
事件传递说明
KeyEvent的事件处理只有两个地方,一个是Activity,一个是具体的View,ViewGroup只负责分发事件给子View不消耗事件。
其实Activity和具体View的事件处理都是交由DecorView处理。
(PhoneWindow)$DecorView.dispatchKeyEvent()->
Activity.dispatchKeyEvent()->
View.dispatchKeyEvent()->
// setonKeyListener != null则回调监听器
View.setOnKeyListener.onKey()->
// setOnKeyListener == null或onKey()返回false则回调onKeyDown()/onKeyUp()
View.onKeyDown()/onKeyUp()->
// setOnClickListener != null && 遥控器ok按键的ACTION_UP则回调监听器
View.setOnClickListener.onClick()->
// View.setOnClickListener == null则返回到Activity回调onKeyDown()/onKeyUp()
Activity.OnKeyDown()/OnKeyUp()
常见的焦点处理方式
dispatchKeyEvent()
遥控器按下和松开会分别调用两次dispatchKeyEvent():
dispatchKeyEvent->onKeyDown->dispatchKeyEvent->onKeyUp
返回true或false都会拦截事件,即子View将不会接收到分发事件,Activity的onKeyDown()/onKeyUp()也不会接收到
return true:拦截事件,不让焦点再移动
return false:拦截事件,焦点会移动
return super.dispatchKeyEvent:往下分发事件不拦截
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
int action = event.getAction();
switch(keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
Log.i(TAG, "dispatchKeyEvent-ACTION_DOWN");
} else if (action == KeyEvent.ACTION_UP) {
Log.i(TAG, "dispatchKeyEvent-ACTION_UP");
}
return true;
case KeyEvent.KEYCODE_DPAD_UP:
if (action == KeyEvent.ACTION_DOWN) {
Log.i(TAG, "dispatchKeyEvent-ACTION_DOWN");
} else if (action == KeyEvent.ACTION_UP) {
Log.i(TAG, "dispatchKeyEvent-ACTION_UP");
}
return false;
}
return super.dispatchKeyEvent(event);
}
onKeyDown()/onKeyUp()
事件走到这两个回调表示子View没有消耗事件抛回给上层,在这里做最后的事件处理。
onKeyDown()
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 如果处于booking录制状态,拦截提示退出
// 将事件抛到onKeyUp处理弹出对话框
if (isInterceptEventWhenRecord(event)) {
return super.onKeyDown(keyCode, event);
}
if (keyCode == KeyEvent.KEYCODE_0) {
recordJumpPlayProgNum(0);
return true;
}
if (keyCode == KeyEvent.KEYCODE_1) {
recordJumpPlayProgNum(1);
return true;
}
if (keyCode == KeyEvent.KEYCODE_2) {
recordJumpPlayProgNum(2);
return true;
}
....
return super.onKeyDown(keyCode, event);
}
onKeyUp()
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// EPG
if (keyCode == KeyEvent.KEYCODE_F1) {
gotoEpg();
return true;
}
// TEXT
if (keyCode == KeyEvent.KEYCODE_TV_TELETEXT) {
showTeletextDialog();
return true;
}
// RECORD
if (keyCode == KeyEvent.KEYCODE_MEDIA_RECORD) {
if (isUsbNotExit()) {
ToastUtils.showToast(R.string.toast_no_storage_device);
return true;
}
if (DTVBookingManager.getInstance().isRecording()) {
showQuitRecordDialog(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_UNKNOWN));
} else {
if (mProgListAdapter.getCount() > 0) {
int recordMinutes = DTVSettingManager.getInstance().getDTVProperty(HSetting_Enum_Property.RecordMaxMin);
if (recordMinutes == 0) {
showInputPvrMinuteDialog();
} else {
mRecordSeconds = recordMinutes >= Integer.MAX_VALUE ? Integer.MAX_VALUE : recordMinutes * 60;
recordProg();
}
}
}
return true;
}
// TV/RADIO
if (keyCode == KeyEvent.KEYCODE_TV_RADIO_SERVICE) {
stopRecord();
int currProgType = DTVProgramManager.getInstance().getCurrProgType();
int group = currProgType == HProg_Enum_Type.GBPROG ? HProg_Enum_Type.TVPROG : HProg_Enum_Type.GBPROG;
int num = DTVProgramManager.getInstance().getProgNumOfType(group, 0);
if (num > 0) {
DTVProgramManager.getInstance().setCurrProgType(currProgType == HProg_Enum_Type.GBPROG ?
HProg_Enum_Type.TVPROG : HProg_Enum_Type.GBPROG, 0);
onProgramUpdate(new ProgramUpdateEvent(true));
} else {
ToastUtils.showToast(group == HProg_Enum_Type.GBPROG ? R.string.toast_no_radio : R.string.toast_no_tv);
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_MENU) {
dismissPfBarScanDialog();
if (!mMenuShow) {
if (mProgListShow) toggleProgList(false); // 隐藏正在显示的频道列表
mItemInstallation.requestFocus();
toggleMenu();
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mMenuShow && (mIvChannelManageBack.getVisibility() == View.VISIBLE)) {
//在channelManage界面按BACK返回menu菜单
restoreMenuItem();
mItemChannelManage.requestFocus();
} else if (mMenuShow && (mIvDTVSettingBack.getVisibility() == View.VISIBLE)) {
//在dtv setting界面按BACK返回menu菜单
restoreMenuItem();
mItemDtvSetting.requestFocus();
} else if (mProgListShow) {
toggleProgList();
} else if (isPfBarShowing()) {
mTvProgNum.setVisibility(View.INVISIBLE);
dismissPfBarScanDialog();
} else if (mMenuShow) {
toggleMenu();
} else {
showExitDialog();
}
return true;
}
return super.onKeyUp(keyCode, event);
}
setOnKeyListener()
一般实现onKeyListener对具体的遥控器事件做特殊处理
mProgListView.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_PROG_YELLOW && event.getAction() == KeyEvent.ACTION_UP) {
showFindProgChannelDialog();
return true;
}
return false;
});
OnClickListener()
一般实现onClickListener都是响应遥控器的ok确认键
new ScanDialog()
.setOnScanSearchListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(TpListingActivity.this, ScanTVandRadioActivity.class);
intent.putExtra(Constants.IntentKey.INTENT_FREQ, getFreq());
intent.putExtra(Constants.IntentKey.INTENT_SATELLITE_INDEX, getIndex());
intent.putExtra(Constants.IntentKey.INTENT_SYMBOL, getSymbol());
intent.putExtra(Constants.IntentKey.INTENT_QAM, getQam());
intent.putExtra(Constants.IntentKey.INTENT_SEARCH_TYPE, Constants.IntentValue.SEARCH_TYPE_TPLISTING);
startActivity(intent);
}
}).show(getSupportFragmentManager(), ScanDialog.TAG);
View的焦点处理
requestFocus():强制获取焦点
android:focusable:设置一个控件是否能获取焦点
android:nextFocusUp/view.setNextFocusUpId(id):设置遥控器按上键时哪个view会获取到焦点,提供一个id
android:nextFocusDown/view.setNextFocusDownId(id):设置遥控器按下键时哪个view会获取到焦点,提供一个id
android:nextFocusLeft/view.setNextFocusLeftId(id):设置遥控器按左键时哪个view会获取到焦点,提供一个id
android:nextFocusRight/view.setNextFocusRightId(id):设置遥控器按右键时哪个view会获取到焦点,提供一个id
view.setOnFocusChangeListener():监听view获取到焦点,一般在该方法处理view背景修改等
遥控器长按事件的监听
isLongPress只是在apk层面表示是否处于长按事件,但并不会正真拦截事件。
只有return true才可以在系统层面实现正真的拦截下发事件。注意事件下发的顺序是dispatchKeyEvent(event.getRepeatCount==0&ACTION_DOWN)->onKeyDown
->dispatchKeyEvent(event.getRepeatCount>0&ACTION_DOWN)->onKeyDown
->dispatchKeyEvent(ACTION_UP)->onKeyUp
如果在if (event.getRepeatCount() == 0) {}在中return true,则事件不会下发至onKeyDown()中,则对应按下单击皆无响应。
如果在event.getRepeatCount()>0条件中return true,则按下点击多次时只会响应dispatchKeyEvent()中的代码而非onKeyDown()中的代码。
如果在if (event.getRepeatCount() == 0) {}中return true,则按下松开之后只会响应dispatchKeyEvent()中的代码而非onKeyUp()中的代码。
private boolean isLongPress;
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
isLongPress = true;
return true;
}
return super.onKeyLongPress(keyCode, event);
}
// dispatchKeyEvent统一处理长按时按下和松开时的处理
// 或者分别在onKeyDown监听长按按下,onKeyUp监听长按松开
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
int action = event.getAction();
switch(keyCode) {
case KeyEvent.KEYCODE_DPAD_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
// 按下时会执行一次,进行event长按跟踪,不断回调dispatchKeyEvent,第二次开始会走else
if (event.getRepeatCount() == 0) {
event.startTracking();
isLongPress = false;
} else {
isLongPress = true;
// 遥控器向下长按按下处理
.....
}
} else if (action == KeyEvent.ACTION_UP) {
if (isLongPress) {
isLongPress = false;
// 遥控器向下长按松开处理
....
}
}
return true;
}
return super.dispatckKeyEvent(event);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
// 按下时会执行一次,进行event长按跟踪
if (event.getRepeatCount() == 0) {
isLongPress = false;
event.startTracking();
}
return true;
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
if (isLongPress) {
isLongPress = false;
// 遥控器向下长按松开处理
....
}
return true;
}
return super.onKeyUp(keyCode, event);
}
如以下代码实现长按频道+键,对此键进行监听。在监听时获取频道信息,在松开时实现切台并播放
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@SuppressLint("RestrictedApi")
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP || event.getKeyCode() == KeyEvent.KEYCODE_CHANNEL_UP) {
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
if (event.getRepeatCount() == 0) {
mLongPressed = false;
event.startTracking();
mNewProgNum = getCurrentProgNo() + 1;
mLongPressDelayTime = System.currentTimeMillis();
} else {
mLongPressed = true;
if (System.currentTimeMillis() - mLongPressDelayTime > 200) {
removePlayProgMsg();
mLongPressDelayTime = System.currentTimeMillis();
if (++mNewProgNum > getLastProgNo()) mNewProgNum = getFirstProgNo();
int position = getPositionByProgNum(mNewProgNum);
if (position >= 0) {
showProgNum(mProgListAdapter.getItem(position).PShowNo, !mLongPressed);
}
}
}
break;
case KeyEvent.ACTION_UP:
if (mProgListShow) {
return super.dispatchKeyEvent(event);
} else {
if (mNewProgNum != DTVProgramManager.getInstance().getCurrProgNo()) {
if (mLongPressed) {
mLongPressed = false;
int position = getPositionByProgNum(mNewProgNum);
if (position >= 0) {
playProg(mProgListAdapter.getItem(position).ProgNo);
}
} else {
nextProg();
}
}
}
mNewProgNum = 0;
mLongPressDelayTime = 0;
break;
}
return true;
}
return super.dispatchKeyEvent(event);
}
焦点丢失问题
android:descendantFocusability=“afterDescendants” 在组合控件(即自定义ViewGroup inflate有多个控件的xml布局)焦点处理中,在使用ListView、GridView、RecyclerView时有时会出现焦点丢失问题,需要添加该属性
// ViewGroup会优先其子类控件而获取焦点
android:descendantsFocusability="beforeDescendants"
// ViewGroup只有当其子类控件不需要获取焦点时才获取焦点
android:descendantsFocusability="afterDescendants"
// ViewGroup会覆盖子类控件而直接获得焦点
android:descandantsFocusability="blockDescendants"
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager manager = new LinearLayoutManager(this);
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter fruitAdapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(fruitAdapter);