Last active
July 19, 2024 11:57
-
-
Save JunkFood02/caf2af3cee41f847c0ad0bcf4d0cf9d8 to your computer and use it in GitHub Desktop.
A music player demo made with Jetpack Compose animation APIs, including shared element transition, list animations, animated content, etc.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.compose_debug | |
import androidx.compose.animation.EnterTransition | |
import androidx.compose.animation.ExitTransition | |
import androidx.compose.animation.core.CubicBezierEasing | |
import androidx.compose.animation.core.Easing | |
import androidx.compose.animation.core.FastOutLinearInEasing | |
import androidx.compose.animation.core.FastOutSlowInEasing | |
import androidx.compose.animation.core.LinearOutSlowInEasing | |
import androidx.compose.animation.core.PathEasing | |
import androidx.compose.animation.core.tween | |
import androidx.compose.animation.fadeIn | |
import androidx.compose.animation.fadeOut | |
import androidx.compose.animation.slideInHorizontally | |
import androidx.compose.animation.slideOutHorizontally | |
import androidx.compose.ui.graphics.Path | |
// Material 3 Emphasized Easing | |
// https://m3.material.io/styles/motion/easing-and-duration/tokens-specs | |
const val DURATION = 600 | |
const val DURATION_ENTER = 400 | |
const val DURATION_ENTER_SHORT = 300 | |
const val DURATION_EXIT = 200 | |
const val DURATION_EXIT_SHORT = 100 | |
private val emphasizedPath = Path().apply { | |
moveTo(0f, 0f) | |
cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f) | |
cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f) | |
} | |
val EmphasizedEasing: Easing = PathEasing(emphasizedPath) | |
val EmphasizedDecelerateEasing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1f) | |
val EmphasizedAccelerateEasing = CubicBezierEasing(0.3f, 0f, 0.8f, 0.15f) | |
public fun materialSharedAxisXIn( | |
initialOffsetX: (fullWidth: Int) -> Int, | |
durationMillis: Int = DURATION_ENTER_SHORT, | |
): EnterTransition = slideInHorizontally( | |
animationSpec = tween( | |
durationMillis = durationMillis, | |
easing = FastOutSlowInEasing | |
), | |
initialOffsetX = initialOffsetX | |
) + fadeIn( | |
animationSpec = tween( | |
durationMillis = durationMillis.ForIncoming, | |
delayMillis = durationMillis.ForOutgoing, | |
easing = LinearOutSlowInEasing | |
) | |
) | |
public fun materialSharedAxisXOut( | |
targetOffsetX: (fullWidth: Int) -> Int, | |
durationMillis: Int = DURATION_ENTER_SHORT, | |
): ExitTransition = slideOutHorizontally( | |
animationSpec = tween( | |
durationMillis = durationMillis, | |
easing = FastOutSlowInEasing | |
), | |
targetOffsetX = targetOffsetX | |
) + fadeOut( | |
animationSpec = tween( | |
durationMillis = durationMillis.ForOutgoing, | |
delayMillis = 0, | |
easing = FastOutLinearInEasing | |
) | |
) | |
private const val ProgressThreshold = 0.35f | |
private val Int.ForOutgoing: Int | |
get() = (this * ProgressThreshold).toInt() | |
private val Int.ForIncoming: Int | |
get() = this - this.ForOutgoing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:OptIn( | |
ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class, | |
ExperimentalAnimationApi::class | |
) | |
package com.example.compose_debug | |
import android.content.res.Configuration | |
import androidx.activity.compose.BackHandler | |
import androidx.compose.animation.AnimatedContent | |
import androidx.compose.animation.BoundsTransform | |
import androidx.compose.animation.ContentTransform | |
import androidx.compose.animation.Crossfade | |
import androidx.compose.animation.ExperimentalAnimationApi | |
import androidx.compose.animation.ExperimentalSharedTransitionApi | |
import androidx.compose.animation.SharedTransitionLayout | |
import androidx.compose.animation.SharedTransitionScope | |
import androidx.compose.animation.SizeTransform | |
import androidx.compose.animation.core.DeferredTargetAnimation | |
import androidx.compose.animation.core.ExperimentalAnimatableApi | |
import androidx.compose.animation.core.MutableTransitionState | |
import androidx.compose.animation.core.Spring | |
import androidx.compose.animation.core.VectorConverter | |
import androidx.compose.animation.core.rememberTransition | |
import androidx.compose.animation.core.spring | |
import androidx.compose.animation.core.tween | |
import androidx.compose.animation.core.updateTransition | |
import androidx.compose.animation.fadeIn | |
import androidx.compose.animation.fadeOut | |
import androidx.compose.animation.togetherWith | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.border | |
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.interaction.MutableInteractionSource | |
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.Spacer | |
import androidx.compose.foundation.layout.WindowInsets | |
import androidx.compose.foundation.layout.asPaddingValues | |
import androidx.compose.foundation.layout.fillMaxHeight | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.navigationBars | |
import androidx.compose.foundation.layout.navigationBarsPadding | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.layout.statusBarsPadding | |
import androidx.compose.foundation.layout.systemBarsPadding | |
import androidx.compose.foundation.layout.width | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.LazyItemScope | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.foundation.shape.CircleShape | |
import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.outlined.MoreHoriz | |
import androidx.compose.material.icons.outlined.MoreVert | |
import androidx.compose.material.icons.outlined.Repeat | |
import androidx.compose.material.icons.rounded.ArrowBackIosNew | |
import androidx.compose.material.icons.rounded.FastForward | |
import androidx.compose.material.icons.rounded.MoreVert | |
import androidx.compose.material.icons.rounded.PlayArrow | |
import androidx.compose.material.icons.rounded.Repeat | |
import androidx.compose.material.icons.rounded.Shuffle | |
import androidx.compose.material3.ExperimentalMaterial3Api | |
import androidx.compose.material3.FilledIconToggleButton | |
import androidx.compose.material3.FilledTonalButton | |
import androidx.compose.material3.FilledTonalIconButton | |
import androidx.compose.material3.FilledTonalIconToggleButton | |
import androidx.compose.material3.Icon | |
import androidx.compose.material3.IconButton | |
import androidx.compose.material3.IconButtonDefaults | |
import androidx.compose.material3.IconToggleButton | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Slider | |
import androidx.compose.material3.SliderDefaults | |
import androidx.compose.material3.SliderState | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableIntStateOf | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.composed | |
import androidx.compose.ui.draw.clip | |
import androidx.compose.ui.draw.rotate | |
import androidx.compose.ui.layout.approachLayout | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.platform.LocalInspectionMode | |
import androidx.compose.ui.res.painterResource | |
import androidx.compose.ui.text.font.FontWeight | |
import androidx.compose.ui.text.style.TextOverflow | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.Constraints | |
import androidx.compose.ui.unit.DpSize | |
import androidx.compose.ui.unit.IntSize | |
import androidx.compose.ui.unit.dp | |
import com.example.compose_debug.ui.theme.ComposeDebugTheme | |
data class Track( | |
val id: Int, | |
val title: String, | |
val artist: String, | |
val resId: Int, | |
val albumTitle: String, | |
) | |
val tracks = listOf( | |
"Without You Without Them", | |
"$20", | |
"Emily I'm Sorry", | |
"True Blue", | |
"Cool About It", | |
"Not Strong Enough", | |
"Revolution 0", | |
"Leonard Cohen", | |
"Satanist", | |
"We're In Love", | |
"Anti-Curse", | |
"Letter To An Old Poet", | |
) | |
val tracks2 = listOf( | |
"Dynasty", | |
"XS", | |
"STFU!", | |
"Comme Des Garçons (Like The Boys)", | |
"Akasaka Sad", | |
"Paradisin'", | |
"Love Me 4 Me", | |
"Bad Friend", | |
"Fuck This World (Interlude)", | |
"Who's Gonna Save U Now?", | |
"Tokyo Love Hotel", | |
"Chosen Family", | |
"Snakeskin" | |
) | |
val TrackList = buildList { | |
var index = 0 | |
tracks.forEach { | |
add( | |
Track( | |
id = index, | |
title = it, | |
artist = "boygenius", | |
albumTitle = "the record", | |
resId = R.drawable.artwork1 | |
) | |
) | |
index++ | |
} | |
tracks2.forEach { | |
add( | |
Track( | |
id = index, | |
title = it, | |
artist = "Rina Sawayama", | |
albumTitle = "SAWAYAMA", | |
resId = R.drawable.artwork2 | |
) | |
) | |
index++ | |
} | |
} | |
private val SmallArtSize = 48.dp | |
private val MediumArtSize = 72.dp | |
private val HorizontalPaddingDp = 16.dp | |
private val AlbumArtBoundsTransform = BoundsTransform { _, _ -> | |
tween(easing = EmphasizedEasing, durationMillis = DURATION) | |
} | |
private fun <T> tweenEnter( | |
delayMillis: Int = DURATION_EXIT, | |
durationMillis: Int = DURATION_ENTER | |
) = | |
tween<T>( | |
delayMillis = delayMillis, | |
durationMillis = durationMillis, | |
easing = EmphasizedDecelerateEasing | |
) | |
private fun <T> tweenExit( | |
durationMillis: Int = DURATION_EXIT_SHORT, | |
) = tween<T>( | |
durationMillis = durationMillis, | |
easing = EmphasizedAccelerateEasing | |
) | |
const val FULL_PLAYER = 0 | |
const val PLAY_QUEUE = 1 | |
const val MINI_PLAYER = 2 | |
@Composable | |
@Preview | |
fun PlayerTransformDemo() { | |
var show by remember { | |
mutableIntStateOf(FULL_PLAYER) | |
} | |
var nowPlaying by remember { | |
mutableStateOf(TrackList[0]) | |
} | |
val playNextList = TrackList.subList(TrackList.indexOf(nowPlaying) + 1, TrackList.size) | |
val offset = with(LocalDensity.current) { (MediumArtSize.toPx()).toInt() } | |
ComposeDebugTheme { | |
SharedTransitionLayout( | |
modifier = Modifier | |
.background(MaterialTheme.colorScheme.surfaceContainer) | |
) { | |
AnimatedContent(targetState = show, label = "", transitionSpec = { | |
fadeIn( | |
tweenEnter(delayMillis = DURATION_EXIT_SHORT) | |
) togetherWith fadeOut( | |
tweenExit(durationMillis = DURATION_EXIT_SHORT) | |
) | |
}) { | |
when (it) { | |
PLAY_QUEUE -> { | |
PlayerQueue( | |
imageModifier = { track -> | |
Modifier.sharedElement( | |
state = rememberSharedContentState( | |
key = track | |
), | |
boundsTransform = AlbumArtBoundsTransform, | |
animatedVisibilityScope = this, | |
placeHolderSize = { contentSize: IntSize, animatedSize: IntSize -> | |
IntSize(contentSize.width, animatedSize.height) | |
}, | |
) | |
}, | |
textModifier = Modifier, | |
nowPlaying = nowPlaying, | |
playNextList = playNextList, | |
onBackPressed = { show = FULL_PLAYER } | |
) { track -> | |
nowPlaying = track | |
} | |
} | |
FULL_PLAYER -> { | |
PlayerView( | |
nowPlaying = nowPlaying, | |
imageModifier = Modifier.sharedElement( | |
state = rememberSharedContentState( | |
key = nowPlaying | |
), | |
animatedVisibilityScope = this, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize, | |
boundsTransform = AlbumArtBoundsTransform, | |
), | |
containerModifier = Modifier.sharedBounds( | |
sharedContentState = rememberSharedContentState( | |
key = "container" | |
), | |
animatedVisibilityScope = this, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.animatedSize, | |
boundsTransform = AlbumArtBoundsTransform, | |
enter = fadeIn( | |
tweenEnter(delayMillis = DURATION_EXIT_SHORT) | |
), | |
exit = fadeOut( | |
tweenExit(durationMillis = DURATION_EXIT_SHORT) | |
) | |
), | |
onBackPressed = { | |
show = MINI_PLAYER | |
} | |
) { | |
show = PLAY_QUEUE | |
} | |
} | |
else -> { | |
MiniPlayer( | |
nowPlaying = nowPlaying, | |
onNextClicked = { | |
nowPlaying = playNextList.firstOrNull() ?: TrackList.first() | |
}, | |
onClick = { show = FULL_PLAYER }, | |
imageModifier = Modifier.sharedElement( | |
state = rememberSharedContentState( | |
key = nowPlaying | |
), | |
animatedVisibilityScope = this, | |
placeHolderSize = { contentSize: IntSize, animatedSize: IntSize -> | |
IntSize(contentSize.width, animatedSize.height) | |
}, | |
boundsTransform = AlbumArtBoundsTransform, | |
), | |
containerModifier = Modifier.sharedBounds( | |
sharedContentState = rememberSharedContentState( | |
key = "container" | |
), | |
animatedVisibilityScope = this, | |
placeHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize, | |
boundsTransform = AlbumArtBoundsTransform, | |
enter = fadeIn( | |
tweenEnter(delayMillis = DURATION_EXIT) | |
), | |
exit = fadeOut( | |
tweenExit(durationMillis = DURATION_EXIT_SHORT) | |
) | |
) | |
) | |
} | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
fun MiniPlayer( | |
nowPlaying: Track, | |
imageModifier: Modifier = Modifier, | |
containerModifier: Modifier = Modifier, | |
onNextClicked: () -> Unit = {}, | |
onClick: () -> Unit = {}, | |
) { | |
val transitionState = remember { MutableTransitionState(nowPlaying) } | |
LaunchedEffect(nowPlaying) { | |
transitionState.targetState = nowPlaying | |
} | |
val transition = rememberTransition(transitionState = transitionState) | |
val albumArtRedId = remember(transitionState.isIdle) { | |
transitionState.currentState.resId | |
} | |
Box( | |
modifier = Modifier | |
.fillMaxWidth() | |
.fillMaxHeight() | |
) { | |
Surface( | |
modifier = containerModifier | |
.align(Alignment.BottomCenter) | |
.fillMaxWidth(), | |
onClick = onClick, | |
color = MaterialTheme.colorScheme.surfaceContainerHigh | |
) { | |
Row( | |
modifier = Modifier | |
.padding(vertical = 12.dp) | |
.navigationBarsPadding(), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Spacer(modifier = Modifier.width(12.dp)) | |
Image( | |
painter = painterResource(id = albumArtRedId), | |
contentDescription = null, | |
modifier = imageModifier | |
.size(SmallArtSize) | |
.clip(MaterialTheme.shapes.small), | |
) | |
Spacer(modifier = Modifier.width(8.dp)) | |
Row( | |
modifier = Modifier.height(48.dp), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
transition.AnimatedContent( | |
modifier = Modifier.weight(1f), | |
transitionSpec = { | |
ContentTransform( | |
materialSharedAxisXIn(initialOffsetX = { it / 10 }), | |
materialSharedAxisXOut(targetOffsetX = { -it / 10 }), | |
sizeTransform = SizeTransform(clip = true) | |
) | |
} | |
) { | |
Text( | |
text = it.title, | |
style = MaterialTheme.typography.titleMedium, | |
maxLines = 1, | |
overflow = TextOverflow.Ellipsis, | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(horizontal = 8.dp) | |
) | |
} | |
FilledTonalIconButton(onClick = { /*TODO*/ }) { | |
Icon( | |
imageVector = Icons.Rounded.PlayArrow, | |
contentDescription = null, | |
modifier = Modifier.size(24.dp), | |
) | |
} | |
IconButton(onClick = onNextClicked) { | |
Icon(imageVector = Icons.Rounded.FastForward, contentDescription = null) | |
} | |
} | |
Spacer(modifier = Modifier.width(8.dp)) | |
} | |
} | |
} | |
} | |
@Composable | |
fun PlayerView( | |
nowPlaying: Track, | |
containerModifier: Modifier = Modifier, | |
imageModifier: Modifier = Modifier, | |
onBackPressed: () -> Unit = {}, | |
onClick: () -> Unit = {} | |
) { | |
BackHandler { | |
onBackPressed() | |
} | |
Surface( | |
modifier = containerModifier.fillMaxHeight(), | |
color = MaterialTheme.colorScheme.surfaceContainer, | |
onClick = onClick | |
) { | |
Column( | |
modifier = Modifier.statusBarsPadding() | |
) { | |
Row( | |
modifier = Modifier | |
.padding(horizontal = 12.dp) | |
.padding(top = 12.dp, bottom = 12.dp) | |
) { | |
IconButton(onClick = onBackPressed) { | |
Icon( | |
imageVector = Icons.Rounded.ArrowBackIosNew, | |
contentDescription = null, | |
modifier = Modifier.rotate(-90f) | |
) | |
} | |
Spacer(modifier = Modifier.weight(1f)) | |
IconButton(onClick = onClick) { | |
Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = null) | |
} | |
} | |
Image( | |
painter = painterResource(id = nowPlaying.resId), | |
contentDescription = null, | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(horizontal = 24.dp, vertical = 16.dp) | |
.then(imageModifier) | |
.clip(MaterialTheme.shapes.small) | |
) | |
Spacer(modifier = Modifier.height(24.dp)) | |
Column(modifier = Modifier.padding(horizontal = 24.dp)) { | |
Text( | |
text = nowPlaying.title, | |
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Medium) | |
) | |
Text( | |
text = nowPlaying.artist, | |
style = MaterialTheme.typography.bodyLarge, | |
) | |
} | |
Column(modifier = Modifier.padding(horizontal = 22.dp)) { | |
val sliderState = remember { | |
SliderState(0.73f) | |
} | |
val interactionSource = remember { | |
MutableInteractionSource() | |
} | |
val colors = SliderDefaults.colors() | |
Spacer(modifier = Modifier.height(16.dp)) | |
Slider( | |
modifier = Modifier.height(20.dp), | |
state = sliderState, | |
colors = colors, | |
track = { | |
SliderDefaults.Track( | |
sliderState = sliderState, | |
drawStopIndicator = null, | |
thumbTrackGapSize = 4.dp, | |
modifier = Modifier.height(8.dp) | |
) | |
}, | |
thumb = { | |
SliderDefaults.Thumb( | |
interactionSource = interactionSource, | |
thumbSize = DpSize(width = 4.dp, height = 20.dp) | |
) | |
}, interactionSource = interactionSource | |
) | |
Row(modifier = Modifier.padding(horizontal = 2.dp)) { | |
Text( | |
text = "00:59", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
Spacer(modifier = Modifier.weight(1f)) | |
Text( | |
text = "01:21", | |
style = MaterialTheme.typography.bodySmall, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
} | |
} | |
} | |
} | |
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) | |
@Composable | |
private fun PlayerPreview() { | |
ComposeDebugTheme { | |
PlayerView(nowPlaying = TrackList[0]) | |
} | |
} | |
@Composable | |
fun PlayerQueue( | |
imageModifier: @Composable (Track) -> Modifier = { Modifier }, | |
textModifier: Modifier = Modifier, | |
nowPlaying: Track, | |
playNextList: List<Track>, | |
playQueueSource: String = "random playlist", | |
onBackPressed: () -> Unit = {}, | |
onClick: (Track) -> Unit, | |
) { | |
BackHandler { | |
onBackPressed() | |
} | |
Surface( | |
color = MaterialTheme.colorScheme.surfaceContainer, | |
modifier = Modifier.fillMaxHeight(), | |
) { | |
Column(modifier = Modifier | |
.clickable { onBackPressed() } | |
.statusBarsPadding() | |
) { | |
Text( | |
text = "Now playing", | |
style = MaterialTheme.typography.titleMedium, | |
modifier = Modifier | |
.padding(top = 24.dp) | |
.padding(horizontal = HorizontalPaddingDp) | |
.padding(bottom = 16.dp) | |
) | |
Row( | |
verticalAlignment = Alignment.CenterVertically, | |
modifier = Modifier.padding(start = HorizontalPaddingDp) | |
) { | |
Image( | |
painter = painterResource(id = nowPlaying.resId), | |
contentDescription = null, | |
modifier = Modifier | |
.then(imageModifier(nowPlaying)) | |
.size(MediumArtSize) | |
.clip(MaterialTheme.shapes.small) | |
) | |
Spacer(modifier = Modifier.width(16.dp)) | |
Column(modifier = textModifier.weight(1f)) { | |
Text(text = nowPlaying.title, style = MaterialTheme.typography.titleMedium) | |
Text( | |
text = nowPlaying.artist, | |
style = MaterialTheme.typography.bodyMedium, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
IconButton(onClick = { /*TODO*/ }) { | |
Icon(imageVector = Icons.Outlined.MoreVert, contentDescription = null) | |
} | |
Spacer(modifier = Modifier.width(8.dp)) | |
} | |
Row( | |
modifier = Modifier | |
.padding(top = 24.dp, bottom = 4.dp) | |
.padding(horizontal = HorizontalPaddingDp), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Text( | |
text = "Playing from %s".format(playQueueSource), | |
style = MaterialTheme.typography.titleMedium, | |
modifier = Modifier | |
.weight(1f) | |
) | |
IconToggleButton( | |
checked = false, | |
onCheckedChange = {}, | |
modifier = Modifier.size(32.dp), | |
colors = IconButtonDefaults.iconToggleButtonColors( | |
checkedContainerColor = MaterialTheme.colorScheme.onSurfaceVariant, | |
checkedContentColor = MaterialTheme.colorScheme.surface | |
) | |
) { | |
Icon( | |
imageVector = Icons.Rounded.Shuffle, | |
contentDescription = null, | |
modifier = Modifier.size(20.dp) | |
) | |
} | |
Spacer(modifier = Modifier.width(8.dp)) | |
IconToggleButton( | |
checked = true, | |
onCheckedChange = {}, | |
modifier = Modifier.size(32.dp), | |
colors = IconButtonDefaults.filledIconToggleButtonColors( | |
checkedContainerColor = MaterialTheme.colorScheme.onSurfaceVariant, | |
checkedContentColor = MaterialTheme.colorScheme.surface | |
) | |
) { | |
Icon( | |
imageVector = Icons.Rounded.Repeat, | |
contentDescription = null, | |
modifier = Modifier.size(20.dp) | |
) | |
} | |
} | |
LazyColumn( | |
modifier = Modifier | |
.fillMaxHeight(), | |
contentPadding = WindowInsets.navigationBars.asPaddingValues(), | |
) { | |
items(items = playNextList, key = { it.id }) { | |
Row( | |
verticalAlignment = Alignment.CenterVertically, | |
modifier = Modifier | |
.fillMaxWidth() | |
.animateItemPreview() | |
.clickable { onClick(it) } | |
.padding(vertical = 12.dp) | |
) { | |
Spacer(modifier = Modifier.width(HorizontalPaddingDp)) | |
Image( | |
painter = painterResource(id = it.resId), | |
contentDescription = null, | |
modifier = Modifier | |
.size(SmallArtSize) | |
.clip(MaterialTheme.shapes.small) | |
) | |
Spacer(modifier = Modifier.width(16.dp)) | |
Column { | |
Text( | |
text = it.title, | |
style = MaterialTheme.typography.titleSmall | |
) | |
Text( | |
text = it.artist, | |
style = MaterialTheme.typography.bodyMedium, | |
color = MaterialTheme.colorScheme.onSurfaceVariant | |
) | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) | |
@Composable | |
private fun QueuePreview() { | |
ComposeDebugTheme { | |
PlayerQueue( | |
nowPlaying = TrackList[0], | |
playNextList = TrackList.subList(1, TrackList.size), | |
) {} | |
} | |
} | |
context (LazyItemScope) | |
fun Modifier.animateItemPreview() = composed { | |
if (!LocalInspectionMode.current) | |
Modifier.animateItem( | |
fadeInSpec = tween(durationMillis = DURATION_ENTER_SHORT), | |
fadeOutSpec = tween(durationMillis = DURATION_EXIT_SHORT) | |
) | |
else this | |
} | |
@Preview | |
@Composable | |
private fun MiniPlayerPreview() { | |
var index by remember { mutableIntStateOf(0) } | |
MiniPlayer(nowPlaying = TrackList[index], onNextClicked = { index++ }) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment