blob: 270cde57d6f2a496f149ed3a2ac47b8ac4cdb2d8 [file] [log] [blame]
package com.airbnb.lottie.sample.compose.player
import android.os.Parcelable
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.foundation.Icon
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.ScrollableRow
import androidx.compose.foundation.Text
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
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.*
import com.airbnb.lottie.sample.compose.BackPressedDispatcherAmbient
import com.airbnb.lottie.sample.compose.ComposeFragment
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.ui.toColorSafe
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 com.airbnb.lottie.sample.compose.utils.quantityStringResource
import com.airbnb.mvrx.args
import kotlinx.android.parcel.Parcelize
import kotlin.math.ceil
import kotlin.math.roundToInt
class PlayerFragment : ComposeFragment() {
private val args: Args by args()
@Composable
override fun root() {
val spec = when (val a = args) {
is Args.Url -> LottieAnimationSpec.Url(a.url)
is Args.File -> LottieAnimationSpec.File(a.fileName)
is Args.Asset -> LottieAnimationSpec.Asset(a.assetName)
}
val backgroundColor = when (val a = args) {
is Args.Url -> a.backgroundColorStr?.toColorSafe()
else -> null
}
PlayerPage(spec, backgroundColor)
}
sealed class Args : Parcelable {
/** colorStr is the value from the LottieFiles API. */
@Parcelize
class Url(val url: String, val backgroundColorStr: String? = null) : Args()
@Parcelize
class File(val fileName: String) : Args()
@Parcelize
class Asset(val assetName: String) : Args()
}
}
@Composable
private 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()
var focusMode by remember { mutableStateOf(false) }
var backgroundColor by remember { mutableStateOf(animationBackgroundColor) }
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)
LaunchedTask(compositionResult) {
if (compositionResult is LottieCompositionResult.Fail) {
scaffoldState.snackbarHostState.showSnackbar(
message = failedMessage,
actionLabel = okMessage,
)
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {},
backgroundColor = Color.Transparent,
elevation = 0.dp,
navigationIcon = {
IconButton(
onClick = { backPressedDispatcher.onBackPressed() },
) {
Icon(Icons.Default.Close)
}
},
actions = {
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,
warnings = compositionResult()?.warnings ?: emptyList()
)
}
}
}
}
@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)
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,
)
}
}
}
@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>,
warnings: List<String>,
) {
var showWarningsDialog by remember { mutableStateOf(false) }
if (showWarningsDialog) {
WarningDialog(warnings = warnings, onDismiss = { showWarningsDialog = false })
}
ScrollableRow(
contentPadding = PaddingValues(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp),
modifier = Modifier
.drawTopBorder()
.fillMaxWidth()
) {
if (warnings.isNotEmpty()) {
ToolbarChip(
iconRes = R.drawable.ic_warning,
label = quantityStringResource(R.plurals.toolbar_item_warning, warnings.size, warnings.size),
isActivated = true,
onClick = { showWarningsDialog = true },
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)
)
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_color,
label = stringResource(R.string.toolbar_item_color),
isActivated = backgroundColor.value,
onClick = { backgroundColor.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"))
}
}