Skip to content

Instantly share code, notes, and snippets.

@razavioo
Last active February 26, 2023 07:38
Show Gist options
  • Save razavioo/1ff84d5d93e50895fdb6dbfce37ad66e to your computer and use it in GitHub Desktop.
Save razavioo/1ff84d5d93e50895fdb6dbfce37ad66e to your computer and use it in GitHub Desktop.
This is a Jetpack Compose function that creates a curved scrollable list of items, with custom scrolling behavior and visual effects. It takes in parameters for item count, center item callback, and item Composable.
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
@Composable
fun CurvedScrollView(
modifier: Modifier = Modifier,
count: Int,
onCenterChanged: ((Int) -> Unit)? = null,
item: @Composable (Int) -> Unit
) {
val scrollState = rememberScrollState()
val size = remember { mutableStateOf(IntSize.Zero) }
val scope = rememberCoroutineScope()
val indices = remember { IntArray(count) { 0 } }
val flingBehaviour = object : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
val velocityScrollValue = (initialVelocity / 15).toInt()
val value = scrollState.value + velocityScrollValue
indices.minByOrNull { abs(it - value) }?.let { scrollPosition ->
scope.launch {
scrollState.animateScrollTo(scrollPosition)
val index = indices.firstOrNull { it == scrollPosition }
index?.let {
if (index == 0) {
onCenterChanged?.invoke(0)
} else {
onCenterChanged?.invoke(index / indices[1])
}
}
}
}
return initialVelocity
}
}
Box(
modifier = modifier
.onSizeChanged {
size.value = it
}
) {
Layout(
content = {
repeat(count) {
item(it)
}
},
modifier = Modifier.verticalScroll(
scrollState, flingBehavior = flingBehaviour
)
) { measurables, constraints ->
val itemSpacing = 8.dp.roundToPx()
var contentHeight = (count - 1) * itemSpacing
val placeables = measurables.mapIndexed { index, measurable ->
val placeable = measurable.measure(constraints = constraints)
contentHeight += if (index == 0 || index == measurables.lastIndex) {
placeable.height / 2
} else {
placeable.height
}
placeable
}
layout(constraints.maxWidth, size.value.height + contentHeight) {
val startOffset = size.value.height / 2 - placeables[0].height / 2
var yPosition = startOffset
val scrollPercent = scrollState.value.toFloat() / scrollState.maxValue
placeables.forEachIndexed { index, placeable ->
val elementRatio = index.toFloat() / placeables.lastIndex
val interpolatedValue = cos((scrollPercent - elementRatio) * PI)
val indent = interpolatedValue * size.value.width / 2
placeable.placeRelativeWithLayer(
x = indent.toInt() - (size.value.width / 2.9).toInt(),
y = yPosition
) {
scaleX = interpolatedValue.toFloat()
scaleY = interpolatedValue.toFloat()
alpha = interpolatedValue.toFloat()
}
indices[index] = yPosition - startOffset
yPosition += placeable.height + itemSpacing
}
}
}
}
}
setContent {
val currentIndex = remember { mutableStateOf(0) }
val itemsActive = listOf(
R.drawable.image_1,
R.drawable.image_2,
R.drawable.image_3
)
val itemsInactive = listOf(
R.drawable.image_1_inactive,
R.drawable.image_2_inactive,
R.drawable.image_3_inactive
)
val onClickableItem = { _: Int, index: Int ->
val event = when (index) {
0 -> HomeViewEvent.OpenPage1
1 -> HomeViewEvent.OpenPage2
2 -> HomeViewEvent.OpenPage3
else -> HomeViewEvent.OpenPage1
}
runEvent(event)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = 10.dp, end = 10.dp),
contentAlignment = Alignment.Center
) {
CurvedScrollView(
count = itemsActive.size,
onCenterChanged = { index: Int ->
currentIndex.value = index
}
) { index ->
val isCenter = index == currentIndex.value
val background = if (isCenter) itemsActive[index] else itemsInactive[index]
val size = if (isCenter) 155.dp else 135.dp
Image(
painter = painterResource(id = background),
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.size(size)
.clickable { onClickableItem(background, index) }
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment