<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 }