Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 96 additions & 24 deletions app/src/main/java/com/pakohan/coverflow_app/CoverFlowScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,46 +41,55 @@ fun CoverFlowScreen(

Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedVisibility(visible = showSettings) {
CoverFlowSettings(
params = params,
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,
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion coverflow/src/main/java/com/pakohan/coverflow/CoverFlow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fun CoverFlow(
item {
Spacer(modifier = Modifier.width(with(LocalDensity.current) { (geometry.spacerWidth).toDp() }))
}

coverFlowScope.apply(content)

item {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {},
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<RowElement>()

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,
)
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading