这可以通过受https://docs.flutter.dev/cookbook/effects/photo-filter-carousel启发的寻呼机实现。我认为寻呼机的问题是我猜不是顺利的。除此之外,它适合用例。
@OptIn(ExperimentalPagerApi::class, ExperimentalSnapperApi::class)
@Composable
fun PagerDemo(modifier: Modifier = Modifier) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val contentPadding = (maxWidth - 50.dp) / 2
val offSet = maxWidth / 5
val itemSpacing = offSet - 50.dp
val pagerState = rememberPagerState()
val scope = rememberCoroutineScope()
HorizontalPager(
count = 30,
contentPadding = PaddingValues(horizontal = contentPadding),
modifier = modifier,
itemSpacing = itemSpacing,
state = pagerState
) { page ->
Box(
modifier = Modifier
.size(50.dp)
.graphicsLayer {
val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue
// Set the item alpha and scale based on the distance from the center
val percentFromCenter = 1.0f - (pageOffset / (5f / 2f))
val itemScale = 0.5f + (percentFromCenter * 0.5f).coerceIn(0f, 1f)
val opacity = 0.25f + (percentFromCenter * 0.75f).coerceIn(0f, 1f)
alpha = opacity
scaleY = itemScale
scaleX = itemScale
shape = CircleShape
clip = true
}
.background(color = colors[page % colors.size])
.clickable(
interactionSource = MutableInteractionSource(),
indication = null,
enabled = true,
) {
scope.launch {
pagerState.animateScrollToPage(page)
}
})
}
}
}
private val colors = listOf(
Color.Red,
Color.Green,
Color.Blue,
Color.Magenta,
Color.Yellow,
Color.Cyan,
)
第二种方式的灵感来自媒体上的博客https://fvilarino.medium.com/recreating-google-podcasts-speed-selector-in-jetpack-compose-7623203a009d。在这里,我想我们应该考虑根据单击的项目进行动画或平滑滚动到中心,而不是在单击时滚动到特定位置。
private val colors = listOf(
Color.Red,
Color.Green,
Color.Blue,
Color.Magenta,
Color.Yellow,
Color.Cyan,
)
@Stable
interface CarouselState {
val currentValue: Float
val range: ClosedRange<Int>
suspend fun snapTo(value: Float)
suspend fun scrollTo(value: Int)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
}
class CarouselStateImpl(
currentValue: Float,
override val range: ClosedRange<Int>,
) : CarouselState {
private val floatRange = range.start.toFloat()..range.endInclusive.toFloat()
private val animatable = Animatable(currentValue)
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
override val currentValue: Float
get() = animatable.value
override suspend fun stop() {
animatable.stop()
}
override suspend fun snapTo(value: Float) {
animatable.snapTo(value.coerceIn(floatRange))
}
override suspend fun scrollTo(value: Int) {
animatable.snapTo(value.toFloat().coerceIn(floatRange))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val target = value.roundToInt().coerceIn(range).toFloat()
animatable.animateTo(
targetValue = target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CarouselStateImpl
if (range != other.range) return false
if (floatRange != other.floatRange) return false
if (animatable != other.animatable) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = range.hashCode()
result = 31 * result + floatRange.hashCode()
result = 31 * result + animatable.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CarouselStateImpl, List<Any>>(
save = { listOf(it.currentValue, it.range.start, it.range.endInclusive) },
restore = {
CarouselStateImpl(
currentValue = it[0] as Float,
range = (it[1] as Int)..(it[2] as Int)
)
}
)
}
}
@Composable
fun rememberCarouselState(
currentValue: Float = 0f,
range: ClosedRange<Int> = 0..40,
): CarouselState {
val state = rememberSaveable(saver = CarouselStateImpl.Saver) {
CarouselStateImpl(currentValue, range)
}
LaunchedEffect(key1 = Unit) {
state.snapTo(state.currentValue.roundToInt().toFloat())
}
return state
}
@Composable
fun InstagramCarousel(
modifier: Modifier = Modifier,
state: CarouselState = rememberCarouselState(),
numSegments: Int = 5,
circleColor: Color = MaterialTheme.colors.onSurface,
currentValueLabel: @Composable (Int) -> Unit = { value -> Text(value.toString()) },
indicatorLabel: @Composable (Int) -> Unit = { value -> Text(value.toString()) },
) {
val context = LocalContext.current
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
currentValueLabel(state.currentValue.roundToInt())
//Icon(Icons.Filled.ArrowDropDown, contentDescription = null)
val scope = rememberCoroutineScope()
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.drag(state, numSegments),
contentAlignment = Alignment.Center,
) {
CenterCircle(
modifier = Modifier.align(Alignment.Center),
fillColor = Color(android.graphics.Color.parseColor("#4DB6AC")),
strokeWidth = 5.dp,
)
val segmentWidth = maxWidth / numSegments
val segmentWidthPx = constraints.maxWidth.toFloat() / numSegments.toFloat()
val halfSegments = (numSegments + 1) / 2
val start = (state.currentValue - halfSegments).toInt()
.coerceAtLeast(state.range.start)
val end = (state.currentValue + halfSegments).toInt()
.coerceAtMost(state.range.endInclusive)
val maxOffset = constraints.maxWidth / 2f
for (i in start..end) {
val offsetX = (i - state.currentValue) * segmentWidthPx
// alpha
val deltaFromCenter = (offsetX)
val percentFromCenter = 1.0f - abs(deltaFromCenter) / maxOffset
val alpha = 0.25f + (percentFromCenter * 0.75f)
// scale
val deltaFromCenterScale = (offsetX)
val percentFromCenterScale = 1.0f - abs(deltaFromCenterScale) / maxOffset
val scale = 0.5f + (percentFromCenterScale * 0.5f)
Column(
modifier = Modifier
.width(segmentWidth)
.wrapContentHeight(Alignment.CenterVertically)
.graphicsLayer(
translationX = offsetX,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.width(55.dp)
.height(55.dp)
.graphicsLayer(
alpha = alpha,
scaleY = scale,
scaleX = scale
)
.clip(CircleShape)
.background(colors[i % colors.size])
.clickable {
scope.launch {
state.scrollTo(i)
}
Toast
.makeText(context, "$i", Toast.LENGTH_SHORT)
.show()
}
)
// indicatorLabel(i)
}
}
}
}
}
@SuppressLint("ReturnFromAwaitPointerEventScope", "MultipleAwaitPointerEventScopes")
private fun Modifier.drag(
state: CarouselState,
numSegments: Int,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
val segmentWidthPx = size.width / numSegments
coroutineScope {
while (true) {
val pointerId =
awaitPointerEventScope { awaitFirstDown(pass = PointerEventPass.Initial).id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
val horizontalDragOffset =
state.currentValue - change.positionChange().x / segmentWidthPx
launch {
state.snapTo(horizontalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
if (change.positionChange() != Offset.Zero) change.consume()
}
}
val velocity = tracker.calculateVelocity().x / numSegments
val targetValue = decay.calculateTargetValue(state.currentValue, -velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}
@Preview(widthDp = 420)
@Composable
fun InstagramCarouselPreview() {
ComposeLearningTheme() {
Surface(modifier = Modifier.fillMaxWidth()) {
InstagramCarousel(
modifier = Modifier
.fillMaxWidth()
.clickable {
}
.padding(vertical = 16.dp),
currentValueLabel = { value ->
Text(
text = "${(value / 10)}.${(value % 10)}x",
style = MaterialTheme.typography.h6
)
},
indicatorLabel = { value ->
if (value % 5 == 0) {
Text(
text = "${(value / 10)}.${(value % 10)}",
style = MaterialTheme.typography.body2,
)
}
}
)
}
}
}
使用自定义布局的第三种方法在这里https://medium.com/@raghunandan2005/creating-instagram-like-carousel-in-compose-92d65de943a。待处理:获取中心项目索引,并在单击项目时平滑滚动到中心
您可以自定义它并实施您需要的解决方案。代码片段是不言自明的。您也可以使用圆形图像/按钮来代替圆形框。