股票APP中美股期权链双向滑动列表UI的 Jetpack Compose实现

2,337 阅读5分钟

引言

在股票类APP的开发中,表格控件是经常被使用到的,除了实现自选股列表这种,固定左边第一列,右边滑动更多列的方式,如博主的另一篇文章里的效果那样:《快速实现股票APP里自选股左固定右滑动表格列表--SmartTableRecycleView》

还有一种表格效果是需要实现对称滑动(双向滑动)的功能,看起来有种双向奔赴的感觉:

实现效果

d1.gif 横屏下的效果 d2.gif

这种效果在港美股的APP期权链里就很常见,看到这里,不妨思考一下,如果用Android传统的View来做的话,你会怎么实现呢?

作为Android开发者,一看到列表,我们的第一直觉就是RecycleView。是,对于这种复杂的列表,想要流畅的实现肯定是非RecycleView莫属了。我们看效果图,列表里的每个Item都是可以滑动的,上下一起滑动,而且左右两边的滑动效果是左右对称的,你把左边的部分向右滑动,那右边的部分就会向左滑动。特别像刚相爱的恋人那样,你靠近我一点,我就更靠近你一点,属于是双向奔赴了,联想到这场景,简直甜蜜死了。

闲话少说,如果用传统的RecycleView,一种思路是Item布局里面放两个HorizontalScrollView,相互设置滑动监听,这样做是可以实现的,但是那逻辑和代码量也绝对是够大的了,各种adapter,需要代码动态添加View设置宽高,监听listener等等,想到头就开始疼了。

能不能快速实现呢?这就非得是强大的Jetpack Compose莫属了。

图片.png 不需要很多代码,三个核心的类就能实现,下面就详细介绍如何使用Jetpack Compose来快速的实现这个效果,给大家提供一些思路。

实现原理

双向滑动的UI布局代码实现:OppositeScrollTable UI组件

@Composable
fun OppositeScrollTable(
    tableDataSet: TableViewDataSet,
    onHeaderClick: (TableViewHeaderEntity, Boolean) -> Unit,
) {
    val horizontalScrollState = rememberScrollState()
    val lazyListState = rememberLazyListState()
    LaunchedEffect(Unit) {
        horizontalScrollState.scrollTo(horizontalScrollState.maxValue)
    }
    val headers = tableDataSet.headers
    var mSelectIndex by remember { mutableIntStateOf(-1) }
    var mIndicatorIndex by remember { mutableIntStateOf(7) }

    Column(
        modifier = Modifier
            .fillMaxHeight()
    ) {
        ScrolledCellItemHeader(horizontalScrollState, tableDataSet, onHeaderClick = {})
        CommDivider()
        LazyColumn() {
            itemsIndexed(tableDataSet.childItems) { i, item ->
                Box(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Column {
                        ScrollTableCellItem(
                            horizontalScrollState,
                            item,
                            headers,
                            mSelectIndex == i,
                            onItemClick = {
                                mSelectIndex = i
                            })
                        HorizontalDivider(color = ColorDivide)
                    }
                    IndicatorView(i, mIndicatorIndex, item)
                }
            }
        }

    }
}

看代码我们UI分为两个部分,顶部是Header,下面是列表,列表是用LazyColumn来实现的,而且注意最外层有个horizontalScrollState的ScrollState滑动状态,这个很重要,我们先把这个滑动状态共享传给Header方法组件。

@Composable
internal fun ScrolledCellItemHeader(
    horizontalScrollState: ScrollState,
    dataSet: TableViewDataSet,
    onHeaderClick: (TableViewHeaderEntity) -> Unit,
) {
    val headers = dataSet.headers
    val headerList = remember { headers }
    val localHeaderList = remember {
        mutableStateListOf<TableViewHeaderEntity>().apply {
            addAll(headerList)
        }
    }

    Row(
        Modifier
            .height(35.dp)
            .background(Color.White),
        horizontalArrangement = Arrangement.SpaceEvenly,
        verticalAlignment = Alignment.CenterVertically
    ) {
        MoveHeaderItemView(
            Modifier
                .weight(0.5f), false, horizontalScrollState, localHeaderList, onHeaderClick
        )
        CenterText("行权价")
        MoveHeaderItemView(
            Modifier
                .weight(0.5f), true, horizontalScrollState, localHeaderList, onHeaderClick
        )
    }

}

Header组件中我们看到有一个Row就是行,里面放了两个MoveHeaderItemView,其实是两个可以滑动的Row组件,再看MoveHeaderItemView里的代码:

