Skip to content

MediaRouteButton - remember dialog state #3172

Closed

Description

@mroczis

Use case description

I have integrated androidx.media3.cast.MediaRouteButton into my Compose UI. The problem is that my UI hides when user does not interact with it (traditional video controls). When user taps on a cast icon then cast dialog is shown. If user does not interact with it then video controls disappear including the dialog. Currently there is no way to tell if dialog is shown since showDialog it is hidden in lib function.

@MainThread
@UnstableApi
@Composable
fun MediaRouteButton(modifier: Modifier = Modifier) {
  CastUtils.verifyMainThread()
  MediaRouteButtonContainer() {
    var showDialog by remember { mutableStateOf(false) }
    IconButton(onClick = { showDialog = true }, modifier) { mediaRouteButtonIcon() }
    if (showDialog) {
      MediaRouteDialog { showDialog = false }
    }
  }
}

Proposed solution

I'd prefer to have an observable that tells me if dialog is shown so I can stop my local timer.

@MainThread
@UnstableApi
@Composable
fun MediaRouteButton(
    modifier: Modifier = Modifier,
    state: MediaRouteState = rememberMediaRouteState() 
) {
    CastUtils.verifyMainThread()
    
    MediaRouteButtonContainer {
        IconButton(
            onClick = { state.showDialog() }, 
            modifier = modifier
        ) { 
            mediaRouteButtonIcon() 
        }

        if (state.isShowingDialog) {
            MediaRouteDialog(onDismissRequest = { state.dismissDialog() })
        }
    }
}

@Stable
class MediaRouteState {
    var isShowingDialog by mutableStateOf(false)
        private set

    internal fun showDialog() {
        isShowingDialog = true
    }

    internal fun dismissDialog() {
        isShowingDialog = false
    }
}

@Composable
fun rememberMediaRouteState(): MediaRouteState {
    return remember { MediaRouteState() }
}

Activity

  1. AquilesCanta commented on Apr 13, 2026

    @AquilesCanta
    Contributor

    Hey thanks for reporting. We are tracking this internally in b/502148085. We'll do our best to share updates soon.

  2. oceanjules commented on Apr 14, 2026

    @oceanjules
    Contributor

    Hi Michal,

    While @AquilesCanta is looking at the issue, can I ask you how your choose to make the controls disappear?

    We had a very similar problem in https://github.com/androidx/media/tree/release/demos/compose, but instead of a Button -> opens -> Dialog, we have a Button -> opens -> BottomSheet.

    However, the problem was in how we set up the "disappearing" condition. See 31f6653 for the first commit. The controls would disappear 3 seconds after the pointer would be up, i.e. the user would stop interacting with the Player composable.

    This works for all composables on the screen (e.g. ProgressSlider dragging, toggling ShuffleButton/RepeatButton/MuteButton and just non-stop touching the video surface). But since PlaybackSpeedButton opens a ModalBottonSheet with more speed controls to interact with (slider, +/- buttons, preset speeds), we don't inherit the pointer tracking from the parent node that we used to open this.

    That means that when we are in that ModalBottomSheet, trying to pick a new speed, the 3 second timeout for fading the control already begins! And if the user is too slow, it will disappear right under the user's fingers.

    The solution to this has been to expose the Modifier not just of the sheet, but the content of the sheet - and keep track of pointer input there - cfb6b45. That meant that we could add "interacting with the sheet" to "pointerDown" logic - 3bb3edf.

    For this case, it would mean for MediaRouteButton to take in both modifier for the IconButton and then dialogModifier for the MediaRouteDialog that it will be "opening". That is, only if you need to be tracking the finger/stylus interaction.

    So back to my original question - what, for you, determines that the controls should disappear?

  3. mroczis commented on Apr 15, 2026

    @mroczis
    Author

    I'm using composition local to force controls to appear. In my AutoHide composable I hold my autoHideState.

    class AutoHideState {
        var forceShow : Boolean by mutableStateOf(false)
    }
    
    @Composable
    fun rememberAutoHideState() = remember { AutoHideState() }
    
    val LocalAutoHide = compositionLocalOf { AutoHideState() }
    
    @Composable
    fun AutoHide(
        dimColor: Color?,
        timeout: Duration = 3.seconds,
        enabled: Boolean = true,
        state: AutoHideState = rememberAutoHideState(),
        content: @Composable () -> Unit
    ) {
        var displayControls by remember { mutableStateOf(true) }
        var resetTimerTrigger by remember { mutableLongStateOf(0L) }
    
        LaunchedEffect(resetTimerTrigger) {
            if (displayControls) {
                delay(timeout)
                displayControls = false
            }
        }
    
        LaunchedEffect(state.forceShow) {
            // reset timer when controls can hide once again
            if (!state.forceShow) { 
                displayControls = true
                resetTimerTrigger = System.currentTimeMillis()
            }
        }
    
        val onInteraction = {
            displayControls = true
            resetTimerTrigger = System.currentTimeMillis()
        }
    
        val dimModifier = if (dimColor) {
            Modifier.fillMaxSize().background(dimColor)
        } else {
            Modifier
        }
    
        Box(
            Modifier.fillMaxSize().pointerInput(Unit) {
                awaitEachGesture {
                    // 1. Wait for the initial touch down
                    awaitFirstDown(requireUnconsumed = false)
                    onInteraction()
    
                    // 2. While the finger is still touching/moving, keep resetting the timer
                    do {
                        val event = awaitPointerEvent()
                        val anyPressed = event.changes.any { it.pressed }
                        if (anyPressed) {
                            onInteraction()
                        }
                    } while (anyPressed)
                }
            }
        ) {
            CompositionLocalProvider(LocalAutoHide provides state) {
                Crossfade(displayControls || !enabled || state.forceShow) { controls ->
                    if (controls) {
                        Box(dimModifier) {
                            content()
                        }
                    }
                }
            }
        }
    }
    

    Then I have a UI component that might trigger dialog. When a dialog is shown then I just update forceShow. This approach is pretty simple and does not support multiple forceShow requestors. AutoHideState can be adjusted accordingly (replace boolean with a counter).

    @Composable
    @OptIn(UnstableApi::class)
    fun CaptionsButton(
        controller: MediaController,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
    ) {
        val autoHide = LocalAutoHide.current
        val state = rememberClosedCaptionsSelectorState(controller) // out of scope of this question
        var showSheet by rememberSaveable { mutableStateOf(false) }
    
        LaunchedEffect(showSheet) {
            autoHide.forceShow = showSheet
        }
    
       IconButton(
            onClick = { showSheet = true },
            enabled = state.captions.isNotEmpty() && enabled,
        ) {
            Icon(
                painter = painterResource(R.drawable.ic_player_cc),
                contentDescription = stringResource(R.string.player_captions),
            )
        }
    
       if (showSheet) {
          val sheetState = rememberModalBottomSheetState()
          // ...
       }
    
    }
  4. oceanjules commented on Jul 23, 2026

    @oceanjules
    Contributor

    Hi @mroczis, please use MediaRouteButtonState

    val mediaRouteButtonState = rememberMediaRouteButtonState()
    
    LaunchedEffect(showControls, mediaRouteButtonState.isPickerVisible ) {
        if (showControls && !mediaRouteButtonState.isPickerVisible) {
          hideControls()
        }
    }
    
    // ...
    MediaRouteButton(state = mediaRouteButtonState)
    // ...
  5. locked and limited conversation to collaborators on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions