需求概述
我们经常能看到爱奇艺或者腾讯视频这类的视频APP在看电视剧的时候都会有一个选集的功能。如下图所示
这个功能其实很简单,就是绘制一些方块,在上面绘制上数字,还有标签啥的。当用户点击对应的数字式时可以切换到对应的剧集。如果剧集太多,屏幕展示不完,就可以滑动屏幕查看更多的剧集,就这么一个很简单的UI小组件。我们使用Compose来实现下。
效果展示
如果剧集少的时候,居左展示。如下
如果是横屏,则如下展示:
实现思路
可能很多读者会很容易的想到使用网格布局的控件LazyVerticalGrid实现,如果说不需要透明背景的话,这种方法是可行的,而且性能也会很好,但是如果要求背景可以设置透明度的话,这种方式就不行了,因为LazyVerticalGrid无法将背景设置成透明的,如我们的效果展示图中,可以看到我们的选集UI出现后,还可以看到后面的背景,如果使用LazyVerticalGrid,则无法实现这个效果。所以我们采用的方式是直接通过for循环绘制。使用两个for循环,分别负责绘制行和列,然后再处理点击的回调和选中的指示器就行了,我们可以使用Column,Box,Row,Text组件搭配使用,这些组件都是可以设置透明度的,能达到需求的效果。
代码实现
代码的实现很简单,就是一个composable函数,在代码中都做了注释,所以就不多废话了,原理也很简单,就是通过两个for循环分别绘制行和列,根据行和列之间的对应关系去计算显示的高度,padding等。
/**
* @param row 需要展示的行数
* @param col 需要展示的列数
* @param contentPadding 内容的padding,默认15dp
* @param displayRowCount 展示的函数,比如这个值为7,传入的row 为10,那么只会展示7行,多余的三行需要滑动查看,默认展示5行
* @param numberPadding 数字方块之间的padding,默认11dp
* @param contentTopPadding 内容顶部的padding,默认0dp
* @param currentNum 当前选中的数字,需要根据它绘制指示器
* @param isPortrait 是否是竖屏,需要根据横屏和竖屏来调整布局,默认我是竖屏
* @param
*/
@Composable
fun ShowDramaSelectUI(
row: Int,
col: Int,
contentPadding: Dp = 15.dp,
displayRowCount: Int = 5,
numberPadding: Dp = 11.dp,
contentTopPadding: Dp = 0.dp,
currentNum: Int = 1,
isPortrait: Boolean = true,
onNumSelect: (Int) -> Unit
) {
// 记录滚动的状态
val scrollState = rememberScrollState()
val displayHeight =
// 根据显示的行数计算容器的高度,下面的表达式不能换行,否则根据kotlin的语法特性,换行后的表达式不会参与计算
// 这里的60dp是数字的方块的大小,也可通过传参数指定
(displayRowCount) * (60.dp).value + (displayRowCount - 1) * numberPadding.value
// 记录选中的数字
var selectedNum by remember { mutableIntStateOf(currentNum) }
Log.d(TAG, "walt: selectedNum====>: $selectedNum")
//背景蒙层
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xCC000000))
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(
top = contentTopPadding,
start = contentPadding,
end = contentPadding
),
contentAlignment = Alignment.TopCenter
) {
Column(
modifier = Modifier
.wrapContentHeight()
.wrapContentWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier.fillMaxWidth().padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(
text = "选集", style = TextStyle(
color = Color(0xFF797F85),
fontSize = 14.sp,
fontWeight = FontWeight.Normal
)
)
} // 选集Box
Spacer(modifier = Modifier.height(10.dp).fillMaxWidth())
Column(
modifier = Modifier
.height(displayHeight.dp)
.fillMaxWidth()
// 让控件拥有滑动的能力
.verticalScroll(scrollState),
verticalArrangement = Arrangement.Top,
// 根据横竖屏设置对齐方式
horizontalAlignment = if ((isPortrait && (col < 5))
|| (!isPortrait && (col < 10))
) {
Alignment.Start
} else {
Alignment.CenterHorizontally
}
) {
// 绘制行
for (i in 1..row) {
Row(
modifier = Modifier.wrapContentWidth().wrapContentHeight()
) {
// 绘制列
for (j in col * (i - 1) + 1..(i - 1) * col + col) {
Box(
modifier = Modifier
.size(60.dp)
.clip(RoundedCornerShape(6.dp))
.background(Color(0x8031373D))
// 回调选中的数字
.clickable {
selectedNum = j
onNumSelect(selectedNum)
},
contentAlignment = Alignment.Center
) {
Text(
text = "$j",
style = TextStyle(
color = Color.White,
fontSize = 20.sp,
textAlign = TextAlign.Center
),
)
// 只有选中的数字和当前的数字相同时,才会展示指示器
if (j == selectedNum) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
Divider(
modifier = Modifier
.fillMaxWidth(),
thickness = 6.dp,
color = Color(0xFF037FF5)
)
}
}
}
// // 绘制两个列之间的间距,如果不是最后一个item,才加Spacer
if ((j != (i - 1) * col + col)) {
Spacer(modifier = Modifier.height(60.dp).width(numberPadding))
}
}
} // Row
// 绘制行之前的间距
Spacer(modifier = Modifier.height(numberPadding).fillMaxWidth())
}
}
}
}
}
测试代码
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyComposeTheme {
Box(modifier =
Modifier
.fillMaxSize()
){
Image(painter = painterResource(R.drawable.m10),
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
contentDescription = null)
ShowDramaSelectUI(row = 10, col = 10, isPortrait = true, onNumSelect = {
num->
Log.d(TAG,"$num has selected1 !!!")
})
}
}
}
}
}
总结
本文主要介绍的是一个剧集选集的功能,这里只是介绍了实现的方式,比较粗糙,读者可以按照自己的需求修改,有更好的实现方案也可以在评论区交流。本文主要起抛砖引玉的作用,也是记录自己实现的一个小需求。给需要的小伙伴打个样,欢迎交流指正。