文章目录
前言
此组件使用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 滚轮选择器效果