android - Compose 中的椭圆列表
解决方案
我有一篇文章在这里展示了如何做到这一点。它不是LazyList
计算所有项目(但只呈现可见项目);您可以以此为基础进行构建。
完整的代码如下:
data class CircularListConfig(
val contentHeight: Float = 0f,
val numItems: Int = 0,
val visibleItems: Int = 0,
val circularFraction: Float = 1f,
val overshootItems: Int = 0,
)
@Stable
interface CircularListState {
val verticalOffset: Float
val firstVisibleItem: Int
val lastVisibleItem: Int
suspend fun snapTo(value: Float)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
fun offsetFor(index: Int): IntOffset
fun setup(config: CircularListConfig)
}
class CircularListStateImpl(
currentOffset: Float = 0f,
) : CircularListState {
private val animatable = Animatable(currentOffset)
private var itemHeight = 0f
private var config = CircularListConfig()
private var initialOffset = 0f
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
private val minOffset: Float
get() = -(config.numItems - 1) * itemHeight
override val verticalOffset: Float
get() = animatable.value
override val firstVisibleItem: Int
get() = ((-verticalOffset - initialOffset) / itemHeight).toInt().coerceAtLeast(0)
override val lastVisibleItem: Int
get() = (((-verticalOffset - initialOffset) / itemHeight).toInt() + config.visibleItems)
.coerceAtMost(config.numItems - 1)
override suspend fun snapTo(value: Float) {
val minOvershoot = -(config.numItems - 1 + config.overshootItems) * itemHeight
val maxOvershoot = config.overshootItems * itemHeight
animatable.snapTo(value.coerceIn(minOvershoot, maxOvershoot))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val constrainedValue = value.coerceIn(minOffset, 0f).absoluteValue
val remainder = (constrainedValue / itemHeight) - (constrainedValue / itemHeight).toInt()
val extra = if (remainder <= 0.5f) 0 else 1
val target =((constrainedValue / itemHeight).toInt() + extra) * itemHeight
animatable.animateTo(
targetValue = -target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override suspend fun stop() {
animatable.stop()
}
override fun setup(config: CircularListConfig) {
this.config = config
itemHeight = config.contentHeight / config.visibleItems
initialOffset = (config.contentHeight - itemHeight) / 2f
}
override fun offsetFor(index: Int): IntOffset {
val maxOffset = config.contentHeight / 2f + itemHeight / 2f
val y = (verticalOffset + initialOffset + index * itemHeight)
val deltaFromCenter = (y - initialOffset)
val radius = config.contentHeight / 2f
val scaledY = deltaFromCenter.absoluteValue * (config.contentHeight / 2f / maxOffset)
val x = if (scaledY < radius) {
sqrt((radius * radius - scaledY * scaledY))
} else {
0f
}
return IntOffset(
x = (x * config.circularFraction).roundToInt(),
y = y.roundToInt()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CircularListStateImpl
if (animatable.value != other.animatable.value) return false
if (itemHeight != other.itemHeight) return false
if (config != other.config) return false
if (initialOffset != other.initialOffset) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = animatable.value.hashCode()
result = 31 * result + itemHeight.hashCode()
result = 31 * result + config.hashCode()
result = 31 * result + initialOffset.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CircularListStateImpl, List<Any>>(
save = { listOf(it.verticalOffset) },
restore = {
CircularListStateImpl(it[0] as Float)
}
)
}
}
@Composable
fun rememberCircularListState(): CircularListState {
val state = rememberSaveable(saver = CircularListStateImpl.Saver) {
CircularListStateImpl()
}
return state
}
@Composable
fun CircularList(
visibleItems: Int,
modifier: Modifier = Modifier,
state: CircularListState = rememberCircularListState(),
circularFraction: Float = 1f,
overshootItems: Int = 3,
content: @Composable () -> Unit,
) {
check(visibleItems > 0) { "Visible items must be positive" }
check(circularFraction > 0f) { "Circular fraction must be positive" }
Layout(
modifier = modifier.clipToBounds().drag(state),
content = content,
) { measurables, constraints ->
val itemHeight = constraints.maxHeight / visibleItems
val itemConstraints = Constraints.fixed(width = constraints.maxWidth, height = itemHeight)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
state.setup(
CircularListConfig(
contentHeight = constraints.maxHeight.toFloat(),
numItems = placeables.size,
visibleItems = visibleItems,
circularFraction = circularFraction,
overshootItems = overshootItems,
)
)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
for (i in state.firstVisibleItem..state.lastVisibleItem) {
placeables[i].placeRelative(state.offsetFor(i))
}
}
}
}
private fun Modifier.drag(
state: CircularListState,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
verticalDrag(pointerId) { change ->
val verticalDragOffset = state.verticalOffset + change.positionChange().y
launch {
state.snapTo(verticalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
change.consumePositionChange()
}
}
val velocity = tracker.calculateVelocity().y
val targetValue = decay.calculateTargetValue(state.verticalOffset, velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}
推荐阅读
- ionic-framework - 使用传单进行实时跟踪
- php - 将 php mysql 项目上传到免费虚拟主机站点时如何修复 dbconn 文件中的错误?
- facebook - 在 Unity 中使用 Facebook 进行跨平台同步
- python - conditional selection on pandas dataframe
- networking - 当客户端向 DHCP 服务器请求 IP 地址时,为什么还要使用 ARP?
- hibernate - 找不到 Instant 类型的属性描述!在 Spring Data JPA 中
- matlab - 在 matlab 代码中使用 for 循环时出现内存不足错误
- python - python - 如何用重复的x轴绘制数据
- elasticsearch - (Grafana 表)${__cell} 包含撇号/单引号将查询字符串中断到 Kibana
- javascript - XMLHttpRequest javascript原型不起作用