Android TV Compose+Media3 ExoPlayer:Talkback开启时DPAD事件异常
Android TV Jetpack Compose + Media3 ExoPlayer:Talkback开启后DPAD事件失效问题解决
问题现象
在Android TV的Jetpack Compose流媒体应用中使用Media3 ExoPlayer,正常模式(Talkback关闭)下所有DPAD事件可正常捕获,但开启Talkback后仅能捕获返回/Enter/Center键,其他DPAD事件无法处理,导致播放器控制器无法显示、播放/暂停/快进等功能失效。Fire TV设备上无此问题。
相关代码
CustomPlayerView实现
@Composable private fun CustomPlayerView( modifier: Modifier = Modifier, areControlsVisible: Boolean, isSubtitlesVisible: Boolean, isInTv: Boolean = false, isInPipMode: Boolean = false, resizeMode: Int = AspectRatioFrameLayout.RESIZE_MODE_FIT, getPlayerView: (view: View) -> Unit ) { val playerManager by rememberLocalPlayer() AndroidViewBinding( modifier = modifier, factory = { inflater, viewGroup, attachToParent -> val binding = CustomPlayerBinding.inflate(inflater, viewGroup, attachToParent) binding.apply { root.isClickable = false root.isFocusable = false playerView.run { this@run.resizeMode = resizeMode this@run.player = playerManager.player // Show the controls forever controllerShowTimeoutMs = Int.MAX_VALUE showController() } } return@AndroidViewBinding binding } ) }
播放器页面调用代码
CustomPlayerView( areControlsVisible = controlsUIState.controlUIState is ControlUISate.Show, isInPipMode = false, playbackState = playbackState, onPlayerAction = onPlayerControlsAction, onInitialize = { // Initialise player }, onRelease = { isForceReleasing -> // Handle release }, modifier = Modifier .focusOnInitialVisibility() .focusable() .handleDPadKeyEvents( isSeeking = trickPlayUIState.isSeeking, onEnter = { _, _ -> handlePlayBack() }, onUp = { _, _ -> //Handle Dpad UP }, onLeft = { isLongPress, isReleased -> //Handle Dpad LEFT }, onRight = { isLongPress, isReleased -> //Handle Dpad RIGHT }, onMediaKeyFf = { isLongPress, isReleased -> //Handle Dpad FF }, onMediaKeyRw = { isLongPress, isReleased -> //Handle Dpad RW }, onKeyDown = { _, _ -> //Handle Dpad DOWN }, onMediaKey = { //Handle Dpad Media Key generic }, onMediaKeyPlayPause = { _, _ -> handlePlayBack() }, shouldConsumeRightEvent = true, shouldConsumeLefEvent = true, shouldConsumeEventPlayPauseEvent = true ), )
DPAD事件处理扩展函数
/** * Handles horizontal (Left & Right) D-Pad Keys and consumes the event(s) so that the focus doesn't * accidentally move to another element. * */ @Composable fun Modifier.handleDPadKeyEvents( onLeft: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onRight: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onUp: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onDown: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onEnter: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onKeyDown: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onBack: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onMediaKey: ((keyCode: Int) -> Unit)? = null, // Callback for media keys onMediaKeyFf: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onMediaKeyRw: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, onMediaKeyPlayPause: ((isLongPress: Boolean, isReleased: Boolean) -> Unit)? = null, shouldConsumeEvent: Boolean = false, shouldConsumeLefEvent: Boolean = false, shouldConsumeRightEvent: Boolean = false, shouldConsumeEventPlayPauseEvent: Boolean = false, isSeeking: Boolean = false ): Modifier { val holdThreshold = 5 // Milliseconds for press-and-hold detection var pressStartTime by remember { mutableIntStateOf(0) } var isLongPressDetected by remember { mutableStateOf(false) } // Flag to track if long press has been detected LaunchedEffect(isSeeking) { // This is required to reset the variables when long press and move focus to another view. // This case On Action up of previous focussed view is not called, so that blocking us to reset these variable if (!isSeeking) { pressStartTime = 0 isLongPressDetected = false } } return this.onPreviewKeyEvent { event -> val nativeKeyEvent = event.nativeKeyEvent fun handleKeyAction(block: (isLongPress: Boolean, isReleased: Boolean) -> Unit) { when (nativeKeyEvent.action) { KeyEvent.ACTION_UP -> { block(isLongPressDetected, true) // Reset variables pressStartTime = 0 isLongPressDetected = false } KeyEvent.ACTION_DOWN -> { // only for the first press, as we have requirement, like on key down only we need to show controller. if (pressStartTime == 0) onKeyDown?.invoke(false, false) pressStartTime += 1 if (pressStartTime > holdThreshold) { block(true, false) isLongPressDetected = true // Mark that long press was detected } } } } when { // Handle D-Pad Navigation Keys DPadEventsKeyCodes.contains(nativeKeyEvent.keyCode) -> { when (nativeKeyEvent.keyCode) { KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> { onLeft?.let { handleKeyAction(it::invoke) return@onPreviewKeyEvent shouldConsumeLefEvent } } KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> { onRight?.let { handleKeyAction(it::invoke) return@onPreviewKeyEvent shouldConsumeRightEvent } } KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP -> { onUp?.let { handleKeyAction(it::invoke) } } KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN -> { onDown?.let { handleKeyAction(it::invoke) } } KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> { onEnter?.let { handleKeyAction(it::invoke) } } } shouldConsumeEvent } // Handle Media Key Events, FF/RW for media keys, rest will be handled by system . MediaKeyCodes.contains(nativeKeyEvent.keyCode) -> { when (nativeKeyEvent.keyCode) { KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { onMediaKeyFf?.let { handleKeyAction(it::invoke) return@onPreviewKeyEvent true } } KeyEvent.KEYCODE_MEDIA_REWIND -> { onMediaKeyRw?.let { handleKeyAction(it::invoke) return@onPreviewKeyEvent true } } // Handle Dedicated Media button Play/Pause at client side KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE -> { onMediaKeyPlayPause?.let { handleKeyAction(it::invoke) return@onPreviewKeyEvent shouldConsumeEventPlayPauseEvent } } else -> { onMediaKey?.invoke(nativeKeyEvent.keyCode) // Invoke media key callback } } shouldConsumeEvent } // Handle System Key Events, we may need to handle like handleKeyAction(onMediaKey?.invoke) for system keys. SystemKeyCodes.contains(nativeKeyEvent.keyCode) && nativeKeyEvent.action == KeyEvent.ACTION_DOWN -> { when (nativeKeyEvent.keyCode) { KeyEvent.KEYCODE_BACK -> onBack?.invoke(false, false) else -> onKeyDown?.invoke(false, false) } shouldConsumeEvent } else -> shouldConsumeEvent } } }
初始焦点请求扩展函数
/** * This modifier can be used to gain focus on a focusable component when it becomes visible for the first time. * */ @Composable fun Modifier.focusOnInitialVisibility( isVisible: MutableState<Boolean> = remember { mutableStateOf(false) } ): Modifier { val focusRequester = remember { FocusRequester() } return focusRequester(focusRequester) .onGloballyPositioned { if (!isVisible.value) { focusRequester.requestFocus() isVisible.value = true } } }
解决方案
1. 调整根View的可访问性与焦点属性
Talkback开启时会优先处理可访问元素的导航,需要确保CustomPlayerView的根View具备正确的交互属性:
binding.apply { // 修改原有配置,确保View可聚焦、可点击 root.isClickable = true root.isFocusable = true root.isFocusableInTouchMode = true // 添加可访问性代理,避免Talkback拦截DPAD事件 root.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onRequestSendAccessibilityEvent( host: ViewGroup?, child: View?, event: AccessibilityEvent? ): Boolean { // 允许事件传递到自定义处理逻辑 return super.onRequestSendAccessibilityEvent(host, child, event) } } // 原有playerView配置... }
2. 替换onPreviewKeyEvent为onKeyEvent
Talkback开启时部分事件会被系统优先处理,onPreviewKeyEvent无法捕获,改用onKeyEvent确保事件能被接收:
// 在handleDPadKeyEvents函数中修改 return this.onKeyEvent { event -> // 原有事件处理逻辑保持不变 }
3. 修正焦点请求逻辑
Talkback初始化过程中会干扰焦点请求,调整focusOnInitialVisibility添加延迟并适配可访问性状态:
@Composable fun Modifier.focusOnInitialVisibility( isVisible: MutableState<Boolean> = remember { mutableStateOf(false) } ): Modifier { val focusRequester = remember { FocusRequester() } val context = LocalContext.current val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager return this .focusRequester(focusRequester) .onGloballyPositioned { if (!isVisible.value) { CoroutineScope(Dispatchers.Main).launch { // 延迟等待Talkback初始化完成 delay(300) if (accessibilityManager.isTouchExplorationEnabled) { it.view.isFocusableInTouchMode = true } focusRequester.requestFocus() isVisible.value = true } } } }
4. 确保UP/DOWN键事件被消费
在handleDPadKeyEvents的DPAD事件处理分支中,明确返回true消费UP/DOWN事件,避免Talkback将其用于导航:
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP -> { onUp?.let { handleKeyAction(it::invoke) } return@onKeyEvent true } KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN -> { onDown?.let { handleKeyAction(it::invoke) } return@onKeyEvent true }
5. 配置ExoPlayer控制器的焦点行为
确保控制器显示时焦点回到PlayerView,避免焦点被控制器元素抢占:
playerView.run { // 原有配置... setControllerVisibilityListener { visibility -> if (visibility == View.VISIBLE) { requestFocus() } } }
内容的提问来源于stack exchange,提问作者Ram Prakash Bhat
相关产品推荐
相关产品推荐

