Last active
September 1, 2022 00:13
-
-
Save fvilarino/e924ac2be9982d8dee934ac5feeb572c to your computer and use it in GitHub Desktop.
Pager - final implementation
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
@Composable | |
fun <T : Any> Pager( | |
items: List<T>, | |
modifier: Modifier = Modifier, | |
orientation: Orientation = Orientation.Horizontal, | |
initialIndex: Int = 0, | |
/*@FloatRange(from = 0.0, to = 1.0)*/ | |
itemFraction: Float = 1f, | |
itemSpacing: Dp = 0.dp, | |
/*@FloatRange(from = 0.0, to = 1.0)*/ | |
overshootFraction: Float = .5f, | |
onItemSelect: (T) -> Unit = {}, | |
contentFactory: @Composable (T) -> Unit, | |
) { | |
Pager( | |
modifier, | |
orientation, | |
initialIndex, | |
itemFraction, | |
itemSpacing, | |
overshootFraction, | |
onItemSelect = { index -> onItemSelect(items[index]) }, | |
) { | |
items.forEach{ item -> | |
Box( | |
modifier = when (orientation) { | |
Orientation.Horizontal -> Modifier.fillMaxWidth() | |
Orientation.Vertical -> Modifier.fillMaxHeight() | |
}, | |
contentAlignment = Alignment.Center, | |
) { | |
contentFactory(item) | |
} | |
} | |
} | |
} | |
@Composable | |
fun Pager( | |
modifier: Modifier = Modifier, | |
orientation: Orientation = Orientation.Horizontal, | |
initialIndex: Int = 0, | |
/*@FloatRange(from = 0.0, to = 1.0)*/ | |
itemFraction: Float = 1f, | |
itemSpacing: Dp = 0.dp, | |
/*@FloatRange(from = 0.0, to = 1.0)*/ | |
overshootFraction: Float = .5f, | |
onItemSelect: (Int) -> Unit = {}, | |
content: @Composable () -> Unit, | |
) { | |
require(initialIndex in 0..items.lastIndex) { "Initial index out of bounds" } | |
require(itemFraction > 0f && itemFraction <= 1f) { "Item fraction must be in the (0f, 1f] range" } | |
require(overshootFraction > 0f && itemFraction <= 1f) { "Overshoot fraction must be in the (0f, 1f] range" } | |
val scope = rememberCoroutineScope() | |
val state = rememberPagerState() | |
state.currentIndex = initialIndex | |
state.numberOfItems = items.size | |
state.itemFraction = itemFraction | |
state.overshootFraction = overshootFraction | |
state.itemSpacing = with(LocalDensity.current) { itemSpacing.toPx() } | |
state.orientation = orientation | |
state.listener = onItemSelect | |
state.scope = scope | |
Layout( | |
content = content, | |
modifier = modifier | |
.clipToBounds() | |
.then(state.inputModifier), | |
) { measurables, constraints -> | |
val dimension = constraints.dimension(orientation) | |
val looseConstraints = constraints.toLooseConstraints(orientation, state.itemFraction) | |
val placeables = measurables.map { measurable -> measurable.measure(looseConstraints) } | |
val size = placeables.getSize(orientation, dimension) | |
val itemDimension = (dimension * state.itemFraction).roundToInt() | |
state.itemDimension = itemDimension | |
val halfItemDimension = itemDimension / 2 | |
layout(size.width, size.height) { | |
val centerOffset = dimension / 2 - halfItemDimension | |
val dragOffset = state.dragOffset.value | |
val roundedDragOffset = dragOffset.roundToInt() | |
val spacing = state.itemSpacing.roundToInt() | |
val itemDimensionWithSpace = itemDimension + state.itemSpacing | |
val first = ceil( | |
(dragOffset -itemDimension - centerOffset) / itemDimensionWithSpace | |
).toInt().coerceAtLeast(0) | |
val last = ((dimension + dragOffset - centerOffset) / itemDimensionWithSpace).toInt() | |
.coerceAtMost(items.lastIndex) | |
for (i in first..last) { | |
val offset = i * (itemDimension + spacing) - roundedDragOffset + centerOffset | |
placeables[i].place( | |
x = when (orientation) { | |
Orientation.Horizontal -> offset | |
Orientation.Vertical -> 0 | |
}, | |
y = when (orientation) { | |
Orientation.Horizontal -> 0 | |
Orientation.Vertical -> offset | |
} | |
) | |
} | |
} | |
} | |
LaunchedEffect(key1 = items, key2 = initialIndex) { | |
state.snapTo(initialIndex) | |
} | |
} | |
@Composable | |
private fun rememberPagerState(): PagerState = remember { PagerState() } | |
private fun Constraints.dimension(orientation: Orientation) = when (orientation) { | |
Orientation.Horizontal -> maxWidth | |
Orientation.Vertical -> maxHeight | |
} | |
private fun Constraints.toLooseConstraints( | |
orientation: Orientation, | |
itemFraction: Float, | |
): Constraints { | |
val dimension = dimension(orientation) | |
return when (orientation) { | |
Orientation.Horizontal -> copy( | |
minWidth = (dimension * itemFraction).roundToInt(), | |
maxWidth = (dimension * itemFraction).roundToInt(), | |
minHeight = 0, | |
) | |
Orientation.Vertical -> copy( | |
minWidth = 0, | |
minHeight = (dimension * itemFraction).roundToInt(), | |
maxHeight = (dimension * itemFraction).roundToInt(), | |
) | |
} | |
} | |
private fun List<Placeable>.getSize( | |
orientation: Orientation, | |
dimension: Int, | |
): IntSize { | |
return when (orientation) { | |
Orientation.Horizontal -> IntSize( | |
dimension, | |
maxByOrNull { it.height }?.height ?: 0 | |
) | |
Orientation.Vertical -> IntSize( | |
maxByOrNull { it.width }?.width ?: 0, | |
dimension | |
) | |
} | |
} | |
private class PagerState { | |
var currentIndex by mutableStateOf(0) | |
var numberOfItems by mutableStateOf(0) | |
var itemFraction by mutableStateOf(0f) | |
var overshootFraction by mutableStateOf(0f) | |
var itemSpacing by mutableStateOf(0f) | |
var itemDimension by mutableStateOf(0) | |
var orientation by mutableStateOf(Orientation.Horizontal) | |
var scope: CoroutineScope? by mutableStateOf(null) | |
var listener: (Int) -> Unit by mutableStateOf({}) | |
val dragOffset = Animatable(0f) | |
private val animationSpec = SpringSpec<Float>( | |
dampingRatio = Spring.DampingRatioLowBouncy, | |
stiffness = Spring.StiffnessLow, | |
) | |
suspend fun snapTo(index: Int) { | |
dragOffset.snapTo(index.toFloat() * (itemDimension + itemSpacing)) | |
} | |
val inputModifier = Modifier.pointerInput(numberOfItems) { | |
fun itemIndex(offset: Int): Int = (offset / (itemDimension + itemSpacing)).roundToInt() | |
.coerceIn(0, numberOfItems - 1) | |
fun updateIndex(offset: Float) { | |
val index = itemIndex(offset.roundToInt()) | |
if (index != currentIndex) { | |
currentIndex = index | |
listener(index) | |
} | |
} | |
fun calculateOffsetLimit(): OffsetLimit { | |
val dimension = when (orientation) { | |
Orientation.Horizontal -> size.width | |
Orientation.Vertical -> size.height | |
} | |
val itemSideMargin = (dimension - itemDimension) / 2f | |
return OffsetLimit( | |
min = -dimension * overshootFraction + itemSideMargin, | |
max = numberOfItems * (itemDimension + itemSpacing) - (1f - overshootFraction) * dimension + itemSideMargin, | |
) | |
} | |
forEachGesture { | |
awaitPointerEventScope { | |
val tracker = VelocityTracker() | |
val decay = splineBasedDecay<Float>(this) | |
val down = awaitFirstDown() | |
val offsetLimit = calculateOffsetLimit() | |
val dragHandler = { change: PointerInputChange -> | |
scope?.launch { | |
val dragChange = change.calculateDragChange(orientation) | |
dragOffset.snapTo( | |
(dragOffset.value - dragChange).coerceIn( | |
offsetLimit.min, | |
offsetLimit.max | |
) | |
) | |
updateIndex(dragOffset.value) | |
} | |
tracker.addPosition(change.uptimeMillis, change.position) | |
} | |
when (orientation) { | |
Orientation.Horizontal -> horizontalDrag(down.id, dragHandler) | |
Orientation.Vertical -> verticalDrag(down.id, dragHandler) | |
} | |
val velocity = tracker.calculateVelocity(orientation) | |
scope?.launch { | |
var targetOffset = decay.calculateTargetValue(dragOffset.value, -velocity) | |
val remainder = targetOffset.toInt().absoluteValue % itemDimension | |
val extra = if (remainder > itemDimension / 2f) 1 else 0 | |
val lastVisibleIndex = | |
(targetOffset.absoluteValue / itemDimension.toFloat()).toInt() + extra | |
targetOffset = (lastVisibleIndex * (itemDimension + itemSpacing) * targetOffset.sign) | |
.coerceIn(0f, (numberOfItems - 1).toFloat() * (itemDimension + itemSpacing)) | |
dragOffset.animateTo( | |
animationSpec = animationSpec, | |
targetValue = targetOffset, | |
initialVelocity = -velocity | |
) { | |
updateIndex(value) | |
} | |
} | |
} | |
} | |
} | |
data class OffsetLimit( | |
val min: Float, | |
val max: Float, | |
) | |
} | |
private fun VelocityTracker.calculateVelocity(orientation: Orientation) = when (orientation) { | |
Orientation.Horizontal -> calculateVelocity().x | |
Orientation.Vertical -> calculateVelocity().y | |
} | |
private fun PointerInputChange.calculateDragChange(orientation: Orientation) = | |
when (orientation) { | |
Orientation.Horizontal -> positionChange().x | |
Orientation.Vertical -> positionChange().y | |
} |
That would be a list of objects that is used later to build a composable for each from the factory.
It just never actually defines items in the second version of the pager... should it be an argument...
edit:
I think I see now, it's using the items listed in the post in one of the clips... it's a little confusing since they're left off this
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
where does
items.lastIndex
,items
in general come from in the second Pager?