You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 07:05:54