summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt154
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlidersComponent.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/SlidersExpandableViewModel.kt31
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
+}