1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.volume.panel.component.mediaoutput.ui.composable
18
19 import androidx.compose.animation.AnimatedContent
20 import androidx.compose.animation.core.snap
21 import androidx.compose.animation.core.tween
22 import androidx.compose.animation.core.updateTransition
23 import androidx.compose.animation.fadeIn
24 import androidx.compose.animation.fadeOut
25 import androidx.compose.animation.scaleIn
26 import androidx.compose.animation.scaleOut
27 import androidx.compose.animation.slideInVertically
28 import androidx.compose.animation.slideOutVertically
29 import androidx.compose.animation.togetherWith
30 import androidx.compose.foundation.background
31 import androidx.compose.foundation.basicMarquee
32 import androidx.compose.foundation.layout.Arrangement
33 import androidx.compose.foundation.layout.Box
34 import androidx.compose.foundation.layout.Column
35 import androidx.compose.foundation.layout.Row
36 import androidx.compose.foundation.layout.RowScope
37 import androidx.compose.foundation.layout.Spacer
38 import androidx.compose.foundation.layout.aspectRatio
39 import androidx.compose.foundation.layout.fillMaxHeight
40 import androidx.compose.foundation.layout.fillMaxSize
41 import androidx.compose.foundation.layout.fillMaxWidth
42 import androidx.compose.foundation.layout.height
43 import androidx.compose.foundation.layout.padding
44 import androidx.compose.foundation.shape.RoundedCornerShape
45 import androidx.compose.material3.MaterialTheme
46 import androidx.compose.material3.Text
47 import androidx.compose.runtime.Composable
48 import androidx.compose.runtime.getValue
49 import androidx.compose.ui.Alignment
50 import androidx.compose.ui.Modifier
51 import androidx.compose.ui.res.stringResource
52 import androidx.compose.ui.semantics.LiveRegionMode
53 import androidx.compose.ui.semantics.liveRegion
54 import androidx.compose.ui.semantics.onClick
55 import androidx.compose.ui.semantics.semantics
56 import androidx.compose.ui.unit.dp
57 import androidx.lifecycle.compose.collectAsStateWithLifecycle
58 import com.android.compose.animation.Expandable
59 import com.android.systemui.common.ui.compose.Icon
60 import com.android.systemui.common.ui.compose.toColor
61 import com.android.systemui.res.R
62 import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.ConnectedDeviceViewModel
63 import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.DeviceIconViewModel
64 import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.MediaOutputViewModel
65 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
66 import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
67 import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
68 import javax.inject.Inject
69
70 @VolumePanelScope
71 class MediaOutputComponent
72 @Inject
73 constructor(
74 private val viewModel: MediaOutputViewModel,
75 ) : ComposeVolumePanelUiComponent {
76
77 @Composable
78 override fun VolumePanelComposeScope.Content(modifier: Modifier) {
79 val connectedDeviceViewModel: ConnectedDeviceViewModel? by
80 viewModel.connectedDeviceViewModel.collectAsStateWithLifecycle()
81 val deviceIconViewModel: DeviceIconViewModel? by
82 viewModel.deviceIconViewModel.collectAsStateWithLifecycle()
83 val clickLabel = stringResource(R.string.volume_panel_enter_media_output_settings)
84 val enabled: Boolean by viewModel.enabled.collectAsStateWithLifecycle()
85
86 Expandable(
87 modifier =
88 Modifier.fillMaxWidth().height(80.dp).semantics {
89 liveRegion = LiveRegionMode.Polite
90 this.onClick(label = clickLabel) {
91 viewModel.onBarClick(null)
92 true
93 }
94 },
95 color = MaterialTheme.colorScheme.surface,
96 shape = RoundedCornerShape(28.dp),
97 onClick =
98 if (enabled) {
99 { viewModel.onBarClick(it) }
100 } else {
101 null
102 },
103 ) { _ ->
104 Row(modifier = Modifier, verticalAlignment = Alignment.CenterVertically) {
105 connectedDeviceViewModel?.let { ConnectedDeviceText(it) }
106
107 deviceIconViewModel?.let { ConnectedDeviceIcon(it) }
108 }
109 }
110 }
111
112 @Composable
113 private fun RowScope.ConnectedDeviceText(connectedDeviceViewModel: ConnectedDeviceViewModel) {
114 Column(
115 modifier = Modifier.weight(1f).padding(start = 24.dp),
116 verticalArrangement = Arrangement.spacedBy(4.dp),
117 ) {
118 Text(
119 modifier = Modifier.basicMarquee(),
120 text = connectedDeviceViewModel.label.toString(),
121 style = MaterialTheme.typography.labelMedium,
122 color = MaterialTheme.colorScheme.onSurfaceVariant,
123 maxLines = 1,
124 )
125 connectedDeviceViewModel.deviceName?.let {
126 Text(
127 modifier = Modifier.basicMarquee(),
128 text = it.toString(),
129 style = MaterialTheme.typography.titleMedium,
130 color = MaterialTheme.colorScheme.onSurface,
131 maxLines = 1,
132 )
133 }
134 }
135 }
136
137 @Composable
138 private fun ConnectedDeviceIcon(deviceIconViewModel: DeviceIconViewModel) {
139 val transition = updateTransition(deviceIconViewModel, label = "MediaOutputIconTransition")
140 Box(
141 modifier = Modifier.padding(16.dp).fillMaxHeight().aspectRatio(1f),
142 contentAlignment = Alignment.Center
143 ) {
144 transition.AnimatedContent(
145 contentKey = { it.backgroundColor },
146 transitionSpec = {
147 if (targetState is DeviceIconViewModel.IsPlaying) {
148 scaleIn(
149 initialScale = 0.9f,
150 animationSpec = isPlayingInIconBackgroundSpec(),
151 ) + fadeIn(animationSpec = isPlayingInIconBackgroundSpec()) togetherWith
152 fadeOut(animationSpec = snap())
153 } else {
154 fadeIn(animationSpec = snap(delayMillis = 900)) togetherWith
155 scaleOut(
156 targetScale = 0.9f,
157 animationSpec = isPlayingOutSpec(),
158 ) + fadeOut(animationSpec = isPlayingOutSpec())
159 }
160 }
161 ) { targetViewModel ->
162 Spacer(
163 modifier =
164 Modifier.fillMaxSize()
165 .background(
166 color = targetViewModel.backgroundColor.toColor(),
167 shape = RoundedCornerShape(12.dp),
168 ),
169 )
170 }
171 transition.AnimatedContent(
172 contentKey = { it.icon },
173 transitionSpec = {
174 if (targetState is DeviceIconViewModel.IsPlaying) {
175 fadeIn(animationSpec = snap(delayMillis = 700)) togetherWith
176 slideOutVertically(
177 targetOffsetY = { it },
178 animationSpec = isPlayingInIconSpec(),
179 ) + fadeOut(animationSpec = isNotPlayingOutIconSpec())
180 } else {
181 slideInVertically(
182 initialOffsetY = { it },
183 animationSpec = isNotPlayingInIconSpec(),
184 ) + fadeIn(animationSpec = isNotPlayingInIconSpec()) togetherWith
185 fadeOut(animationSpec = isPlayingOutSpec())
186 }
187 }
188 ) {
189 Icon(
190 icon = it.icon,
191 tint = it.iconColor.toColor(),
192 modifier = Modifier.padding(12.dp).fillMaxSize(),
193 )
194 }
195 }
196 }
197 }
198
isPlayingOutSpecnull199 private fun <T> isPlayingOutSpec() = tween<T>(durationMillis = 400, delayMillis = 500)
200
201 private fun <T> isPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300)
202
203 private fun <T> isPlayingInIconBackgroundSpec() = tween<T>(durationMillis = 400, delayMillis = 700)
204
205 private fun <T> isNotPlayingOutIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300)
206
207 private fun <T> isNotPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 900)
208