@Composable
private fun MoveHeaderItemView(
    modifier: Modifier,
    isRight: Boolean,
    horizontalScrollState: ScrollState,
    localHeaderList: SnapshotStateList<TableViewHeaderEntity>,
    onHeaderClick: (TableViewHeaderEntity) -> Unit,
) {
    Box(
        modifier = modifier
            .clickable {

            }) {
        Row(
            modifier = Modifier
                .fillMaxHeight()
                .horizontalScroll(horizontalScrollState, reverseScrolling = isRight),
            verticalAlignment = Alignment.CenterVertically
        ) {
            val displayedList = if (isRight) localHeaderList else localHeaderList.asReversed()
            repeat(displayedList.size) { index ->
                val headerItem = displayedList[index]
                Row(
                    Modifier
                        .fillMaxHeight()
                        .width(headerItem.width),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Text(
                        text = headerItem.title,
                        textAlign = TextAlign.Center,
                        fontSize = 13.sp
                    )
                }
            }
        }
    }
}

关键代码:horizontalScroll(horizontalScrollState, reverseScrolling = isRight)

其实列表双向滑动的精髓就在这行代码上,我们把horizontalScrollState事件传给header里左边Row时,设置让它正常滚动,就是和手指移动的方向是相同的,reverseScrolling设置为false。同时我们把horizontalScrollState事件传给右边的Row的时候reverseScrolling设置为true,让它反向滑动,意思是和手指滑动的方向相反,于是我们就实现了对称滑动的效果。

同时因为左边的数据和右边的数据是反的,左边的数据要倒序一下才能遍历绘制。

我们知道HorizontalScrollView里面的控件一开始是在最左边的,Row也是一样,所以我们在组件可见的时候,我们把列表左边的部分先滑动到最右边,而右边的部分本身就是在最左边的。然后用户一开始滑左边部分,因为已经是在最右边了,所以只能往左边滑动。右边部分默认是在最左边,就只能往右边滑动。

LaunchedEffect(Unit) { horizontalScrollState.scrollTo(horizontalScrollState.maxValue) }

完成了Header标题栏左右列表对称滑动的效果后,item内容的左右列表的双向滑动其实也是一样的实现方式,这里就不再过多说明了。

定义数据类

为了能够复用,定义了数据类data class支持传入不一样的宽度,字体和颜色,方便后面的UI定制和多页面的复用:

enum class TableViewSort {
    ASC,//升序
    DESC,//降序
    NONE
}
data class TableViewDataSet(
    val middleColumName: String,
    val middleColumWith: Dp,
    val itemHeight: Dp=50.dp,
    val childItems:ArrayList<TabViewItemsEntity>,
    val headers:ArrayList<TableViewHeaderEntity>,
)
data class TabViewItemsEntity(
    val childItemsLeft:ArrayList<TableViewChildItemEntity>,
    val childItemsMiddle: String,
    val childItemsRight:ArrayList<TableViewChildItemEntity>,
)

data class TableViewHeaderEntity(
    val  title:String = "",
    val  width:Dp = 60.dp,
    var  asc: TableViewSort? = null,
    var  sortType:Int = 0
)
data class TableViewChildItemEntity(
    val value:String,
    val color: Color? = null,
    val textSize: TextUnit? = null,
)

定义好排序的方向和排序类型,后期也可以支持排序功能。这样我们只要拼装一个完整的TableViewDataSet就能实现这个效果了,在Activity中调用代码如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            OppositeScrollListTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    val data = getOptionChainData(30)
                    Column(
                        modifier = Modifier.padding(innerPadding),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(
                            text ="期权链",
                            color = Color.Black,
                        )
                        Spacer(modifier = Modifier.height(10.dp))
                        OppositeScrollTable(
                            data,
                            onHeaderClick = { header, isHeader -> })
                    }
                }
            }
        }
    }
}

总结

到此,打完收工,是不是比传统View的实现来的更加简单。Jetpack Compose总能给人以一种快速又意想不到的方式实现传统View里的一些看着实现起来很复杂的UI效果。而且官方也不断得在给Jetpack Compose更新新功能,加上Android Studio上对Jetpack Compose支持越来越完善,实时预览,多主题,多语言预览等,开发体验真是太棒了,Android小伙伴们,早点用起来吧。

具体实现细节可查看github地址:github.com/finddreams/… 希望能对你有所帮助。