blob: b27fc0f3cc997550d2a3de9c2fe9674a5da6016e [file] [log] [blame]
package com.airbnb.lottie.sample.compose.player
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.ScrollableRow
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.RemoveRedEye
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Providers
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onCommit
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.ui.tooling.preview.Preview
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieAnimationSpec
import com.airbnb.lottie.compose.LottieAnimationState
import com.airbnb.lottie.compose.LottieCompositionResult
import com.airbnb.lottie.compose.rememberLottieAnimationState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.BackPressedDispatcherAmbient
import com.airbnb.lottie.sample.compose.BuildConfig
import com.airbnb.lottie.sample.compose.R
import com.airbnb.lottie.sample.compose.composables.DebouncedCircularProgressIndicator
import com.airbnb.lottie.sample.compose.composables.SeekBar
import com.airbnb.lottie.sample.compose.ui.Teal
import com.airbnb.lottie.sample.compose.utils.drawTopBorder
import com.airbnb.lottie.sample.compose.utils.maybeBackground
import com.airbnb.lottie.sample.compose.utils.maybeDrawBorder
import kotlin.math.ceil
import kotlin.math.roundToInt
@Composable
fun PlayerPage(
spec: LottieAnimationSpec,
animationBackgroundColor: Color? = null,
) {
val backPressedDispatcher = BackPressedDispatcherAmbient.current
val compositionResult = rememberLottieComposition(spec)
val animationState = rememberLottieAnimationState(autoPlay = true, repeatCount = Integer.MAX_VALUE)
val scaffoldState = rememberScaffoldState()
val outlineMasksAndMattes = remember { mutableStateOf(false) }
val applyOpacityToLayers = remember { mutableStateOf(false) }
var focusMode by remember { mutableStateOf(false) }
var backgroundColor by remember { mutableStateOf(animationBackgroundColor) }
var showWarningsDialog by remember { mutableStateOf(false) }
val borderToolbar = remember { mutableStateOf(false) }
val speedToolbar = remember { mutableStateOf(false) }
val backgroundColorToolbar = remember { mutableStateOf(false) }
val failedMessage = stringResource(R.string.failed_to_load)
val okMessage = stringResource(R.string.ok)
LaunchedEffect(compositionResult) {
if (compositionResult is LottieCompositionResult.Fail) {
scaffoldState.snackbarHostState.showSnackbar(
message = failedMessage,
actionLabel = okMessage,
)
}
}
onCommit(outlineMasksAndMattes.value) {
animationState.outlineMasksAndMattes = outlineMasksAndMattes.value
}
onCommit(applyOpacityToLayers.value) {
animationState.applyOpacityToLayers = applyOpacityToLayers.value
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {},
backgroundColor = Color.Transparent,
elevation = 0.dp,
navigationIcon = {
IconButton(
onClick = { backPressedDispatcher.onBackPressed() },
) {
Icon(Icons.Default.Close)
}
},
actions = {
if (compositionResult()?.warnings?.isNotEmpty() == true) {
IconButton(
onClick = { showWarningsDialog = true }
) {
Icon(
Icons.Filled.Warning,
tint = Color.Black,
)
}
}
IconButton(
onClick = { focusMode = !focusMode },
) {
Icon(
Icons.Filled.RemoveRedEye,
tint = if (focusMode) Teal else Color.Black,
)
}
}
)
},
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxHeight()
) {
Box(
alignment = Alignment.Center,
modifier = Modifier
.weight(1f)
.maybeBackground(backgroundColor)
.fillMaxWidth()
) {
LottieAnimation(
compositionResult,
animationState,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
.maybeDrawBorder(borderToolbar.value)
)
if (compositionResult is LottieCompositionResult.Loading) {
DebouncedCircularProgressIndicator(
color = Teal,
modifier = Modifier
.preferredSize(48.dp)
)
}
}
if (speedToolbar.value && !focusMode) {
SpeedToolbar(
speed = animationState.speed,
onSpeedChanged = { animationState.speed = it }
)
}
if (backgroundColorToolbar.value && !focusMode) {
BackgroundColorToolbar(
animationBackgroundColor = animationBackgroundColor,
onColorChanged = { backgroundColor = it }
)
}
if (!focusMode) {
PlayerControlsRow(animationState, compositionResult())
Toolbar(
border = borderToolbar,
speed = speedToolbar,
backgroundColor = backgroundColorToolbar,
outlineMasksAndMattes = outlineMasksAndMattes,
applyOpacityToLayers = applyOpacityToLayers,
)
}
}
}
if (showWarningsDialog) {
WarningDialog(warnings = compositionResult()?.warnings ?: emptyList(), onDismiss = { showWarningsDialog = false })
}
}
@Composable
private fun PlayerControlsRow(
animationState: LottieAnimationState,
composition: LottieComposition?,
) {
val totalTime = ((composition?.duration ?: 0L / animationState.speed) / 1000.0)
val totalTimeFormatted = ("%.1f").format(totalTime)
val progress = (totalTime / 100.0) * ((animationState.progress * 100.0).roundToInt())
val progressFormatted = ("%.1f").format(progress)
Box(
modifier = Modifier
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.drawTopBorder()
) {
Box(
alignment = Alignment.Center
) {
IconButton(
onClick = { animationState.toggleIsPlaying() },
) {
Icon(if (animationState.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow)
}
Text(
"${animationState.frame}/${ceil(composition?.durationFrames ?: 0f).toInt()}\n${progressFormatted}/$totalTimeFormatted",
style = TextStyle(fontSize = 8.sp),
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 48.dp, bottom = 8.dp)
)
}
SeekBar(
progress = animationState.progress,
onProgressChanged = {
animationState.progress = it
},
modifier = Modifier.weight(1f)
)
IconButton(onClick = {
val repeatCount = if (animationState.repeatCount == Integer.MAX_VALUE) 0 else Integer.MAX_VALUE
animationState.repeatCount = repeatCount
}) {
Icon(
Icons.Filled.Repeat,
tint = if (animationState.repeatCount > 0) Teal else Color.Black,
)
}
}
Text(
BuildConfig.VERSION_NAME,
fontSize = 6.sp,
color = Color.Gray,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 12.dp)
)
}
}
@Composable
private fun SpeedToolbar(
speed: Float,
onSpeedChanged: (Float) -> Unit,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.drawTopBorder()
.padding(vertical = 12.dp, horizontal = 16.dp)
.fillMaxWidth()
) {
ToolbarChip(
label = "0.5x",
isActivated = speed == 0.5f,
onClick = { onSpeedChanged(0.5f) },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
label = "1x",
isActivated = speed == 1f,
onClick = { onSpeedChanged(1f) },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
label = "1.5x",
isActivated = speed == 1.5f,
onClick = { onSpeedChanged(1.5f) },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
label = "2x",
isActivated = speed == 2f,
onClick = { onSpeedChanged(2f) },
modifier = Modifier.padding(end = 8.dp)
)
}
}
@Composable
private fun BackgroundColorToolbar(
animationBackgroundColor: Color?,
onColorChanged: (Color) -> Unit,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.drawTopBorder()
.padding(vertical = 12.dp, horizontal = 16.dp)
.fillMaxWidth()
) {
listOfNotNull(
colorResource(R.color.background_color1),
colorResource(R.color.background_color2),
colorResource(R.color.background_color3),
colorResource(R.color.background_color4),
colorResource(R.color.background_color5),
colorResource(R.color.background_color6),
animationBackgroundColor.takeIf { it != Color.White },
).forEachIndexed { i, color ->
val strokeColor = if (i == 0) colorResource(R.color.background_color1_stroke) else color
BackgroundToolbarItem(
color = color,
strokeColor = strokeColor,
onClick = { onColorChanged(color) }
)
}
}
}
@Composable
private fun BackgroundToolbarItem(
color: Color,
strokeColor: Color = color,
onClick: () -> Unit,
) {
Box(
modifier = Modifier
.clip(CircleShape)
.background(color)
.clickable(onClick = onClick)
.preferredSize(24.dp)
.border(1.dp, strokeColor, shape = CircleShape)
)
}
@Composable
private fun Toolbar(
border: MutableState<Boolean>,
speed: MutableState<Boolean>,
backgroundColor: MutableState<Boolean>,
outlineMasksAndMattes: MutableState<Boolean>,
applyOpacityToLayers: MutableState<Boolean>,
) {
ScrollableRow(
contentPadding = PaddingValues(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp),
modifier = Modifier
.drawTopBorder()
.fillMaxWidth()
) {
ToolbarChip(
iconRes = R.drawable.ic_masks_and_mattes,
label = stringResource(R.string.toolbar_item_masks),
isActivated = outlineMasksAndMattes.value,
onClick = { outlineMasksAndMattes.value = it },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
iconRes = R.drawable.ic_layers,
label = stringResource(R.string.toolbar_item_opacity_layers),
isActivated = applyOpacityToLayers.value,
onClick = { applyOpacityToLayers.value = it },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
iconRes = R.drawable.ic_color,
label = stringResource(R.string.toolbar_item_color),
isActivated = backgroundColor.value,
onClick = { backgroundColor.value = it },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
iconRes = R.drawable.ic_speed,
label = stringResource(R.string.toolbar_item_speed),
isActivated = speed.value,
onClick = { speed.value = it },
modifier = Modifier.padding(end = 8.dp)
)
ToolbarChip(
iconRes = R.drawable.ic_border,
label = stringResource(R.string.toolbar_item_border),
isActivated = border.value,
onClick = { border.value = it },
modifier = Modifier.padding(end = 8.dp)
)
}
}
@Composable
fun WarningDialog(
warnings: List<String>,
onDismiss: () -> Unit,
) {
Dialog(onDismissRequest = onDismiss) {
Surface(
shape = RoundedCornerShape(4.dp),
modifier = Modifier
.preferredWidth(400.dp)
.heightIn(min = 32.dp, max = 500.dp)
) {
Box(
alignment = Alignment.TopCenter,
modifier = Modifier
) {
ScrollableColumn {
warnings.forEachIndexed { i, warning ->
Text(
warning,
fontSize = 8.sp,
textAlign = TextAlign.Left,
modifier = Modifier
.fillMaxWidth()
.run { if (i != 0) drawTopBorder() else this }
.padding(vertical = 12.dp, horizontal = 16.dp)
)
}
}
}
}
}
}
@Preview
@Composable
fun SpeedToolbarPreview() {
SpeedToolbar(speed = 1f, onSpeedChanged = {})
}
@Preview(name = "Player")
@Composable
fun PlayerPagePreview() {
Providers(
BackPressedDispatcherAmbient provides OnBackPressedDispatcher()
) {
PlayerPage(LottieAnimationSpec.Url("https://lottiefiles.com/download/public/32922"))
}
}