Jetpack Compose : 超简单实现滚轮控件(WheelPicker)

前言

滚轮应该是我们很经常用到一个控件了,比如日期选择,时间选择,地区选择等都习惯用滚轮来展示。

滚轮控件的识点

上图是由三个滚轮控件组成的日期选择器,以此我们分析所需要的知识点:

  1. 手势(滑动,惯性滚动)
  2. 内容循环滚动
  3. 实现滚轮样式

手势(滑动,惯性滚动)

我首先想到的是 Compose滚动修饰符 效果如下:

接下来就是解决惯性问题了,我很自然想到列表组件 LazyColumn 就准备去看它的源码是怎么实现时。

好吧,就决定是你了 LazyColumn

内容循环滚动

既然决定使用 LazyColumn 那内容循环也变的简单,这里直接贴代码:

Kotlin 复制代码
val size = data.size
val count = Int.MAX_VALUE
val startIndex = count / 2
val listState = rememberPagerState(initialPage = startIndex - startIndex % size)

LazyColumn(
            modifier = Modifier,
            state = listState,
            flingBehavior = rememberSnapFlingBehavior(listState),
        ) {
            items(count) { index ->
                ......
            }
        }

实现滚轮样式

如何通过调整 LazyColumn 的 Item 项样式实现滚轮效果呢,这里放一张我画的草图:

由上面的草图我们发现关键点在 Item 项的旋转角度和平移距离。

原理竟然比草图还简单。

既然如此我们先拿到 Item 项的滑动时的偏移距离,直接贴代码:

Kotlin 复制代码
val listState = rememberPagerState(initialPage = startIndex - startIndex % size)
val layoutInfo by remember { derivedStateOf { listState.layoutInfo } }
LazyColumn(
            modifier = Modifier,
            state = listState,
            flingBehavior = rememberSnapFlingBehavior(listState),
        ) {
            items(count) { index ->
                val item = layoutInfo.visibleItemsInfo.find { it.index == index }
				if (item != null) {
					val itemCenterY = item.offset + item.size / 2 //获取Item项的偏移距离
				}
            }
        }

通过偏离距离计算调整系数。

Kotlin 复制代码
/**
* pickerCenterLinePx 滚轮控件中线
* itemCenterY < pickerCenterLinePx 说明Item项在上半部,逐渐缩小,反之则逐渐放大
**/

currentsAdjust = 0.75f + 0.25f * if (itemCenterY < pickerCenterLinePx) {
    itemCenterY / pickerCenterLinePx
} else {
    1 - (itemCenterY - pickerCenterLinePx) / pickerCenterLinePx
}

最后按照惯例贴上完整代码。

Kotlin 复制代码
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T> WheelPicker(
    data: List<T>,
    selectIndex: Int,
    visibleCount: Int,
    modifier: Modifier = Modifier,
    onSelect: (index: Int, item: T) -> Unit,
    content: @Composable (item: T) -> Unit,
) {
    BoxWithConstraints(modifier = modifier, propagateMinConstraints = true) {
        val density = LocalDensity.current
        val size = data.size
        val count = size * 10000
        val pickerHeight = maxHeight
        val pickerHeightPx = density.run { pickerHeight.toPx() }
        val pickerCenterLinePx = pickerHeightPx / 2
        val itemHeight = pickerHeight / visibleCount
        val itemHeightPx = pickerHeightPx / visibleCount
        val startIndex = count / 2
        val listState = rememberLazyListState(
            initialFirstVisibleItemIndex = startIndex - startIndex.floorMod(size) + selectIndex,
            initialFirstVisibleItemScrollOffset = ((itemHeightPx - pickerHeightPx) / 2).roundToInt(),
        )
        val layoutInfo by remember { derivedStateOf { listState.layoutInfo } }
        LazyColumn(
            modifier = Modifier,
            state = listState,
            flingBehavior = rememberSnapFlingBehavior(listState),
        ) {
            items(count) { index ->
                val currIndex = (index - startIndex).floorMod(size)
                val item = layoutInfo.visibleItemsInfo.find { it.index == index }
                var currentsAdjust = 1f
                if (item != null) {
                    val itemCenterY = item.offset + item.size / 2
                    currentsAdjust = 0.75f + 0.25f * if (itemCenterY < pickerCenterLinePx) {
                        itemCenterY / pickerCenterLinePx
                    } else {
                        1 - (itemCenterY - pickerCenterLinePx) / pickerCenterLinePx
                    }
                    if (!listState.isScrollInProgress
                        && item.offset < pickerCenterLinePx
                        && item.offset + item.size > pickerCenterLinePx
                    ) {
                        onSelect(currIndex, data[currIndex])
                    }
                }
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(itemHeight)
                        .graphicsLayer {
                            alpha = currentsAdjust
                            scaleX = currentsAdjust
                            scaleY = currentsAdjust
                            rotationX = (1 + currentsAdjust) * 180
                        },
                    contentAlignment = Alignment.Center,
                ) {
                    content(data[currIndex])
                }
            }
        }
    }
}

private fun Int.floorMod(other: Int): Int = when (other) {
    0 -> this
    else -> this - floorDiv(other) * other
}

Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。

如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。

谢谢~~

源代码地址

相关推荐
Libraeking3 小时前
破壁行动:在旧项目中丝滑嵌入 Compose(混合开发实战)
android·经验分享·android jetpack
市场部需要一个软件开发岗位3 小时前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
JMchen1235 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs6 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob6 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔6 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei9966 小时前
flutter和Android动画的对比
android·flutter·动画
lxysbly8 小时前
md模拟器安卓版带金手指2026
android
儿歌八万首9 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-194311 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed