• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.airbnb.lottie.sample.compose.player
2 
3 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
4 import androidx.compose.animation.AnimatedVisibility
5 import androidx.compose.animation.expandVertically
6 import androidx.compose.animation.shrinkVertically
7 import androidx.compose.foundation.background
8 import androidx.compose.foundation.border
9 import androidx.compose.foundation.clickable
10 import androidx.compose.foundation.horizontalScroll
11 import androidx.compose.foundation.layout.Arrangement
12 import androidx.compose.foundation.layout.Box
13 import androidx.compose.foundation.layout.Column
14 import androidx.compose.foundation.layout.ColumnScope
15 import androidx.compose.foundation.layout.Row
16 import androidx.compose.foundation.layout.fillMaxHeight
17 import androidx.compose.foundation.layout.fillMaxWidth
18 import androidx.compose.foundation.layout.heightIn
19 import androidx.compose.foundation.layout.padding
20 import androidx.compose.foundation.layout.size
21 import androidx.compose.foundation.layout.width
22 import androidx.compose.foundation.lazy.LazyColumn
23 import androidx.compose.foundation.lazy.itemsIndexed
24 import androidx.compose.foundation.rememberScrollState
25 import androidx.compose.foundation.shape.CircleShape
26 import androidx.compose.foundation.shape.RoundedCornerShape
27 import androidx.compose.material.Icon
28 import androidx.compose.material.IconButton
29 import androidx.compose.material.Scaffold
30 import androidx.compose.material.Slider
31 import androidx.compose.material.Surface
32 import androidx.compose.material.Text
33 import androidx.compose.material.TopAppBar
34 import androidx.compose.material.icons.Icons
35 import androidx.compose.material.icons.filled.Close
36 import androidx.compose.material.icons.filled.MergeType
37 import androidx.compose.material.icons.filled.Pause
38 import androidx.compose.material.icons.filled.PlayArrow
39 import androidx.compose.material.icons.filled.RemoveRedEye
40 import androidx.compose.material.icons.filled.Repeat
41 import androidx.compose.material.icons.filled.Warning
42 import androidx.compose.material.rememberScaffoldState
43 import androidx.compose.runtime.Composable
44 import androidx.compose.runtime.LaunchedEffect
45 import androidx.compose.runtime.Stable
46 import androidx.compose.runtime.getValue
47 import androidx.compose.runtime.mutableStateOf
48 import androidx.compose.runtime.remember
49 import androidx.compose.runtime.setValue
50 import androidx.compose.ui.Alignment
51 import androidx.compose.ui.Modifier
52 import androidx.compose.ui.draw.clip
53 import androidx.compose.ui.graphics.Color
54 import androidx.compose.ui.graphics.vector.rememberVectorPainter
55 import androidx.compose.ui.platform.LocalDensity
56 import androidx.compose.ui.res.colorResource
57 import androidx.compose.ui.res.painterResource
58 import androidx.compose.ui.res.stringResource
59 import androidx.compose.ui.text.TextStyle
60 import androidx.compose.ui.text.style.TextAlign
61 import androidx.compose.ui.tooling.preview.Preview
62 import androidx.compose.ui.unit.dp
63 import androidx.compose.ui.unit.sp
64 import androidx.compose.ui.window.Dialog
65 import com.airbnb.lottie.LottieComposition
66 import com.airbnb.lottie.compose.LottieAnimatable
67 import com.airbnb.lottie.compose.LottieAnimation
68 import com.airbnb.lottie.compose.LottieCompositionSpec
69 import com.airbnb.lottie.compose.LottieConstants
70 import com.airbnb.lottie.compose.rememberLottieComposition
71 import com.airbnb.lottie.compose.resetToBeginning
72 import com.airbnb.lottie.sample.compose.BuildConfig
73 import com.airbnb.lottie.sample.compose.R
74 import com.airbnb.lottie.sample.compose.composables.DebouncedCircularProgressIndicator
75 import com.airbnb.lottie.sample.compose.ui.Teal
76 import com.airbnb.lottie.sample.compose.utils.drawBottomBorder
77 import com.airbnb.lottie.sample.compose.utils.maybeBackground
78 import com.airbnb.lottie.sample.compose.utils.maybeDrawBorder
79 import com.airbnb.lottie.sample.compose.utils.toDummyBitmap
80 import kotlin.math.ceil
81 import kotlin.math.roundToInt
82 
83 @Stable
84 class PlayerPageState(backgroundColor: Color?) {
85     val animatable = LottieAnimatable()
86 
87     var backgroundColor by mutableStateOf(backgroundColor)
88     var outlineMasksAndMattes by mutableStateOf(false)
89     var applyOpacityToLayers by mutableStateOf(false)
90     var enableMergePaths by mutableStateOf(false)
91     var focusMode by mutableStateOf(false)
92     var showWarningsDialog by mutableStateOf(false)
93 
94     var borderToolbar by mutableStateOf(false)
95     var speedToolbar by mutableStateOf(false)
96     var backgroundColorToolbar by mutableStateOf(false)
97 
98     var progressSliderGesture: Float? by mutableStateOf(null)
99     var shouldPlay by mutableStateOf(true)
100     var targetSpeed by mutableStateOf(1f)
101     var shouldLoop by mutableStateOf(true)
102 }
103 
104 @Composable
PlayerPagenull105 fun PlayerPage(
106     spec: LottieCompositionSpec,
107     animationBackgroundColor: Color? = null,
108 ) {
109     val scaffoldState = rememberScaffoldState()
110     val state = remember { PlayerPageState(animationBackgroundColor) }
111 
112     val failedMessage = stringResource(R.string.failed_to_load)
113     val okMessage = stringResource(R.string.ok)
114 
115     val compositionResult = rememberLottieComposition(spec)
116 
117     LaunchedEffect(compositionResult.isFailure) {
118         if (!compositionResult.isFailure) return@LaunchedEffect
119         scaffoldState.snackbarHostState.showSnackbar(
120             message = failedMessage,
121             actionLabel = okMessage,
122         )
123     }
124 
125     val dummyBitmapStrokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
126     LaunchedEffect(compositionResult.value) {
127         val composition = compositionResult.value ?: return@LaunchedEffect
128         for (asset in composition.images.values) {
129             if (asset.bitmap != null) continue
130             asset.bitmap = asset.toDummyBitmap(dummyBitmapStrokeWidth)
131         }
132     }
133 
134     Scaffold(
135         scaffoldState = scaffoldState,
136         topBar = { PlayerPageTopAppBar(state, compositionResult.value) },
137     ) { padding ->
138         Box(
139             modifier = Modifier
140                 .padding(padding)
141         ) {
142             PlayerPageContent(
143                 state,
144                 compositionResult.value,
145                 compositionResult.isLoading,
146                 animationBackgroundColor,
147             )
148         }
149     }
150 
151     if (state.showWarningsDialog) {
152         WarningDialog(warnings = compositionResult.value?.warnings ?: emptyList(), onDismiss = { state.showWarningsDialog = false })
153     }
154 }
155 
156 @Composable
ExpandVisibilitynull157 private fun ColumnScope.ExpandVisibility(
158     visible: Boolean,
159     content: @Composable () -> Unit,
160 ) {
161     AnimatedVisibility(
162         visible = visible,
163         enter = expandVertically(),
164         exit = shrinkVertically()
165     ) {
166         content()
167     }
168 }
169 
170 @Composable
PlayerPageTopAppBarnull171 private fun PlayerPageTopAppBar(
172     state: PlayerPageState,
173     composition: LottieComposition?,
174 ) {
175     val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
176 
177     TopAppBar(
178         title = {},
179         backgroundColor = Color.Transparent,
180         elevation = 0.dp,
181         navigationIcon = {
182             IconButton(
183                 onClick = { backPressedDispatcher?.onBackPressed() },
184             ) {
185                 Icon(
186                     Icons.Default.Close,
187                     contentDescription = null
188                 )
189             }
190         },
191         actions = {
192             if (composition?.warnings?.isNotEmpty() == true) {
193                 IconButton(
194                     onClick = { state.showWarningsDialog = true }
195                 ) {
196                     Icon(
197                         Icons.Filled.Warning,
198                         tint = Color.Black,
199                         contentDescription = null
200                     )
201                 }
202             }
203             IconButton(
204                 onClick = { state.focusMode = !state.focusMode },
205             ) {
206                 Icon(
207                     Icons.Filled.RemoveRedEye,
208                     tint = if (state.focusMode) Teal else Color.Black,
209                     contentDescription = null
210                 )
211             }
212         }
213     )
214 }
215 
216 @Composable
PlayerPageContentnull217 fun PlayerPageContent(
218     state: PlayerPageState,
219     composition: LottieComposition?,
220     isLoading: Boolean,
221     animationBackgroundColor: Color?,
222 ) {
223     LaunchedEffect(
224         composition,
225         state.shouldPlay,
226         state.targetSpeed,
227         state.shouldLoop,
228         state.progressSliderGesture,
229     ) {
230         composition ?: return@LaunchedEffect
231         state.progressSliderGesture?.let { p ->
232             state.animatable.snapTo(composition, p, resetLastFrameNanos = true)
233             return@LaunchedEffect
234         }
235         if (state.shouldPlay) {
236             if (!state.animatable.isPlaying && state.animatable.isAtEnd) {
237                 state.animatable.resetToBeginning()
238             }
239             state.animatable.animate(
240                 composition,
241                 iterations = if (state.shouldLoop) LottieConstants.IterateForever else 1,
242                 initialProgress = state.animatable.progress,
243                 speed = state.targetSpeed,
244                 continueFromPreviousAnimate = state.animatable.isPlaying,
245             )
246             state.shouldPlay = false
247         }
248     }
249 
250     Column(
251         verticalArrangement = Arrangement.SpaceBetween,
252         modifier = Modifier.fillMaxHeight()
253     ) {
254         Box(
255             contentAlignment = Alignment.Center,
256             modifier = Modifier
257                 .weight(1f)
258                 .maybeBackground(state.backgroundColor)
259                 .fillMaxWidth()
260         ) {
261             PlayerPageLottieAnimation(
262                 composition,
263                 { state.animatable.progress },
264                 modifier = Modifier
265                     // TODO: figure out how maxWidth can play nice with the aspectRatio modifier inside of LottieAnimation.
266                     .fillMaxWidth()
267                     .align(Alignment.Center)
268                     .maybeDrawBorder(state.borderToolbar)
269             )
270             if (isLoading) {
271                 DebouncedCircularProgressIndicator(
272                     color = Teal,
273                     modifier = Modifier
274                         .size(48.dp)
275                 )
276             }
277         }
278         ExpandVisibility(state.speedToolbar && !state.focusMode) {
279             SpeedToolbar(state)
280         }
281         ExpandVisibility(!state.focusMode && state.backgroundColorToolbar) {
282             BackgroundColorToolbar(
283                 animationBackgroundColor = animationBackgroundColor,
284                 onColorChanged = { state.backgroundColor = it }
285             )
286         }
287         ExpandVisibility(!state.focusMode) {
288             PlayerControlsRow(state, composition)
289         }
290         ExpandVisibility(!state.focusMode) {
291             Toolbar(state)
292         }
293     }
294 }
295 
296 @Composable
PlayerPageLottieAnimationnull297 private fun PlayerPageLottieAnimation(
298     composition: LottieComposition?,
299     progressProvider: () -> Float,
300     modifier: Modifier = Modifier,
301 ) {
302     LottieAnimation(
303         composition,
304         progressProvider,
305         modifier = modifier,
306     )
307 }
308 
309 @Composable
PlayerControlsRownull310 private fun PlayerControlsRow(
311     state: PlayerPageState,
312     composition: LottieComposition?,
313 ) {
314     val totalTime = (((composition?.duration ?: (0L / state.animatable.speed)) / 1000.0))
315     val totalTimeFormatted = ("%.1f").format(totalTime)
316 
317     val progressFormatted = ("%.1f").format(state.animatable.progress * totalTime)
318 
319     val frame = composition?.getFrameForProgress(state.animatable.progress)?.roundToInt() ?: 0
320     val durationFrames = ceil(composition?.durationFrames ?: 0f).roundToInt()
321     Box(
322         modifier = Modifier
323             .fillMaxWidth()
324     ) {
325         Row(
326             verticalAlignment = Alignment.CenterVertically,
327         ) {
328             Box(
329                 contentAlignment = Alignment.Center
330             ) {
331                 IconButton(
332                     onClick = { state.shouldPlay = !state.shouldPlay },
333                 ) {
334                     Icon(
335                         if (state.animatable.isPlaying) Icons.Filled.Pause
336                         else Icons.Filled.PlayArrow,
337                         contentDescription = null
338                     )
339                 }
340                 Text(
341                     "$frame/$durationFrames\n${progressFormatted}/$totalTimeFormatted",
342                     style = TextStyle(fontSize = 8.sp),
343                     textAlign = TextAlign.Center,
344                     modifier = Modifier
345                         .padding(top = 48.dp, bottom = 8.dp)
346                 )
347             }
348             Slider(
349                 value = state.progressSliderGesture ?: state.animatable.progress,
350                 onValueChange = { state.progressSliderGesture = it },
351                 onValueChangeFinished = { state.progressSliderGesture = null },
352                 modifier = Modifier.weight(1f)
353             )
354             IconButton(
355                 onClick = { state.shouldLoop = !state.shouldLoop },
356             ) {
357                 Icon(
358                     Icons.Filled.Repeat,
359                     tint = if (state.animatable.iterations == 1) Color.Black else Teal,
360                     contentDescription = null
361                 )
362             }
363         }
364         Text(
365             BuildConfig.VERSION_NAME,
366             fontSize = 6.sp,
367             color = Color.Gray,
368             modifier = Modifier
369                 .align(Alignment.BottomCenter)
370                 .padding(bottom = 12.dp)
371         )
372     }
373 }
374 
375 @Composable
SpeedToolbarnull376 private fun SpeedToolbar(state: PlayerPageState) {
377     Row(
378         horizontalArrangement = Arrangement.SpaceBetween,
379         modifier = Modifier
380             .drawBottomBorder()
381             .padding(vertical = 12.dp, horizontal = 16.dp)
382             .fillMaxWidth()
383     ) {
384         ToolbarChip(
385             label = "0.5x",
386             isActivated = state.animatable.speed == 0.5f,
387             onClick = { state.targetSpeed = 0.5f },
388             modifier = Modifier.padding(end = 8.dp)
389         )
390         ToolbarChip(
391             label = "1x",
392             isActivated = state.animatable.speed == 1f,
393             onClick = { state.targetSpeed = 1f },
394             modifier = Modifier.padding(end = 8.dp)
395         )
396         ToolbarChip(
397             label = "1.5x",
398             isActivated = state.animatable.speed == 1.5f,
399             onClick = { state.targetSpeed = 1.5f },
400             modifier = Modifier.padding(end = 8.dp)
401         )
402         ToolbarChip(
403             label = "2x",
404             isActivated = state.animatable.speed == 2f,
405             onClick = { state.targetSpeed = 2f },
406             modifier = Modifier.padding(end = 8.dp)
407         )
408     }
409 }
410 
411 @Composable
BackgroundColorToolbarnull412 private fun BackgroundColorToolbar(
413     animationBackgroundColor: Color?,
414     onColorChanged: (Color) -> Unit,
415 ) {
416     Row(
417         horizontalArrangement = Arrangement.SpaceBetween,
418         modifier = Modifier
419             .drawBottomBorder()
420             .padding(vertical = 12.dp, horizontal = 16.dp)
421             .fillMaxWidth()
422     ) {
423         listOfNotNull(
424             colorResource(R.color.background_color1),
425             colorResource(R.color.background_color2),
426             colorResource(R.color.background_color3),
427             colorResource(R.color.background_color4),
428             colorResource(R.color.background_color5),
429             colorResource(R.color.background_color6),
430             animationBackgroundColor.takeIf { it != Color.White },
431         ).forEachIndexed { i, color ->
432             val strokeColor = if (i == 0) colorResource(R.color.background_color1_stroke) else color
433             BackgroundToolbarItem(
434                 color = color,
435                 strokeColor = strokeColor,
436                 onClick = { onColorChanged(color) }
437             )
438         }
439     }
440 }
441 
442 @Composable
BackgroundToolbarItemnull443 private fun BackgroundToolbarItem(
444     color: Color,
445     strokeColor: Color = color,
446     onClick: () -> Unit,
447 ) {
448     Box(
449         modifier = Modifier
450             .clip(CircleShape)
451             .background(color)
452             .clickable(onClick = onClick)
453             .size(24.dp)
454             .border(1.dp, strokeColor, shape = CircleShape)
455     )
456 }
457 
458 @Composable
Toolbarnull459 private fun Toolbar(state: PlayerPageState) {
460     Row(
461         modifier = Modifier
462             .horizontalScroll(rememberScrollState())
463             .padding(horizontal = 8.dp)
464             .padding(bottom = 8.dp)
465     ) {
466         ToolbarChip(
467             iconPainter = painterResource(R.drawable.ic_masks_and_mattes),
468             label = stringResource(R.string.toolbar_item_masks),
469             isActivated = state.outlineMasksAndMattes,
470             onClick = { state.outlineMasksAndMattes = it },
471             modifier = Modifier.padding(end = 8.dp)
472         )
473         ToolbarChip(
474             iconPainter = painterResource(R.drawable.ic_layers),
475             label = stringResource(R.string.toolbar_item_opacity_layers),
476             isActivated = state.applyOpacityToLayers,
477             onClick = { state.applyOpacityToLayers = it },
478             modifier = Modifier.padding(end = 8.dp)
479         )
480         ToolbarChip(
481             iconPainter = painterResource(R.drawable.ic_color),
482             label = stringResource(R.string.toolbar_item_color),
483             isActivated = state.backgroundColorToolbar,
484             onClick = { state.backgroundColorToolbar = it },
485             modifier = Modifier.padding(end = 8.dp)
486         )
487         ToolbarChip(
488             iconPainter = painterResource(R.drawable.ic_speed),
489             label = stringResource(R.string.toolbar_item_speed),
490             isActivated = state.speedToolbar,
491             onClick = { state.speedToolbar = it },
492             modifier = Modifier.padding(end = 8.dp)
493         )
494         ToolbarChip(
495             iconPainter = painterResource(R.drawable.ic_border),
496             label = stringResource(R.string.toolbar_item_border),
497             isActivated = state.borderToolbar,
498             onClick = { state.borderToolbar = it },
499             modifier = Modifier.padding(end = 8.dp)
500         )
501         ToolbarChip(
502             iconPainter = rememberVectorPainter(Icons.Default.MergeType),
503             label = stringResource(R.string.toolbar_item_merge_paths),
504             isActivated = state.enableMergePaths,
505             onClick = { state.enableMergePaths = it },
506             modifier = Modifier.padding(end = 8.dp)
507         )
508     }
509 }
510 
511 @Composable
WarningDialognull512 fun WarningDialog(
513     warnings: List<String>,
514     onDismiss: () -> Unit,
515 ) {
516     Dialog(onDismissRequest = onDismiss) {
517         Surface(
518             shape = RoundedCornerShape(4.dp),
519             modifier = Modifier
520                 .width(400.dp)
521                 .heightIn(min = 32.dp, max = 500.dp)
522         ) {
523             Box(
524                 contentAlignment = Alignment.TopCenter,
525                 modifier = Modifier
526             ) {
527                 LazyColumn {
528                     itemsIndexed(warnings) { i, warning ->
529                         Text(
530                             warning,
531                             fontSize = 8.sp,
532                             textAlign = TextAlign.Left,
533                             modifier = Modifier
534                                 .fillMaxWidth()
535                                 .run { if (i != 0) drawBottomBorder() else this }
536                                 .padding(vertical = 12.dp, horizontal = 16.dp)
537                         )
538                     }
539                 }
540             }
541         }
542     }
543 }
544 
545 @Preview
546 @Composable
SpeedToolbarPreviewnull547 fun SpeedToolbarPreview() {
548     val state = remember { PlayerPageState(null) }
549     SpeedToolbar(state)
550 }
551 
552 @Preview(name = "Player")
553 @Composable
PlayerPagePreviewnull554 fun PlayerPagePreview() {
555     PlayerPage(LottieCompositionSpec.Url("https://lottiefiles.com/download/public/32922"))
556 }