Last active
February 26, 2023 07:38
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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