需求概述

我们经常能看到爱奇艺或者腾讯视频这类的视频APP在看电视剧的时候都会有一个选集的功能。如下图所示

Android Jetpack Compose 实现一个电视剧选集界面-LMLPHP这个功能其实很简单,就是绘制一些方块,在上面绘制上数字,还有标签啥的。当用户点击对应的数字式时可以切换到对应的剧集。如果剧集太多,屏幕展示不完,就可以滑动屏幕查看更多的剧集,就这么一个很简单的UI小组件。我们使用Compose来实现下。

效果展示

Android Jetpack Compose 实现一个电视剧选集界面-LMLPHP

如果剧集少的时候,居左展示。如下
Android Jetpack Compose 实现一个电视剧选集界面-LMLPHP
如果是横屏,则如下展示:
Android Jetpack Compose 实现一个电视剧选集界面-LMLPHP

实现思路

可能很多读者会很容易的想到使用网格布局的控件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 !!!")
                    })
                }
            }
        }
    }
}

总结

本文主要介绍的是一个剧集选集的功能,这里只是介绍了实现的方式,比较粗糙,读者可以按照自己的需求修改,有更好的实现方案也可以在评论区交流。本文主要起抛砖引玉的作用,也是记录自己实现的一个小需求。给需要的小伙伴打个样,欢迎交流指正。

06-11 20:25