feat: new launcher icon + motion polish (adjacent-tab glide, animateItem rows, header crossfade, cheaper pressDip)
BIN
Satsamp Icon.png
Normal file
|
After Width: | Height: | Size: 960 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
package com.rada.ui.library
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -284,14 +285,18 @@ fun LibraryScreen(
|
|||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 4.dp, bottom = 2.dp),
|
||||
) {
|
||||
// Crossfade the header when the selected folder changes — a
|
||||
// hard text swap reads as a glitch when tabs are tapped.
|
||||
Crossfade(targetState = headerTitle, label = "libraryHeader") { t ->
|
||||
Text(
|
||||
text = headerTitle,
|
||||
text = t,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = LocalTokens.current.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "${uiState.songs.size} songs",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
|
|
@ -477,8 +482,12 @@ fun LibraryScreen(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(uiState.songs, key = { it.id }) { song ->
|
||||
// Keyed rows animate in/out when the folder tab or
|
||||
// search filter changes instead of popping.
|
||||
val rowMod = if (LocalReduceMotion.current) Modifier else Modifier.animateItem()
|
||||
SongListItem(
|
||||
song = song,
|
||||
modifier = rowMod,
|
||||
isPlaying = song.id == uiState.currentPlayingId,
|
||||
onClick = { viewModel.playSong(song) },
|
||||
onToggleFavorite = { viewModel.toggleFavorite(song.id) },
|
||||
|
|
@ -768,10 +777,11 @@ private fun SongListItem(
|
|||
onToggleFavorite: () -> Unit,
|
||||
onAddToPlaylist: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.pressDip(interaction, LocalReduceMotion.current)
|
||||
.clickable(interactionSource = interaction, indication = androidx.compose.foundation.LocalIndication.current, onClick = onClick)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.rada.ui.player.NowPlayingBar
|
|||
import com.rada.ui.playlists.PlaylistManagerScreen
|
||||
import com.rada.ui.stations.StationBrowserScreen
|
||||
import com.rada.ui.streams.StreamsScreen
|
||||
import com.rada.ui.theme.LocalReduceMotion
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -49,16 +50,28 @@ fun TabsScreen(
|
|||
) {
|
||||
val pagerState = rememberPagerState(initialPage = initialIndex.coerceIn(0, 4)) { 5 }
|
||||
val scope = rememberCoroutineScope()
|
||||
val reduceMotion = LocalReduceMotion.current
|
||||
// Deep-link target for Library (playlist picker → open that playlist).
|
||||
var libraryPlaylistId by remember { mutableStateOf(initialPlaylistId) }
|
||||
// Tab taps land immediately. Swiping remains animated, but animating a
|
||||
// tap composes and disposes heavy pages while the gesture is in flight.
|
||||
// Adjacent tabs glide (neighbors stay composed, so it's smooth); far
|
||||
// taps jump instantly — animating a multi-page hop composes heavy pages
|
||||
// mid-flight and reintroduces the jank the jump was there to avoid.
|
||||
fun goToPage(page: Int) {
|
||||
if (pagerState.currentPage == page) return
|
||||
scope.launch {
|
||||
if (kotlin.math.abs(pagerState.currentPage - page) == 1 && !reduceMotion) {
|
||||
pagerState.animateScrollToPage(
|
||||
page,
|
||||
animationSpec = androidx.compose.animation.core.tween(
|
||||
durationMillis = 260,
|
||||
easing = com.rada.ui.theme.MotionArrival,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
pagerState.scrollToPage(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.rada.ui.theme
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
|
|
@ -130,12 +131,15 @@ fun feedbackDuration(reduce: Boolean): Int = if (reduce) 0 else FeedbackMs
|
|||
/**
|
||||
* Press-dip for list rows: scales to 0.97 while pressed. Use on rows that
|
||||
* already have a clickable/combinedClickable modifier earlier in the chain.
|
||||
* (A plain @Composable extension — the old composed{} form spun up an extra
|
||||
* subcomposition per row, wasted recomposition churn in long lists.)
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.pressDip(
|
||||
interactionSource: MutableInteractionSource,
|
||||
reduce: Boolean,
|
||||
): Modifier = composed {
|
||||
if (reduce) return@composed this
|
||||
): Modifier {
|
||||
if (reduce) return this
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (pressed) 0.97f else 1f,
|
||||
|
|
@ -145,7 +149,7 @@ fun Modifier.pressDip(
|
|||
),
|
||||
label = "pressDip",
|
||||
)
|
||||
this.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
return this.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||
}
|
||||
|
||||
// ── Dismiss-by-drag ───────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 379 B After Width: | Height: | Size: 7 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 22 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 37 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 37 KiB |