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