diff --git a/app/src/main/java/com/pakohan/coverflow_app/CoverFlowScreen.kt b/app/src/main/java/com/pakohan/coverflow_app/CoverFlowScreen.kt index 3369288..8ed4021 100644 --- a/app/src/main/java/com/pakohan/coverflow_app/CoverFlowScreen.kt +++ b/app/src/main/java/com/pakohan/coverflow_app/CoverFlowScreen.kt @@ -4,18 +4,31 @@ import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.requiredSize import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.pakohan.coverflow.CoverFlow import com.pakohan.coverflow.CoverFlowParams +import com.pakohan.coverflow.OffsetLinearDistanceFactor +import com.pakohan.coverflow.lazyayout.CenteredLazyRow +import com.pakohan.coverflow.lazyayout.rememberCenteredLazyRowState import com.pakohan.coverflow.rememberCoverFlowState // This is for playing around with the parameters @@ -28,6 +41,7 @@ fun CoverFlowScreen( Column( modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, ) { AnimatedVisibility(visible = showSettings) { CoverFlowSettings( @@ -35,39 +49,47 @@ fun CoverFlowScreen( onParamsUpdate = { params = it }, ) } + + val items = listOf( + "This is a simple Compose CoverFlow implementation", + "Apple patented it, but the patent expired in 2024", + "This component is based on LazyRow, which makes it efficient", + "It's customizable, but comes with good standard options, so it's easy to use", + "It also keeps its layout when scaled, since all configuration options are relative to the container size", + "It measures the container and then uses the shorter edge of it", + "The first param is the size factor: it's multiplied with the short edge to get the cover size", + "All other Parameters are relative to the cover size", + "Offset is how much space is between each element", + "Then there are three effects being applied to the elements on the side: angle, horizontal shift, and zoom", + "The effect is added gradually, depending on the distance to the center", + "The start parameter tells when the effects start being applied, the end parameter tells from which distance they should be fully applied", + ) + + val firstState = rememberCoverFlowState( + onSelectHandler = { + Log.d( + "CoverFlowScreen", + "onSelectHandler outside of scope: $it", + ) + }, + ) + + CenterIndicator() CoverFlow( - modifier = Modifier.background(Color.Black), - state = rememberCoverFlowState( - onSelectHandler = { - Log.d( - "CoverFlowScreen", - "onSelectHandler outside of scope: $it", - ) - }, - ), + modifier = Modifier + .background(Color.Black) + .weight(1f), + state = firstState, params = params, ) { items( - onSelectHandler = { item: String, index: Int -> + onSelectHandler = { _: String, index: Int -> Log.d( "CoverFlowScreen", "onSelectHandler in scope: $index", ) }, - items = listOf( - "This is a simple Compose CoverFlow implementation", - "Apple patented it, but the patent expired in 2024", - "This component is based on LazyRow, which makes it efficient", - "It's customizable, but comes with good standard options, so it's easy to use", - "It also keeps its layout when scaled, since all configuration options are relative to the container size", - "It measures the container and then uses the shorter edge of it", - "The first param is the size factor: it's multiplied with the short edge to get the cover size", - "All other Parameters are relative to the cover size", - "Offset is how much space is between each element", - "Then there are three effects being applied to the elements on the side: angle, horizontal shift, and zoom", - "The effect is added gradually, depending on the distance to the center", - "The start parameter tells when the effects start being applied, the end parameter tells from which distance they should be fully applied", - ), + items = items, ) { Text( text = it, @@ -77,9 +99,59 @@ fun CoverFlowScreen( ) } } + + CenterIndicator() + + val state = rememberCenteredLazyRowState() + + val old = OffsetLinearDistanceFactor( + 200f, + 400f, + ) + + CenteredLazyRow( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background(Color.Black), + state = state, + ) { + items(items) { _, distance, item -> + Text( + modifier = Modifier + .requiredSize(with(LocalDensity.current) { (state.calculatedCenteredLazyRowLayoutInfo.itemWidth * 1.1f).toDp() }) + .coverGraphicsLayer { + rotationY = -55f * old.factor(it.toFloat()) + } + .background(Color.White), + + text = "$distance\n$item", + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .requiredSize(with(LocalDensity.current) { (state.calculatedCenteredLazyRowLayoutInfo.itemWidth * 1.1f).toDp() }) + .mirrorGraphicsLayer { + rotationY = -55f * old.factor(it.toFloat()) + } + .background(Color.White), + + text = item.uppercase(), + textAlign = TextAlign.Center, + ) + } + } + CenterIndicator() } } +@Composable +fun CenterIndicator(height: Dp = 16.dp) = Row(modifier = Modifier.height(height)) { + Spacer(modifier = Modifier.weight(1f)) + VerticalDivider() + Spacer(modifier = Modifier.weight(1f)) +} + /** * For debugging. Sets the Parameters to values so that we get a normal LazyList without any * CoverFlow effects. diff --git a/coverflow/src/main/java/com/pakohan/coverflow/CoverFlow.kt b/coverflow/src/main/java/com/pakohan/coverflow/CoverFlow.kt index 21d9da2..717b7e8 100644 --- a/coverflow/src/main/java/com/pakohan/coverflow/CoverFlow.kt +++ b/coverflow/src/main/java/com/pakohan/coverflow/CoverFlow.kt @@ -63,7 +63,7 @@ fun CoverFlow( item { Spacer(modifier = Modifier.width(with(LocalDensity.current) { (geometry.spacerWidth).toDp() })) } - + coverFlowScope.apply(content) item { diff --git a/coverflow/src/main/java/com/pakohan/coverflow/CoverFlowState.kt b/coverflow/src/main/java/com/pakohan/coverflow/CoverFlowState.kt index e208993..3d75f09 100644 --- a/coverflow/src/main/java/com/pakohan/coverflow/CoverFlowState.kt +++ b/coverflow/src/main/java/com/pakohan/coverflow/CoverFlowState.kt @@ -30,7 +30,7 @@ fun rememberCoverFlowState(onSelectHandler: (Int) -> Unit = {}): CoverFlowState * Exposes the scroll state. */ class CoverFlowState internal constructor( - internal val lazyListState: LazyListState, + val lazyListState: LazyListState, private val coroutineScope: CoroutineScope, private val onSelectHandler: (Int) -> Unit = {}, ) { diff --git a/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CalculatedCenteredLazyRowLayoutInfo.kt b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CalculatedCenteredLazyRowLayoutInfo.kt new file mode 100644 index 0000000..8a69d9a --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CalculatedCenteredLazyRowLayoutInfo.kt @@ -0,0 +1,84 @@ +package com.pakohan.coverflow.lazyayout + +import kotlin.math.abs + +data class CalculatedCenteredLazyRowLayoutInfo( + val scrollOffset: Int, + val containerSize: Dimension, + val centeredLazyRowLayoutInfo: CenteredLazyRowLayoutInfo, + val itemCount: Int, +) { + private val halfContainerHeight = containerSize.height / 2 + + val itemWidth = centeredLazyRowLayoutInfo.itemWidth(containerSize) + + private val halfWidth = itemWidth / 2 + + private val spacerWidth = containerSize.width / 2 - halfWidth + + private val visibleSpacer = spacerWidth - scrollOffset + + internal val maximumScrollOffset = itemWidth * itemCount - itemWidth + + internal val remainingScrollOffset = maximumScrollOffset - scrollOffset + + val maxHeight: Int = halfContainerHeight + halfWidth + + val firstVisibleItemX = if (visibleSpacer < 0) { + visibleSpacer % itemWidth + } else { + visibleSpacer + } + + private val firstVisibleItemIndex = if (visibleSpacer < 0) { + (-visibleSpacer) / itemWidth + } else { + 0 + } + + private val firstFullVisibleItemX = if (firstVisibleItemX < 0) { + itemWidth + firstVisibleItemX + } else { + firstVisibleItemX + } + + private val spaceFromFirstFullVisibleItem = containerSize.width - firstFullVisibleItemX + + private val amountFullyVisibleItems = spaceFromFirstFullVisibleItem / itemWidth + + private val spaceLeftAfterFullyVisibleItems = spaceFromFirstFullVisibleItem % itemWidth + + private val lastItemFullyVisible = spaceLeftAfterFullyVisibleItems == 0 + + private val selectedIndex = (scrollOffset + halfWidth) / itemWidth + + private val lastVisibleItemIndex: Int + get() { + var result = firstVisibleItemIndex + result += amountFullyVisibleItems + if (!lastItemFullyVisible) { + result++ + } + if (result > itemCount - 1) { + result = itemCount - 1 + } + return result + } + + private fun itemsToCenter(index: Int) = -abs(selectedIndex - index) + + internal fun zIndex(index: Int) = itemsToCenter(index).toFloat() + + fun distanceToCenter(index: Int) = index * itemWidth - scrollOffset + + internal fun y(itemHeight: Int) = centeredLazyRowLayoutInfo.itemY( + containerSize, + itemHeight, + ) + + internal val indexRange: IntRange = firstVisibleItemIndex..lastVisibleItemIndex + + internal val nextItemLeftOffset: Int = scrollOffset % itemWidth + + internal val nextItemRightOffset: Int = itemWidth - nextItemLeftOffset +} diff --git a/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRow.kt b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRow.kt new file mode 100644 index 0000000..6a0e446 --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRow.kt @@ -0,0 +1,94 @@ +package com.pakohan.coverflow.lazyayout + +import android.graphics.Point +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.lazy.layout.LazyLayout +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.unit.Constraints + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CenteredLazyRow( + modifier: Modifier = Modifier, + state: CenteredLazyRowState = rememberCenteredLazyRowState(), + content: CenteredLazyRowScope.() -> Unit, +) { + rememberUpdatedState(content).also { state.lazyListScope.apply(it.value) } + + LazyLayout( + modifier = modifier.scrollable( + state = state.scrollableState, + orientation = Orientation.Horizontal, + flingBehavior = rememberSnapFlingBehavior(state.snapLayoutInfoProvider), + ), + itemProvider = { state.itemProvider }, + ) { + state.containerSize = Dimension( + width = it.maxWidth, + height = it.maxHeight, + ) + + val calculatedGeometry = state.calculatedCenteredLazyRowLayoutInfo + var x = calculatedGeometry.firstVisibleItemX + val visibleItems = mutableListOf() + + for (i in calculatedGeometry.indexRange) { + val placeables = measure( + i, + Constraints( + maxWidth = calculatedGeometry.itemWidth, + maxHeight = calculatedGeometry.maxHeight, + ), + ) + if (placeables.size !in 1..2) { + throw IllegalArgumentException("CustomLazyLayout only supports one composable per item, got ${placeables.size}") + } + + visibleItems.add( + RowElement( + placeables[0], + if (placeables.size > 1) placeables[1] else null, + Point( + x, + calculatedGeometry.y(placeables[0].height), + ), + calculatedGeometry.zIndex(i), + ), + ) + x += calculatedGeometry.itemWidth + } + + layout( + it.maxWidth, + it.maxHeight, + ) { + visibleItems.forEach { element -> + element.placeable.placeRelative( + element.coordinates.x, + element.coordinates.y, + element.zIndex, + ) + if (element.mirrorPlaceable != null) { + element.mirrorPlaceable.placeRelative( + element.coordinates.x, + element.coordinates.y + element.placeable.height, + element.zIndex, + ) + } + } + } + } +} + +private data class RowElement( + val placeable: Placeable, + val mirrorPlaceable: Placeable?, + val coordinates: Point, + val zIndex: Float, +) diff --git a/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowItemScope.kt b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowItemScope.kt new file mode 100644 index 0000000..9104224 --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowItemScope.kt @@ -0,0 +1,24 @@ +package com.pakohan.coverflow.lazyayout + +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer + +interface CenteredLazyRowItemScope { + fun Modifier.coverGraphicsLayer(block: GraphicsLayerScope.(Int) -> Unit): Modifier + fun Modifier.mirrorGraphicsLayer(block: GraphicsLayerScope.(Int) -> Unit): Modifier +} + +class CenteredLazyRowItemScopeImpl(private val distanceToCenter: Int) : CenteredLazyRowItemScope { + override fun Modifier.coverGraphicsLayer(block: GraphicsLayerScope.(Int) -> Unit): Modifier = + this.graphicsLayer { block(distanceToCenter) } + + override fun Modifier.mirrorGraphicsLayer(block: GraphicsLayerScope.(Int) -> Unit): Modifier = this.graphicsLayer { + transformOrigin = TransformOrigin( + .5f, + -.5f, + ) + block(distanceToCenter) + } +} diff --git a/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowLayoutInfo.kt b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowLayoutInfo.kt new file mode 100644 index 0000000..6aac4a1 --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowLayoutInfo.kt @@ -0,0 +1,42 @@ +package com.pakohan.coverflow.lazyayout + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable + +interface CenteredLazyRowLayoutInfo { + fun itemWidth(containerSize: Dimension): Int + + fun itemY( + containerSize: Dimension, + itemHeight: Int, + ): Int +} + +class CoverFlowLayoutInfo(private val itemSizeFactor: Float = .5f) : CenteredLazyRowLayoutInfo { + override fun itemWidth(containerSize: Dimension): Int = (containerSize.shortEdge * itemSizeFactor).toInt() + + private fun halfWidth(containerSize: Dimension): Int { + return itemWidth(containerSize) / 2 + } + + override fun itemY( + containerSize: Dimension, + itemHeight: Int, + ) = containerSize.height / 2 + halfWidth(containerSize) - itemHeight +} + +@Stable +@Immutable +data class Dimension( + val width: Int, + val height: Int, +) { + val shortEdge: Int = if (width < height) width else height + + companion object { + val Zero = Dimension( + 0, + 0, + ) + } +} diff --git a/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowScope.kt b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowScope.kt new file mode 100644 index 0000000..4b6e7b4 --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowScope.kt @@ -0,0 +1,13 @@ +package com.pakohan.coverflow.lazyayout + +interface CenteredLazyRowScope { + fun items( + amount: Int, + itemContent: ItemFunc, + ) + + fun items( + items: List, + itemContent: ParameterItemFunc, + ) +} diff --git a/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowState.kt b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowState.kt new file mode 100644 index 0000000..4e49c89 --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/lazyayout/CenteredLazyRowState.kt @@ -0,0 +1,162 @@ +package com.pakohan.coverflow.lazyayout + +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider +import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density + +@Composable +fun rememberCenteredLazyRowState(centeredLazyRowLayoutInfo: CenteredLazyRowLayoutInfo = CoverFlowLayoutInfo()): CenteredLazyRowState { + val density = LocalDensity.current + return remember { // remember to save the scrollOffset and items + CenteredLazyRowState( + centeredLazyRowLayoutInfo, + density, + ) + } +} + +typealias ItemFunc = @Composable CenteredLazyRowItemScope.(Int, Int) -> Unit +typealias ParameterItemFunc = @Composable CenteredLazyRowItemScope.(Int, Int, T) -> Unit + +@OptIn(ExperimentalFoundationApi::class) +class CenteredLazyRowState( + private val centeredLazyRowLayoutInfo: CenteredLazyRowLayoutInfo, + private val density: Density, +) { + private var scrollOffset by mutableIntStateOf(0) + private val items = mutableListOf() + internal var containerSize: Dimension = Dimension.Zero + + private data class LazyLayoutItemContent( + val index: Int, + val itemContent: ItemFunc, + ) + + val calculatedCenteredLazyRowLayoutInfo + get() = CalculatedCenteredLazyRowLayoutInfo( + scrollOffset = scrollOffset, + containerSize = containerSize, + centeredLazyRowLayoutInfo = centeredLazyRowLayoutInfo, + itemCount = items.size, + ) + + private fun consumeScrollDelta(delta: Float): Float { + var consumedDelta = delta + if (consumedDelta > calculatedCenteredLazyRowLayoutInfo.remainingScrollOffset.toFloat()) { + consumedDelta = calculatedCenteredLazyRowLayoutInfo.remainingScrollOffset.toFloat() + } + scrollOffset -= consumedDelta.toInt() + + Log.d( + "consumeScrollDelta", + "$delta -> $consumedDelta", + ) + return consumedDelta + } + + internal val scrollableState = ScrollableState(::consumeScrollDelta) + + internal val itemProvider = object : LazyLayoutItemProvider { + override val itemCount + get() = items.size + + @Composable + override fun Item( + index: Int, + key: Any, + ) { + val item = items.getOrNull(index) ?: return + val distance = calculatedCenteredLazyRowLayoutInfo.distanceToCenter(index) + item.itemContent( + CenteredLazyRowItemScopeImpl(distance), + item.index, + distance, + ) + } + } + + internal val lazyListScope = object : CenteredLazyRowScope { + override fun items( + amount: Int, + itemContent: ItemFunc, + ) { + for (i in 0.. items( + items: List, + itemContent: ParameterItemFunc, + ) = items(items.size) { index, distance -> + itemContent( + index, + distance, + items[index], + ) + } + } + + internal val snapLayoutInfoProvider = object : SnapLayoutInfoProvider { + override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f +// { +// val decayAnimationSpec: DecayAnimationSpec = splineBasedDecay(density) +// val offset = decayAnimationSpec.calculateTargetValue( +// 0f, +// initialVelocity, +// ).absoluteValue +// +// val estimatedNumberOfItemsInDecay = +// floor(offset.absoluteValue / calculatedCenteredLazyRowLayoutInfo.itemWidth) +// +// val approachOffset = +// estimatedNumberOfItemsInDecay * calculatedCenteredLazyRowLayoutInfo.itemWidth - calculatedCenteredLazyRowLayoutInfo.itemWidth +// val finalDecayOffset = approachOffset.coerceAtLeast(0f) +// .coerceAtMost(calculatedCenteredLazyRowLayoutInfo.maximumScrollOffset.toFloat()) +// return if (finalDecayOffset == 0f) { +// finalDecayOffset +// } else { +// finalDecayOffset * initialVelocity.sign +// } +// } + + // positive numbers lead back to the beginning + override fun calculateSnapOffset(velocity: Float): Float { + val result = if (velocity < 0) { // scrolling to the right, elements moving to the left + if (scrollOffset + calculatedCenteredLazyRowLayoutInfo.nextItemRightOffset.toFloat() > calculatedCenteredLazyRowLayoutInfo.maximumScrollOffset) { + calculatedCenteredLazyRowLayoutInfo.nextItemLeftOffset.toFloat() + } else { + -calculatedCenteredLazyRowLayoutInfo.nextItemRightOffset.toFloat() + } + } else if (velocity > 0) { + if (scrollOffset - calculatedCenteredLazyRowLayoutInfo.nextItemLeftOffset.toFloat() < 0) { + -calculatedCenteredLazyRowLayoutInfo.nextItemRightOffset.toFloat() + } else { + calculatedCenteredLazyRowLayoutInfo.nextItemLeftOffset.toFloat() + } + } else { + 0f + } + + Log.d( + "calculateSnappingOffset", + velocity.toString(), + ) + return 10f + } + } +} diff --git a/coverflow/src/main/java/com/pakohan/coverflow/list/SelectCenterLazyList.kt b/coverflow/src/main/java/com/pakohan/coverflow/list/SelectCenterLazyList.kt new file mode 100644 index 0000000..4b37a8e --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/list/SelectCenterLazyList.kt @@ -0,0 +1,51 @@ +package com.pakohan.coverflow.list + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntSize + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun SelectCenterLazyRow( + modifier: Modifier = Modifier, + verticalAlignment: Alignment.Vertical = Alignment.Top, + content: SelectCenterLazyRowScope.() -> Unit, +) { + var size by remember { mutableStateOf(IntSize.Zero) } + val spacerModifier = Modifier.width(with(LocalDensity.current) { (size.width / 2).toDp() }) + + val state: LazyListState = rememberLazyListState() + + LazyRow( + modifier = modifier.onGloballyPositioned { + size = it.size + }, + state = state, + verticalAlignment = verticalAlignment, + flingBehavior = rememberSnapFlingBehavior(lazyListState = state), + ) { + item { + Spacer(modifier = spacerModifier) + } + + SelectCenterLazyRowScopeImpl(this).content() + + item { + Spacer(modifier = spacerModifier) + } + } +} diff --git a/coverflow/src/main/java/com/pakohan/coverflow/list/SelectCenterLazyRowScope.kt b/coverflow/src/main/java/com/pakohan/coverflow/list/SelectCenterLazyRowScope.kt new file mode 100644 index 0000000..9c3fcfe --- /dev/null +++ b/coverflow/src/main/java/com/pakohan/coverflow/list/SelectCenterLazyRowScope.kt @@ -0,0 +1,69 @@ +package com.pakohan.coverflow.list + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable + +/** + * Receiver scope to add elements to the CoverFlow list. + */ +interface SelectCenterLazyRowScope { + /** + * Add typed items. See [LazyListScope.items] + */ + fun items( + items: List, + onSelectHandler: (item: T, index: Int) -> Unit = { _: T, _: Int -> }, + key: ((index: Int) -> Any)? = null, + contentType: (index: Int) -> Any? = { null }, + itemContent: @Composable ((item: T) -> Unit), + ) + + /** + * Add typed items. See [LazyListScope.items] + */ + fun items( + count: Int, + onSelectHandler: (index: Int) -> Unit = {}, + key: ((index: Int) -> Any)? = null, + contentType: (index: Int) -> Any? = { null }, + itemContent: @Composable ((index: Int) -> Unit), + ) +} + +internal class SelectCenterLazyRowScopeImpl( + private val lazyListScope: LazyListScope, +) : SelectCenterLazyRowScope { + override fun items( + count: Int, + onSelectHandler: (index: Int) -> Unit, + key: ((index: Int) -> Any)?, + contentType: (index: Int) -> Any?, + itemContent: @Composable (index: Int) -> Unit, + ) = lazyListScope.items( + count, + key, + contentType, + ) { + itemContent(it) + } + + override fun items( + items: List, + onSelectHandler: (item: T, index: Int) -> Unit, + key: ((index: Int) -> Any)?, + contentType: (index: Int) -> Any?, + itemContent: @Composable (item: T) -> Unit, + ) = items( + count = items.size, + onSelectHandler = { + onSelectHandler( + items[it], + it, + ) + }, + key = key, + contentType = contentType, + ) { + itemContent(items[it]) + } +}