引言
在股票类APP的开发中,表格控件是经常被使用到的,除了实现自选股列表这种,固定左边第一列,右边滑动更多列的方式,如博主的另一篇文章里的效果那样:《快速实现股票APP里自选股左固定右滑动表格列表--SmartTableRecycleView》
还有一种表格效果是需要实现对称滑动(双向滑动)的功能,看起来有种双向奔赴的感觉:
实现效果
横屏下的效果
这种效果在港美股的APP期权链里就很常见,看到这里,不妨思考一下,如果用Android传统的View来做的话,你会怎么实现呢?
作为Android开发者,一看到列表,我们的第一直觉就是RecycleView。是,对于这种复杂的列表,想要流畅的实现肯定是非RecycleView莫属了。我们看效果图,列表里的每个Item都是可以滑动的,上下一起滑动,而且左右两边的滑动效果是左右对称的,你把左边的部分向右滑动,那右边的部分就会向左滑动。特别像刚相爱的恋人那样,你靠近我一点,我就更靠近你一点,属于是双向奔赴了,联想到这场景,简直甜蜜死了。
闲话少说,如果用传统的RecycleView,一种思路是Item布局里面放两个HorizontalScrollView,相互设置滑动监听,这样做是可以实现的,但是那逻辑和代码量也绝对是够大的了,各种adapter,需要代码动态添加View设置宽高,监听listener等等,想到头就开始疼了。
能不能快速实现呢?这就非得是强大的Jetpack Compose莫属了。
不需要很多代码,三个核心的类就能实现,下面就详细介绍如何使用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/… 希望能对你有所帮助。