Jetpack Compose 滚轮选择器

文章目录


前言

此组件使用LazyColumn利用LazyListState相关属性实现滚轮选择器。支持设置默认选择值、滚动选中回调、循环滚动、自定义Item布局、自定义选中项覆盖层等

一、自定义Jetpack Compose 滚轮选择器

kotlin 复制代码
/**
 * 滚轮选择器组件
 *
 * @param modifier 修饰符
 * @param items 要显示的项目列表
 * @param selectedIndex 初始选中的项目索引
 * @param onItemSelected 选中项目时的回调函数
 * @param itemHeight 每个项目的高度
 * @param itemSpace 项目之间的间距
 * @param extraRow 额外的行数(在选中项上下显示)
 * @param isLooping 是否启用循环滚动
 * @param maxTiltAngle 滚轮倾斜的最大角度 [-360f,360f] 不应用倾斜传值0f
 * @param minAlpha 最小透明度比例 [0f,1f] 不应用透明度传值1f
 * @param minScaleX 最小缩放比例 [0f,1f] 不应用缩放传值1f
 * @param selectOverlay 覆盖在滚轮上的内容组件
 * @param itemContent 自定义项目内容的显示方式
 *
 */
@Composable
internal fun <T> WheelPicker(
    modifier: Modifier = Modifier,
    items: List<T>,
    selectedIndex: Int = 0,
    onItemSelected: (index: Int, item: T) -> Unit = { _, _ -> },
    itemHeight: Dp = 50.dp,
    itemSpace: Dp = 20.dp,
    extraRow: Int = 1,
    isLooping: Boolean = false,
    maxTiltAngle: Float = 0f,
    minAlpha: Float = 1f,
    minScaleX: Float = 1f,
    selectOverlay: @Composable () -> Unit = {},
    itemContent: @Composable (index: Int, item: T, isItemSelected: Boolean) -> Unit
) {
    // 当前选中的Item索引
    var selectedItemIndex by remember { mutableIntStateOf(selectedIndex) }

    val listState = if (!isLooping) {
        // 非循环滚动
        rememberLazyListState(selectedItemIndex)
    } else {
        // 循环滚动
        // 居中的参考点
        val initialIndex = Int.MAX_VALUE / 2
        // 目标索引
        val targetIndex = selectedItemIndex - extraRow
        // 计算上边界:离initialIndex最近的且能被size整除的位置加目标索引
        val upperLimit = (initialIndex / items.size) * items.size + targetIndex
        // 计算下边界
        val lowerLimit = upperLimit + items.size
        // 计算初始索引:取离initialIndex最近的边界
        val loopInitialIndex = if ((initialIndex - upperLimit) <= (lowerLimit - initialIndex)) {
            upperLimit
        } else {
            lowerLimit
        }
        rememberLazyListState(loopInitialIndex)
    }
    // 计算滚轮总高度
    val wheelHeight = (itemHeight * (extraRow * 2)) + (itemSpace * (extraRow * 2 + 2)) + itemHeight
    // 计算最大偏移量(Item高度 + 间距)
    val maxOffset = with(LocalDensity.current) { itemHeight.toPx() + itemSpace.toPx() }
    //可见首项索引
    val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }
    //可见首项偏移
    val firstVisibleItemScrollOffset by remember { derivedStateOf { listState.firstVisibleItemScrollOffset } }
    // 滚动时更新选中Item
    LaunchedEffect(firstVisibleItemScrollOffset) {
        val selected = if (isLooping) {
            // 循环滚动:计算当前选中项,考虑循环逻辑
            (firstVisibleItemIndex + if (firstVisibleItemScrollOffset > maxOffset / 2) extraRow + 1 else extraRow) % items.size
        } else {
            // 非循环滚动:计算当前选中项
            (firstVisibleItemIndex + if (firstVisibleItemScrollOffset > maxOffset / 2) 1 else 0) % items.size
        }
        onItemSelected(selected, items[selected])
        selectedItemIndex = selected
    }
    //是否滚动中
    val isScrolling by remember { derivedStateOf { listState.isScrollInProgress } }
    // 是否允许自动滚动
    var allowAutoScrolling by remember { mutableStateOf(true) }
    // 滚动停止时自动对齐到最近的Item
    LaunchedEffect(isScrolling) {
        if (!isScrolling) {
            if (allowAutoScrolling) {
                listState.animateScrollToItem(firstVisibleItemIndex + if (firstVisibleItemScrollOffset > maxOffset / 2) 1 else 0)
                allowAutoScrolling = false
            } else {
                allowAutoScrolling = true
            }
        }
    }
    // 绘制滚轮容器
    Box(
        modifier = modifier.height(wheelHeight),
        contentAlignment = Alignment.Center
    ) {
        //选中项覆盖层
        selectOverlay()
        // 可滚动列表
        LazyColumn(
            modifier = Modifier.fillMaxWidth(),
            state = listState,
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(itemSpace),
            contentPadding = PaddingValues(
                top = itemSpace,
                // 非循环滚动时底部需要额外的填充来保持选中项在中心
                bottom = if (isLooping) itemSpace else (itemSpace * (extraRow + 1)) + (itemHeight * extraRow)
            )
        ) {
            // 非循环滚动时在顶部添加空白间隔以保持选中项在中心
            if (!isLooping) {
                for (x in 1..extraRow) {
                    item {
                        Spacer(modifier = Modifier.height(itemHeight))
                    }
                }
            }
            // 绘制列表项
            val itemCount = if (isLooping) (Int.MAX_VALUE) else items.size
            items(itemCount) { index ->
                val itemIndex = index % items.size
                val item = items[itemIndex]
                val isItemSelected = selectedItemIndex == itemIndex
                val countIndex = index % itemCount
                val actualIndex = if (isLooping) countIndex - extraRow else itemIndex
                // 计算相对于选中项的偏移量,用于倾斜效果
                val offsetFromCenter = if (isLooping) {
                    val distance = (index - (firstVisibleItemIndex + extraRow)) % items.size
                    // 处理负数情况
                    if (distance > items.size / 2) distance - items.size
                    else if (distance < -items.size / 2) distance + items.size
                    else distance
                } else {
                    index - (firstVisibleItemIndex + if (firstVisibleItemScrollOffset > maxOffset / 2) 1 else 0)
                }
                Box(
                    modifier = Modifier
                        .height(itemHeight)
                        .graphicsLayer {
                            // 根据距离选中项的远近计算倾斜因子 [-1, 1]
                            val tiltFactor = (offsetFromCenter / (extraRow + 1).toFloat()).coerceIn(-1f, 1f)
                            rotationX = tiltFactor * maxTiltAngle
                            // 透明度
                            alpha = (1f - abs(tiltFactor) * (1f - minAlpha)).coerceIn(minAlpha, 1f)
                            // X轴缩放
                            scaleX = (1f - abs(tiltFactor) * (1f - minScaleX)).coerceIn(minScaleX, 1f)
                        },
                    contentAlignment = Alignment.Center,
                ) {
                    itemContent(itemIndex, item, isItemSelected)
                }
            }
        }
    }
}

