Add snap fling behaviour to the quick pics horizontal grid
This commit is contained in:
parent
400b47f6bd
commit
a5e92e87c7
3 changed files with 176 additions and 58 deletions
|
@ -2,9 +2,11 @@ package it.vfsfitvnm.vimusic.ui.screens.home
|
|||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -12,23 +14,29 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
|
||||
import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver
|
||||
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||
|
@ -49,6 +57,7 @@ import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
|||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
import it.vfsfitvnm.vimusic.utils.SnapLayoutInfoProvider
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||
|
@ -109,8 +118,19 @@ fun QuickPicks(
|
|||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 24.dp, bottom = 8.dp)
|
||||
|
||||
val quickPicksLazyGridItemWidthFactor = 0.9f
|
||||
val quickPicksLazyGridState = rememberLazyGridState()
|
||||
val snapLayoutInfoProvider = remember(quickPicksLazyGridState) {
|
||||
SnapLayoutInfoProvider(
|
||||
lazyGridState = quickPicksLazyGridState,
|
||||
positionInLayout = {layoutSize, itemSize ->
|
||||
(layoutSize * quickPicksLazyGridItemWidthFactor / 2f - itemSize / 2f)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BoxWithConstraints {
|
||||
val itemInHorizontalGridWidth = maxWidth * 0.9f
|
||||
val itemInHorizontalGridWidth = maxWidth * quickPicksLazyGridItemWidthFactor
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
@ -123,7 +143,9 @@ fun QuickPicks(
|
|||
|
||||
relatedPageResult?.getOrNull()?.let { related ->
|
||||
LazyHorizontalGrid(
|
||||
state = quickPicksLazyGridState,
|
||||
rows = GridCells.Fixed(4),
|
||||
flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height((songThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 4)
|
||||
|
@ -134,6 +156,15 @@ fun QuickPicks(
|
|||
song = song,
|
||||
thumbnailSizePx = songThumbnailSizePx,
|
||||
thumbnailSizeDp = songThumbnailSizeDp,
|
||||
trailingContent = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.star),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
|
@ -160,7 +191,7 @@ fun QuickPicks(
|
|||
}
|
||||
|
||||
items(
|
||||
items = related.songs ?: emptyList(),
|
||||
items = related.songs?.dropLast(1) ?: emptyList(),
|
||||
key = Innertube.SongItem::key
|
||||
) { song ->
|
||||
SongItem(
|
||||
|
@ -192,71 +223,77 @@ fun QuickPicks(
|
|||
}
|
||||
}
|
||||
|
||||
BasicText(
|
||||
text = "Related albums",
|
||||
style = typography.m.semiBold,
|
||||
modifier = sectionTextModifier
|
||||
)
|
||||
related.albums?.let { albums ->
|
||||
BasicText(
|
||||
text = "Related albums",
|
||||
style = typography.m.semiBold,
|
||||
modifier = sectionTextModifier
|
||||
)
|
||||
|
||||
LazyRow {
|
||||
items(
|
||||
items = related.albums ?: emptyList(),
|
||||
key = Innertube.AlbumItem::key
|
||||
) { album ->
|
||||
AlbumItem(
|
||||
album = album,
|
||||
thumbnailSizePx = albumThumbnailSizePx,
|
||||
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onAlbumClick(album.key) })
|
||||
)
|
||||
LazyRow {
|
||||
items(
|
||||
items = albums,
|
||||
key = Innertube.AlbumItem::key
|
||||
) { album ->
|
||||
AlbumItem(
|
||||
album = album,
|
||||
thumbnailSizePx = albumThumbnailSizePx,
|
||||
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onAlbumClick(album.key) })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicText(
|
||||
text = "Similar artists",
|
||||
style = typography.m.semiBold,
|
||||
modifier = sectionTextModifier
|
||||
)
|
||||
related.artists?.let { artists ->
|
||||
BasicText(
|
||||
text = "Similar artists",
|
||||
style = typography.m.semiBold,
|
||||
modifier = sectionTextModifier
|
||||
)
|
||||
|
||||
LazyRow {
|
||||
items(
|
||||
items = related.artists ?: emptyList(),
|
||||
key = Innertube.ArtistItem::key,
|
||||
) { artist ->
|
||||
ArtistItem(
|
||||
artist = artist,
|
||||
thumbnailSizePx = artistThumbnailSizePx,
|
||||
thumbnailSizeDp = artistThumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onArtistClick(artist.key) })
|
||||
)
|
||||
LazyRow {
|
||||
items(
|
||||
items = artists,
|
||||
key = Innertube.ArtistItem::key,
|
||||
) { artist ->
|
||||
ArtistItem(
|
||||
artist = artist,
|
||||
thumbnailSizePx = artistThumbnailSizePx,
|
||||
thumbnailSizeDp = artistThumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onArtistClick(artist.key) })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BasicText(
|
||||
text = "Playlists you might like",
|
||||
style = typography.m.semiBold,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 24.dp, bottom = 8.dp)
|
||||
)
|
||||
related.playlists?.let { playlists ->
|
||||
BasicText(
|
||||
text = "Playlists you might like",
|
||||
style = typography.m.semiBold,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 24.dp, bottom = 8.dp)
|
||||
)
|
||||
|
||||
LazyRow {
|
||||
items(
|
||||
items = related.playlists ?: emptyList(),
|
||||
key = Innertube.PlaylistItem::key,
|
||||
) { playlist ->
|
||||
PlaylistItem(
|
||||
playlist = playlist,
|
||||
thumbnailSizePx = playlistThumbnailSizePx,
|
||||
thumbnailSizeDp = playlistThumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onPlaylistClick(playlist.key) })
|
||||
)
|
||||
LazyRow {
|
||||
items(
|
||||
items = playlists,
|
||||
key = Innertube.PlaylistItem::key,
|
||||
) { playlist ->
|
||||
PlaylistItem(
|
||||
playlist = playlist,
|
||||
thumbnailSizePx = playlistThumbnailSizePx,
|
||||
thumbnailSizeDp = playlistThumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onPlaylistClick(playlist.key) })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: relatedPageResult?.exceptionOrNull()?.let {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastSumBy
|
||||
|
||||
fun Density.calculateDistanceToDesiredSnapPosition(
|
||||
layoutInfo: LazyGridLayoutInfo,
|
||||
item: LazyGridItemInfo,
|
||||
positionInLayout: Density.(layoutSize: Float, itemSize: Float) -> Float
|
||||
): Float {
|
||||
val containerSize =
|
||||
with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding }
|
||||
|
||||
val desiredDistance = positionInLayout(containerSize.toFloat(), item.size.width.toFloat())
|
||||
val itemCurrentPosition = item.offset.x.toFloat()
|
||||
|
||||
return itemCurrentPosition - desiredDistance
|
||||
}
|
||||
|
||||
private val LazyGridLayoutInfo.singleAxisViewportSize: Int
|
||||
get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
fun SnapLayoutInfoProvider(
|
||||
lazyGridState: LazyGridState,
|
||||
positionInLayout: Density.(layoutSize: Float, itemSize: Float) -> Float =
|
||||
{ layoutSize, itemSize -> (layoutSize / 2f - itemSize / 2f) }
|
||||
): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider {
|
||||
|
||||
private val layoutInfo: LazyGridLayoutInfo
|
||||
get() = lazyGridState.layoutInfo
|
||||
|
||||
// Single page snapping is the default
|
||||
override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
|
||||
|
||||
override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
|
||||
var lowerBoundOffset = Float.NEGATIVE_INFINITY
|
||||
var upperBoundOffset = Float.POSITIVE_INFINITY
|
||||
|
||||
layoutInfo.visibleItemsInfo.fastForEach { item ->
|
||||
val offset =
|
||||
calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout)
|
||||
|
||||
// Find item that is closest to the center
|
||||
if (offset <= 0 && offset > lowerBoundOffset) {
|
||||
lowerBoundOffset = offset
|
||||
}
|
||||
|
||||
// Find item that is closest to center, but after it
|
||||
if (offset >= 0 && offset < upperBoundOffset) {
|
||||
upperBoundOffset = offset
|
||||
}
|
||||
}
|
||||
|
||||
return lowerBoundOffset.rangeTo(upperBoundOffset)
|
||||
}
|
||||
|
||||
override fun Density.snapStepSize(): Float = with(layoutInfo) {
|
||||
if (visibleItemsInfo.isNotEmpty()) {
|
||||
visibleItemsInfo.fastSumBy { it.size.width } / visibleItemsInfo.size.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/star.xml
Normal file
9
app/src/main/res/drawable/star.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M394,480a16,16 0,0 1,-9.39 -3L256,383.76 127.39,477a16,16 0,0 1,-24.55 -18.08L153,310.35 23,221.2A16,16 0,0 1,32 192H192.38l48.4,-148.95a16,16 0,0 1,30.44 0l48.4,149H480a16,16 0,0 1,9.05 29.2L359,310.35l50.13,148.53A16,16 0,0 1,394 480Z"/>
|
||||
</vector>
|
Loading…
Reference in a new issue