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.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
|
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver
|
import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
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.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.SnapLayoutInfoProvider
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
|
@ -109,8 +118,19 @@ fun QuickPicks(
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(top = 24.dp, bottom = 8.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 {
|
BoxWithConstraints {
|
||||||
val itemInHorizontalGridWidth = maxWidth * 0.9f
|
val itemInHorizontalGridWidth = maxWidth * quickPicksLazyGridItemWidthFactor
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -123,7 +143,9 @@ fun QuickPicks(
|
||||||
|
|
||||||
relatedPageResult?.getOrNull()?.let { related ->
|
relatedPageResult?.getOrNull()?.let { related ->
|
||||||
LazyHorizontalGrid(
|
LazyHorizontalGrid(
|
||||||
|
state = quickPicksLazyGridState,
|
||||||
rows = GridCells.Fixed(4),
|
rows = GridCells.Fixed(4),
|
||||||
|
flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height((songThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 4)
|
.height((songThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 4)
|
||||||
|
@ -134,6 +156,15 @@ fun QuickPicks(
|
||||||
song = song,
|
song = song,
|
||||||
thumbnailSizePx = songThumbnailSizePx,
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
thumbnailSizeDp = songThumbnailSizeDp,
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
trailingContent = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.star),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
|
@ -160,7 +191,7 @@ fun QuickPicks(
|
||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = related.songs ?: emptyList(),
|
items = related.songs?.dropLast(1) ?: emptyList(),
|
||||||
key = Innertube.SongItem::key
|
key = Innertube.SongItem::key
|
||||||
) { song ->
|
) { song ->
|
||||||
SongItem(
|
SongItem(
|
||||||
|
@ -192,71 +223,77 @@ fun QuickPicks(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicText(
|
related.albums?.let { albums ->
|
||||||
text = "Related albums",
|
BasicText(
|
||||||
style = typography.m.semiBold,
|
text = "Related albums",
|
||||||
modifier = sectionTextModifier
|
style = typography.m.semiBold,
|
||||||
)
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
LazyRow {
|
LazyRow {
|
||||||
items(
|
items(
|
||||||
items = related.albums ?: emptyList(),
|
items = albums,
|
||||||
key = Innertube.AlbumItem::key
|
key = Innertube.AlbumItem::key
|
||||||
) { album ->
|
) { album ->
|
||||||
AlbumItem(
|
AlbumItem(
|
||||||
album = album,
|
album = album,
|
||||||
thumbnailSizePx = albumThumbnailSizePx,
|
thumbnailSizePx = albumThumbnailSizePx,
|
||||||
thumbnailSizeDp = albumThumbnailSizeDp,
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
alternative = true,
|
alternative = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = { onAlbumClick(album.key) })
|
.clickable(onClick = { onAlbumClick(album.key) })
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicText(
|
related.artists?.let { artists ->
|
||||||
text = "Similar artists",
|
BasicText(
|
||||||
style = typography.m.semiBold,
|
text = "Similar artists",
|
||||||
modifier = sectionTextModifier
|
style = typography.m.semiBold,
|
||||||
)
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
LazyRow {
|
LazyRow {
|
||||||
items(
|
items(
|
||||||
items = related.artists ?: emptyList(),
|
items = artists,
|
||||||
key = Innertube.ArtistItem::key,
|
key = Innertube.ArtistItem::key,
|
||||||
) { artist ->
|
) { artist ->
|
||||||
ArtistItem(
|
ArtistItem(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
thumbnailSizePx = artistThumbnailSizePx,
|
thumbnailSizePx = artistThumbnailSizePx,
|
||||||
thumbnailSizeDp = artistThumbnailSizeDp,
|
thumbnailSizeDp = artistThumbnailSizeDp,
|
||||||
alternative = true,
|
alternative = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = { onArtistClick(artist.key) })
|
.clickable(onClick = { onArtistClick(artist.key) })
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicText(
|
related.playlists?.let { playlists ->
|
||||||
text = "Playlists you might like",
|
BasicText(
|
||||||
style = typography.m.semiBold,
|
text = "Playlists you might like",
|
||||||
modifier = Modifier
|
style = typography.m.semiBold,
|
||||||
.padding(horizontal = 16.dp)
|
modifier = Modifier
|
||||||
.padding(top = 24.dp, bottom = 8.dp)
|
.padding(horizontal = 16.dp)
|
||||||
)
|
.padding(top = 24.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
LazyRow {
|
LazyRow {
|
||||||
items(
|
items(
|
||||||
items = related.playlists ?: emptyList(),
|
items = playlists,
|
||||||
key = Innertube.PlaylistItem::key,
|
key = Innertube.PlaylistItem::key,
|
||||||
) { playlist ->
|
) { playlist ->
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
playlist = playlist,
|
playlist = playlist,
|
||||||
thumbnailSizePx = playlistThumbnailSizePx,
|
thumbnailSizePx = playlistThumbnailSizePx,
|
||||||
thumbnailSizeDp = playlistThumbnailSizeDp,
|
thumbnailSizeDp = playlistThumbnailSizeDp,
|
||||||
alternative = true,
|
alternative = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = { onPlaylistClick(playlist.key) })
|
.clickable(onClick = { onPlaylistClick(playlist.key) })
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: relatedPageResult?.exceptionOrNull()?.let {
|
} ?: 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