修复积分弹窗列表滚动穿透及优化加载更多
- 通过`nestedScroll`修复`LazyColumn`在`ModalBottomSheet`中滚动时导致底部工作表(BottomSheet)也跟着滚动的问题。 - 移除手动点击的“加载更多”按钮,改为滚动到底部时自动加载更多积分记录。 - 引入`debounce`和`distinctUntilChanged`来优化滚动加载逻辑,防止重复触发。 - 在积分列表底部添加了“加载中”和“已经到底”的提示文本。 - 调整了Tab项的布局,使用`SpaceBetween`使其均匀分布。
This commit is contained in:
@@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -30,8 +32,16 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -71,12 +81,17 @@ fun PointsBottomSheet(
|
|||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
containerColor = AppColors.background
|
containerColor = AppColors.background
|
||||||
) {
|
) {
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.9f)
|
.fillMaxHeight(0.9f)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
// 头部
|
// 头部
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -156,7 +171,7 @@ fun PointsBottomSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
TabItem(
|
TabItem(
|
||||||
@@ -165,7 +180,6 @@ fun PointsBottomSheet(
|
|||||||
onClick = { tab = 0 },
|
onClick = { tab = 0 },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
TabSpacer()
|
|
||||||
TabItem(
|
TabItem(
|
||||||
text = stringResource(R.string.how_to_earn),
|
text = stringResource(R.string.how_to_earn),
|
||||||
isSelected = tab == 1,
|
isSelected = tab == 1,
|
||||||
@@ -186,6 +200,7 @@ fun PointsBottomSheet(
|
|||||||
} else {
|
} else {
|
||||||
HowToEarnList()
|
HowToEarnList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,9 +226,75 @@ private fun PointsHistoryList(
|
|||||||
) {
|
) {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) }
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val loading = PointsViewModel.loading
|
||||||
|
var lastLoadTriggeredIndex by remember { mutableStateOf(-1) }
|
||||||
|
var previousItemsSize by remember { mutableStateOf(items.size) }
|
||||||
|
|
||||||
|
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
|
||||||
|
// 使用 onPostScroll 来消费 LazyColumn 处理后的剩余滚动事件
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// 不消费任何事件,让 LazyColumn 先处理
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// 消费 LazyColumn 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
// 不消费惯性滚动,让 LazyColumn 先处理
|
||||||
|
return Velocity.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
// 消费 LazyColumn 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当数据加载完成后,重置触发索引,以便可以继续加载
|
||||||
|
LaunchedEffect(items.size, loading) {
|
||||||
|
if (items.size > previousItemsSize && !loading) {
|
||||||
|
// 数据已加载完成,重置触发索引
|
||||||
|
lastLoadTriggeredIndex = -1
|
||||||
|
previousItemsSize = items.size
|
||||||
|
} else if (items.size != previousItemsSize) {
|
||||||
|
previousItemsSize = items.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听滚动位置,接近底部时自动加载更多
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow {
|
||||||
|
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
|
}
|
||||||
|
.debounce(500) // 防抖500ms,等待滚动停止后再触发,避免快速滚动时频繁触发
|
||||||
|
.distinctUntilChanged() // 只在值变化时触发
|
||||||
|
.collect { lastVisibleIndex ->
|
||||||
|
if (lastVisibleIndex >= 0 && hasNext && !loading) {
|
||||||
|
val totalItems = items.size
|
||||||
|
// 当滚动到倒数第3个item时,触发加载更多
|
||||||
|
// 并且确保不会重复触发(检查上次触发的索引)
|
||||||
|
if (totalItems > 0 &&
|
||||||
|
lastVisibleIndex >= totalItems - 3 &&
|
||||||
|
lastVisibleIndex != lastLoadTriggeredIndex) {
|
||||||
|
lastLoadTriggeredIndex = lastVisibleIndex
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth()
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
items(items) { item ->
|
items(items) { item ->
|
||||||
Row(
|
Row(
|
||||||
@@ -249,12 +330,37 @@ private fun PointsHistoryList(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasNext) {
|
// 显示加载状态
|
||||||
|
if (loading && items.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Button(onClick = onLoadMore, modifier = Modifier
|
Box(
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(top = 8.dp)) {
|
.fillMaxWidth()
|
||||||
Text(stringResource(R.string.load_more))
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.load_more),
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 显示已经到底提示
|
||||||
|
if (!hasNext && items.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "已经到底",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,6 +371,31 @@ private fun PointsHistoryList(
|
|||||||
private fun HowToEarnList() {
|
private fun HowToEarnList() {
|
||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
|
|
||||||
|
// 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// 不消费任何事件,让 LazyColumn 先处理
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// 消费 LazyColumn 处理后的剩余滚动事件,防止传递到 ModalBottomSheet
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
// 不消费惯性滚动,让 LazyColumn 先处理
|
||||||
|
return Velocity.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
// 消费 LazyColumn 处理后的剩余惯性滚动,防止传递到 ModalBottomSheet
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RowItem(title: String, desc: String, amount: String) {
|
fun RowItem(title: String, desc: String, amount: String) {
|
||||||
Row(
|
Row(
|
||||||
@@ -294,7 +425,9 @@ private fun HowToEarnList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
RowItem(stringResource(R.string.new_user_reward), stringResource(R.string.new_user_reward_desc), "+500")
|
RowItem(stringResource(R.string.new_user_reward), stringResource(R.string.new_user_reward_desc), "+500")
|
||||||
|
|||||||
Reference in New Issue
Block a user