diff options
4 files changed, 198 insertions, 59 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index a3467f2ab78e..1def7fe95a4b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -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,21 +98,13 @@ 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 - }, - isExpanded = isExpanded, - onExpandedChanged = onExpandedChanged, - sliderColors = sliderColors, - ) - } + ExpandButton( + modifier = Modifier.align(Alignment.CenterEnd), + isExpanded = isExpanded, + isExpandable = isExpandable, + onExpandedChanged = onExpandedChanged, + sliderColors = sliderColors, + ) } transition.AnimatedVisibility( visible = { it || !isExpandable }, @@ -147,30 +147,48 @@ fun ColumnVolumeSliders( @Composable private fun ExpandButton( isExpanded: Boolean, + isExpandable: Boolean, onExpandedChanged: (Boolean) -> Unit, sliderColors: PlatformSliderColors, modifier: Modifier = Modifier, ) { - IconButton( - modifier = modifier.size(64.dp), - onClick = { onExpandedChanged(!isExpanded) }, - colors = - IconButtonDefaults.filledIconButtonColors( - containerColor = sliderColors.indicatorColor, - contentColor = sliderColors.iconColor - ), + 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(), ) { - Icon( - painter = - painterResource( - if (isExpanded) { - R.drawable.ic_filled_arrow_down - } else { - R.drawable.ic_filled_arrow_up - } + IconButton( + modifier = + Modifier.size(64.dp).semantics { + role = Role.Switch + stateDescription = expandButtonStateDescription + }, + onClick = { onExpandedChanged(!isExpanded) }, + colors = + IconButtonDefaults.filledIconButtonColors( + containerColor = sliderColors.indicatorColor, + contentColor = sliderColors.iconColor ), - contentDescription = null, - ) + ) { + Icon( + painter = + painterResource( + if (isExpanded) { + R.drawable.ic_filled_arrow_down + } else { + R.drawable.ic_filled_arrow_up + } + ), + contentDescription = null, + ) + } } } @@ -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" + ) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt index fdf8ee872019..79056b26d051 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt @@ -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(), ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt index 26d6a9a0153d..4b4d69a31db4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt @@ -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 - } - }, - ) - .stateIn(scope, SharingStarted.Eagerly, false) + fun isExpandable(isPortrait: Boolean): Flow<SlidersExpandableViewModel> { + return if (isPortrait) { + portraitExpandable + } else { + flowOf(SlidersExpandableViewModel.Fixed) + } + } fun onExpandedChanged(isExpanded: Boolean) { - scope.launch { mutableIsExpanded.emit(isExpanded) } + scope.launch { mutableIsExpanded.value = isExpanded } } private fun CoroutineScope.createSessionViewModel( diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/SlidersExpandableViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/SlidersExpandableViewModel.kt new file mode 100644 index 000000000000..19b9ead88ebb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/SlidersExpandableViewModel.kt @@ -0,0 +1,31 @@ +/* + * 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 +} |