• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.compose.animation.scene.demo
18 
19 import androidx.compose.foundation.background
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.fillMaxWidth
22 import androidx.compose.foundation.layout.height
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.shape.RoundedCornerShape
25 import androidx.compose.material.icons.Icons
26 import androidx.compose.material.icons.filled.Pause
27 import androidx.compose.material.icons.filled.PlayArrow
28 import androidx.compose.material3.FilledIconButton
29 import androidx.compose.material3.Icon
30 import androidx.compose.material3.IconButtonDefaults
31 import androidx.compose.material3.MaterialTheme
32 import androidx.compose.runtime.Composable
33 import androidx.compose.ui.Alignment
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.unit.dp
36 import com.android.compose.animation.scene.ContentKey
37 import com.android.compose.animation.scene.ContentScope
38 import com.android.compose.animation.scene.ElementKey
39 import com.android.compose.animation.scene.MovableElementContentPicker
40 import com.android.compose.animation.scene.MovableElementKey
41 import com.android.compose.animation.scene.StaticElementContentPicker
42 import com.android.compose.animation.scene.content.state.TransitionState
43 
44 object MediaPlayer {
45     object Elements {
46         val MediaPlayer = MovableElementKey("MediaPlayer", contentPicker = ContentPicker)
47         val SmallMediaPlayer =
48             MovableElementKey(
49                 "SmallMediaPlayer",
50                 contentPicker = MovableElementContentPicker(setOf(Overlays.QuickSettings)),
51             )
52     }
53 
54     object Dimensions {
55         val HeightLarge = 150.dp
56         val HeightSmall = 70.dp
57     }
58 
59     object Shapes {
60         val Background = RoundedCornerShape(24.dp)
61     }
62 
63     object ContentPicker : StaticElementContentPicker {
64         override val contents =
65             setOf(
66                 Scenes.Lockscreen,
67                 Scenes.SplitLockscreen,
68                 Scenes.Shade,
69                 Scenes.SplitShade,
70                 Scenes.QuickSettings,
71                 Overlays.Notifications,
72             )
73 
contentDuringTransitionnull74         override fun contentDuringTransition(
75             element: ElementKey,
76             transition: TransitionState.Transition,
77             fromContentZIndex: Long,
78             toContentZIndex: Long,
79         ): ContentKey {
80             return when {
81                 // During the Lockscreen => Shade transition, the media player is visible in the
82                 // Lockscreen when progress is in [0; 0.5]. It is then visible in the Shade scene
83                 // when progress is in [0.8; 1]. We move it half-way through, when progress = 0.65.
84                 transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> {
85                     if (transition.progress < 0.65f) {
86                         Scenes.Lockscreen
87                     } else {
88                         Scenes.Shade
89                     }
90                 }
91 
92                 // Same as Lockscreen => Shade, but with reversed progress.
93                 transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> {
94                     if (transition.progress < 1f - 0.65f) {
95                         Scenes.Shade
96                     } else {
97                         Scenes.Lockscreen
98                     }
99                 }
100 
101                 // When going from QS <=> Shade we always want to compose the media player in the QS
102                 // scene, otherwise it will be drawn above the QS footer actions.
103                 transition.isTransitioningBetween(Scenes.QuickSettings, Scenes.Shade) -> {
104                     Scenes.QuickSettings
105                 }
106 
107                 // When going from SplitLockscreen to SplitShade, we always compose the media player
108                 // in the SplitShade, otherwise the shade will fade above it.
109                 transition.isTransitioningBetween(Scenes.SplitLockscreen, Scenes.SplitShade) ->
110                     Scenes.SplitShade
111                 transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.QuickSettings) ->
112                     Scenes.QuickSettings
113                 transition.isTransitioningFromOrTo(Overlays.Notifications) -> Overlays.Notifications
114                 else -> pickSingleContentIn(contents, transition, element)
115             }
116         }
117     }
118 }
119 
120 @Composable
ContentScopenull121 fun ContentScope.MediaPlayer(
122     presentationStyle: DemoMediaPresentationStyle,
123     isPlaying: Boolean,
124     onIsPlayingChange: (Boolean) -> Unit,
125     onVisibilityChange: (Boolean) -> Unit,
126     modifier: Modifier = Modifier,
127 ) {
128     val injectedContentOrNull = LocalDependencies.current.mediaPlayer
129     if (injectedContentOrNull != null) {
130         Element(
131             key =
132                 if (presentationStyle != DemoMediaPresentationStyle.Compact) {
133                     MediaPlayer.Elements.MediaPlayer
134                 } else {
135                     MediaPlayer.Elements.SmallMediaPlayer
136                 },
137             modifier = modifier.fillMaxWidth(),
138         ) {
139             injectedContentOrNull(presentationStyle, onVisibilityChange)
140         }
141     } else {
142         val isSmall = presentationStyle == DemoMediaPresentationStyle.Compact
143         val key =
144             if (isSmall) {
145                 MediaPlayer.Elements.SmallMediaPlayer
146             } else {
147                 MediaPlayer.Elements.MediaPlayer
148             }
149 
150         MovableElement(
151             key,
152             modifier
153                 .fillMaxWidth()
154                 .height(
155                     if (isSmall) MediaPlayer.Dimensions.HeightSmall
156                     else MediaPlayer.Dimensions.HeightLarge
157                 ),
158         ) {
159             content {
160                 Box(
161                     Modifier.background(
162                             MaterialTheme.colorScheme.tertiary,
163                             MediaPlayer.Shapes.Background,
164                         )
165                         .padding(8.dp)
166                 ) {
167                     FilledIconButton(
168                         onClick = { onIsPlayingChange(!isPlaying) },
169                         Modifier.align(Alignment.CenterEnd),
170                         colors =
171                             IconButtonDefaults.filledIconButtonColors(
172                                 MaterialTheme.colorScheme.onTertiary
173                             ),
174                     ) {
175                         val color = MaterialTheme.colorScheme.tertiary
176                         if (isPlaying) {
177                             Icon(Icons.Default.Pause, null, tint = color)
178                         } else {
179                             Icon(Icons.Default.PlayArrow, null, tint = color)
180                         }
181                     }
182                 }
183             }
184         }
185     }
186 }
187 
188 enum class DemoMediaPresentationStyle {
189     Default,
190     Compressed,
191     Compact,
192 }
193