二、使用

kotlin 复制代码
        /**
         * 循环滚动数字选择示例
         */
        WheelPicker(
            modifier = Modifier
                .fillMaxWidth()
                .background(color = Color.Gray.copy(alpha = 0.1f)),
            items = listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "10"),
            selectedIndex = 3,
            itemHeight = 40.dp,
            itemSpace = 10.dp,
            extraRow = 1,
            isLooping = true,
            maxTiltAngle = 0f,
            minAlpha = 1f,
            minScaleX = 1f,
            onItemSelected = { index, item ->
                ToastUtils.showShort("数字: index:$index,item:$item")
            },
            selectOverlay = {
                Box(
                    modifier = Modifier
                        .padding(start = 16.dp, end = 16.dp)
                        .fillMaxWidth()
                        .height(40.dp)
                        .border(width = 1.dp, color = Color.Red, shape = RoundedCornerShape(10.dp))
                )
            },
            itemContent = { index, item, isItemSelected ->
                Text(
                    text = item.toString(),
                    fontSize = if (isItemSelected) 28.sp else 18.sp,
                    color = if (isItemSelected) Color.Red else Color.Black
                )
            }
        )

		//------------------------------------------------------------

        /**
         * 城市选择示例
         */
        WheelPicker(
            modifier = Modifier
                .fillMaxWidth()
                .background(color = Color.Gray.copy(alpha = 0.1f)),
            items = listOf("北京", "上海", "深圳", "重庆", "长沙", "武汉", "南昌", "青海", "邵阳", "衡阳"),
            selectedIndex = 0,
            itemHeight = 50.dp,
            itemSpace = 10.dp,
            extraRow = 2,
            maxTiltAngle = -45f,
            minAlpha = 0.5f,
            minScaleX = 0.5f,
            isLooping = false,
            onItemSelected = { index, item ->
                ToastUtils.showShort("城市: index:$index,item:$item")
            },
            selectOverlay = {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(50.dp)
                        .background(color = Color.Blue.copy(alpha = 0.2f))
                )
            },
            itemContent = { index, item, isItemSelected ->
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center
                ) {
                    //...其它控件
                    Text(
                        text = item.toString(),
                        fontSize = 22.sp,
                        color = if (isItemSelected) Color.Blue else Color.Black
                    )
                }
            }
        )

三、效果

Compose 滚轮选择器效果

相关推荐
stevenzqzq18 小时前
Android Studio Logcat 基础认知
android·ide·android studio·日志
代码不停18 小时前
MySQL事务
android·数据库·mysql
朝花不迟暮19 小时前
使用Android Studio生成apk,卡在Running Gradle task ‘assembleDebug...解决方法
android·ide·android studio
yngsqq19 小时前
使用VS(.NET MAUI)开发第一个安卓APP
android·.net
Android-Flutter19 小时前
android compose LazyVerticalGrid上下滚动的网格布局 使用
android·kotlin
Android-Flutter19 小时前
android compose LazyHorizontalGrid水平滚动的网格布局 使用
android·kotlin
千里马-horse19 小时前
RK3399E Android 11 将自己的库放到系统库方法
android·so·设置系统库
美狐美颜sdk19 小时前
Android直播美颜SDK:选择指南与开发方案
android·人工智能·计算机视觉·第三方美颜sdk·视频美颜sdk·人脸美型sdk
我命由我1234519 小时前
Kotlin 面向对象 - 装箱与拆箱
android·java·开发语言·kotlin·android studio·android jetpack·android-studio