Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 71d01d34 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Hide expand button when there is no active media playing" into main

parents a1de1721 ec989c70
Loading
Loading
Loading
Loading
+116 −38
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.expandVertically
@@ -28,10 +30,8 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -39,6 +39,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@@ -49,15 +50,21 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSliderColors
import com.android.compose.modifiers.padding
import com.android.systemui.res.R
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel

private const val EXPAND_DURATION_MILLIS = 500
private const val COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS = 350
private const val COLLAPSE_DURATION_MILLIS = 300
private const val EXPAND_BUTTON_ANIMATION_DURATION_MILLIS = 350
private const val TOP_SLIDER_ANIMATION_DURATION_MILLIS = 400
private const val SHRINK_FRACTION = 0.55f
private const val SCALE_FRACTION = 0.9f
private const val EXPAND_BUTTON_SCALE = 0.8f

/** Volume sliders laid out in a collapsable column */
@OptIn(ExperimentalAnimationApi::class)
@@ -73,14 +80,15 @@ fun ColumnVolumeSliders(
    require(viewModels.isNotEmpty())
    val transition = updateTransition(isExpanded, label = "CollapsableSliders")
    Column(modifier = modifier) {
        Row(
        Box(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            val sliderViewModel: SliderViewModel = viewModels.first()
            val sliderState by viewModels.first().slider.collectAsState()
            val sliderPadding by topSliderPadding(isExpandable)

            VolumeSlider(
                modifier = Modifier.weight(1f),
                modifier = Modifier.padding(end = { sliderPadding.roundToPx() }).fillMaxWidth(),
                state = sliderState,
                onValueChange = { newValue: Float ->
                    sliderViewModel.onValueChanged(sliderState, newValue)
@@ -90,22 +98,14 @@ fun ColumnVolumeSliders(
                sliderColors = sliderColors,
            )

            val expandButtonStateDescription =
                if (isExpanded) stringResource(R.string.volume_panel_expanded_sliders)
                else stringResource(R.string.volume_panel_collapsed_sliders)
            if (isExpandable) {
            ExpandButton(
                    modifier =
                        Modifier.semantics {
                            role = Role.Switch
                            stateDescription = expandButtonStateDescription
                        },
                modifier = Modifier.align(Alignment.CenterEnd),
                isExpanded = isExpanded,
                isExpandable = isExpandable,
                onExpandedChanged = onExpandedChanged,
                sliderColors = sliderColors,
            )
        }
        }
        transition.AnimatedVisibility(
            visible = { it || !isExpandable },
            enter =
@@ -147,12 +147,29 @@ fun ColumnVolumeSliders(
@Composable
private fun ExpandButton(
    isExpanded: Boolean,
    isExpandable: Boolean,
    onExpandedChanged: (Boolean) -> Unit,
    sliderColors: PlatformSliderColors,
    modifier: Modifier = Modifier,
) {
    val expandButtonStateDescription =
        if (isExpanded) {
            stringResource(R.string.volume_panel_expanded_sliders)
        } else {
            stringResource(R.string.volume_panel_collapsed_sliders)
        }
    AnimatedVisibility(
        modifier = modifier,
        visible = isExpandable,
        enter = expandButtonEnterTransition(),
        exit = expandButtonExitTransition(),
    ) {
        IconButton(
        modifier = modifier.size(64.dp),
            modifier =
                Modifier.size(64.dp).semantics {
                    role = Role.Switch
                    stateDescription = expandButtonStateDescription
                },
            onClick = { onExpandedChanged(!isExpanded) },
            colors =
                IconButtonDefaults.filledIconButtonColors(
@@ -173,6 +190,7 @@ private fun ExpandButton(
            )
        }
    }
}

private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
    val enterDelay = ((totalCount - index + 1) * 10).coerceAtLeast(0)
@@ -204,3 +222,63 @@ private fun exitTransition(index: Int, totalCount: Int): ExitTransition {
        ) +
        fadeOut(animationSpec = tween(durationMillis = exitDuration))
}

private fun expandButtonEnterTransition(): EnterTransition {
    return fadeIn(
        tween(
            delayMillis = COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS,
            durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
        )
    ) +
        scaleIn(
            animationSpec =
                tween(
                    delayMillis = COLLAPSE_EXPAND_BUTTON_DELAY_MILLIS,
                    durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
                ),
            initialScale = EXPAND_BUTTON_SCALE,
        )
}

private fun expandButtonExitTransition(): ExitTransition {
    return fadeOut(
        tween(
            delayMillis = EXPAND_DURATION_MILLIS,
            durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
        )
    ) +
        scaleOut(
            animationSpec =
                tween(
                    delayMillis = EXPAND_DURATION_MILLIS,
                    durationMillis = EXPAND_BUTTON_ANIMATION_DURATION_MILLIS,
                ),
            targetScale = EXPAND_BUTTON_SCALE,
        )
}

@Composable
private fun topSliderPadding(isExpandable: Boolean): State<Dp> {
    val animationSpec: AnimationSpec<Dp> =
        if (isExpandable) {
            tween(
                delayMillis = COLLAPSE_DURATION_MILLIS,
                durationMillis = TOP_SLIDER_ANIMATION_DURATION_MILLIS,
            )
        } else {
            tween(
                delayMillis = EXPAND_DURATION_MILLIS,
                durationMillis = TOP_SLIDER_ANIMATION_DURATION_MILLIS,
            )
        }
    return animateDpAsState(
        targetValue =
            if (isExpandable) {
                72.dp
            } else {
                0.dp
            },
        animationSpec = animationSpec,
        label = "TopVolumeSliderPadding"
    )
}
+11 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier
import com.android.compose.PlatformSliderDefaults
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
import com.android.systemui.volume.panel.component.volume.ui.viewmodel.AudioVolumeComponentViewModel
import com.android.systemui.volume.panel.component.volume.ui.viewmodel.SlidersExpandableViewModel
import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
import com.android.systemui.volume.panel.ui.composable.isPortrait
@@ -48,13 +49,21 @@ constructor(
                modifier = modifier.fillMaxWidth(),
            )
        } else {
            val isExpanded by viewModel.isExpanded.collectAsState()
            val expandableViewModel: SlidersExpandableViewModel by
                viewModel
                    .isExpandable(isPortrait)
                    .collectAsState(SlidersExpandableViewModel.Unavailable)
            if (expandableViewModel is SlidersExpandableViewModel.Unavailable) {
                return
            }
            val isExpanded =
                (expandableViewModel as? SlidersExpandableViewModel.Expandable)?.isExpanded ?: true
            ColumnVolumeSliders(
                viewModels = sliderViewModels,
                isExpanded = isExpanded,
                onExpandedChanged = viewModel::onExpandedChanged,
                sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors(),
                isExpandable = isPortrait,
                isExpandable = expandableViewModel is SlidersExpandableViewModel.Expandable,
                modifier = modifier.fillMaxWidth(),
            )
        }
+40 −19
Original line number Diff line number Diff line
@@ -31,13 +31,15 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
@@ -53,12 +55,39 @@ class AudioVolumeComponentViewModel
constructor(
    @VolumePanelScope private val scope: CoroutineScope,
    mediaOutputInteractor: MediaOutputInteractor,
    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
    mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
    private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
    private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
    streamsInteractor: AudioSlidersInteractor,
) {

    private val mutableIsExpanded = MutableStateFlow<Boolean?>(null)
    private val isPlaybackActive: Flow<Boolean?> =
        mediaOutputInteractor.defaultActiveMediaSession
            .filterData()
            .flatMapLatest { session ->
                if (session == null) {
                    flowOf(false)
                } else {
                    mediaDeviceSessionInteractor.playbackState(session).map { it?.isActive == true }
                }
            }
            .onEach { isPlaybackActive -> mutableIsExpanded.value = !isPlaybackActive }
            .stateIn(scope, SharingStarted.Eagerly, null)
    private val portraitExpandable: Flow<SlidersExpandableViewModel> =
        isPlaybackActive
            .filterNotNull()
            .flatMapLatest { isActive ->
                if (isActive) {
                    mutableIsExpanded.filterNotNull().map { isExpanded ->
                        SlidersExpandableViewModel.Expandable(isExpanded)
                    }
                } else {
                    flowOf(SlidersExpandableViewModel.Fixed)
                }
            }
            .stateIn(scope, SharingStarted.Eagerly, SlidersExpandableViewModel.Unavailable)

    val sliderViewModels: StateFlow<List<SliderViewModel>> =
        streamsInteractor.volumePanelSliders
            .transformLatest { sliderTypes ->
@@ -76,24 +105,16 @@ constructor(
            }
            .stateIn(scope, SharingStarted.Eagerly, emptyList())

    private val mutableIsExpanded = MutableSharedFlow<Boolean>()

    val isExpanded: StateFlow<Boolean> =
        merge(
                mutableIsExpanded,
                mediaOutputInteractor.defaultActiveMediaSession.filterData().flatMapLatest { session
                    ->
                    if (session == null) flowOf(true)
                    else
                        mediaDeviceSessionInteractor.playbackState(session).map {
                            it?.isActive != true
    fun isExpandable(isPortrait: Boolean): Flow<SlidersExpandableViewModel> {
        return if (isPortrait) {
            portraitExpandable
        } else {
            flowOf(SlidersExpandableViewModel.Fixed)
        }
    }
                },
            )
            .stateIn(scope, SharingStarted.Eagerly, false)

    fun onExpandedChanged(isExpanded: Boolean) {
        scope.launch { mutableIsExpanded.emit(isExpanded) }
        scope.launch { mutableIsExpanded.value = isExpanded }
    }

    private fun CoroutineScope.createSessionViewModel(
+31 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.volume.panel.component.volume.ui.viewmodel

/**
 * Models expandability state of the
 * [com.android.systemui.volume.panel.component.volume.ui.composable.VolumeSlidersComponent].
 */
sealed interface SlidersExpandableViewModel {

    /** [SlidersExpandableViewModel] is not loaded. */
    data object Unavailable : SlidersExpandableViewModel

    data class Expandable(val isExpanded: Boolean) : SlidersExpandableViewModel

    data object Fixed : SlidersExpandableViewModel
}