diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt index 0553cd8..237f8a7 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/tabs/profile/ProfileV3.kt @@ -4,9 +4,11 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log +import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,7 +16,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -22,12 +23,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -37,12 +37,12 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -52,26 +52,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.runtime.collectAsState +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.aiosman.ravenow.AppState import com.aiosman.ravenow.ConstVars -import com.aiosman.ravenow.GuestLoginCheckOut -import com.aiosman.ravenow.GuestLoginCheckOutScene import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalNavController import com.aiosman.ravenow.MainActivity @@ -82,14 +80,9 @@ import com.aiosman.ravenow.entity.AgentEntity import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.composables.CustomAsyncImage -import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher -import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffold -import com.aiosman.ravenow.ui.composables.toolbar.ScrollStrategy -import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState import com.aiosman.ravenow.ui.index.IndexViewModel import com.aiosman.ravenow.ui.index.tabs.profile.composable.GalleryGrid -import com.aiosman.ravenow.ui.post.MenuActionItem import com.aiosman.ravenow.ui.index.tabs.profile.composable.GroupChatEmptyContent import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList @@ -97,23 +90,13 @@ import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserContentPageIndicator import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserItem import com.aiosman.ravenow.ui.modifiers.noRippleClickable -import com.aiosman.ravenow.ui.navigateToPost +import com.aiosman.ravenow.ui.post.MenuActionItem import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File -import androidx.compose.foundation.rememberScrollState -import androidx.compose.ui.res.stringResource -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.border -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.graphics.Brush import java.text.NumberFormat import java.util.Locale -import com.aiosman.ravenow.ui.points.PointsBottomSheet import kotlin.math.max @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @@ -233,16 +216,13 @@ fun ProfileV3( } val pointsBalanceState = PointService.pointsBalance.collectAsState(initial = null) - // 计算导航栏背景透明度,根据滚动位置从0到1 + // 计算导航栏背景透明度,根据滚动位置从0到1,在前段就达到完全不透明 val toolbarBackgroundAlpha by remember { derivedStateOf { - if (!isSelf) { - 1f - } else { - val maxScroll = 600f // 增加最大滚动距离,让渐变更平缓 - val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f) - progress - } + val maxScroll = 120f // 大幅减少最大滚动距离,让前段就完全不透明 + val progress = (scrollState.value.coerceAtMost(maxScroll.toInt()) / maxScroll).coerceIn(0f, 1f) + // 直接使用线性插值,尽快达到完全不透明 + progress } } @@ -518,7 +498,7 @@ fun ProfileV3( ) HorizontalPager( state = pagerState, - modifier = Modifier.height(500.dp) // 固定滚动高度 + modifier = Modifier.height(650.dp) // 固定滚动高度 ) { idx -> when (idx) { 0 -> GalleryGrid( @@ -703,24 +683,18 @@ fun TopNavigationBar( // 仅本人主页显示积分:收集全局积分 val pointsBalanceState = if (isSelf) PointService.pointsBalance.collectAsState(initial = null) else null - // 根据背景透明度和主题决定图标与边框颜色 - val iconColor = if (backgroundAlpha >= 0.7f) appColors.text else Color.White - val cardBorderColor = if (backgroundAlpha >= 0.7f) appColors.divider else Color.White - val toolbarSolidColor = remember(backgroundAlpha, appColors) { - appColors.background.copy(alpha = backgroundAlpha.coerceIn(0f, 1f)) + // 根据背景透明度和暗色模式决定图标颜色 + // 暗色模式下:图标始终为白色 + // 亮色模式下:根据背景透明度决定,透明度为1时变黑,否则为白色 + val iconColor = if (AppState.darkMode) { + Color.White // 暗色模式下图标始终为白色 + } else { + if (backgroundAlpha >= 1f) Color.Black else Color.White } - val toolbarOverlayBrush = remember(backgroundAlpha) { - val overlayAlpha = (1f - backgroundAlpha).coerceIn(0f, 1f) * 0.25f - if (overlayAlpha > 0f) { - Brush.verticalGradient( - colors = listOf( - Color.Black.copy(alpha = overlayAlpha), - Color.Transparent - ) - ) - } else { - null - } + val cardBorderColor = if (AppState.darkMode) { + Color.White // 暗色模式下边框应为白色 + } else { + if (backgroundAlpha >= 1f) Color.Black else Color.White } Box( @@ -731,25 +705,31 @@ fun TopNavigationBar( val statusBarHeight = statusBarPadding.calculateTopPadding() val navigationBarHeight = 56.dp // 增加导航栏高度,包括图标和额外空间 - // 导航栏背景层,包括状态栏区域,根据滚动位置逐渐变白 + // 导航栏背景层,包括状态栏区域,根据滚动位置逐渐显示实色填充 val totalHeight = statusBarHeight + navigationBarHeight + // 根据滚动位置计算背景颜色,从透明逐渐变为实色填充,尽快完成 + val toolbarBackgroundColor = remember(backgroundAlpha) { + val progress = backgroundAlpha.coerceIn(0f, 1f) + + if (AppState.darkMode) { + // 暗色模式下:从透明逐渐变为黑色实色填充 + Color.Black.copy(alpha = progress) + } else { + // 亮色模式下:从透明逐渐变为白色实色填充 + Color.White.copy(alpha = progress) + } + } + Box( modifier = Modifier .fillMaxWidth() .height(totalHeight) // 状态栏高度 + 导航栏高度 .align(Alignment.TopCenter) - .background(toolbarSolidColor) + .background( + color = toolbarBackgroundColor // 实色填充,不使用渐变 + ) ) - toolbarOverlayBrush?.let { brush -> - Box( - modifier = Modifier - .fillMaxWidth() - .height(totalHeight) - .align(Alignment.TopCenter) - .background(brush) - ) - } // 功能按钮区域,图标和文字根据背景透明度改变颜色 Row( @@ -761,13 +741,25 @@ fun TopNavigationBar( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { - // 左侧:互动数据卡片(仅本人主页显示) + // 左侧:互动数据卡片(仅自己的界面显示) if (isSelf) { + // 根据 toolbar 背景透明度动态调整卡片背景 + val cardBackgroundColor = remember(backgroundAlpha) { + val smoothProgress = backgroundAlpha.coerceIn(0f, 1f) + if (AppState.darkMode) { + // 暗色模式:从半透明白色逐渐变为更不透明的白色 + Color.White.copy(alpha = 0.52f + (0.48f * smoothProgress)) + } else { + // 亮色模式:从半透明白色逐渐变为完全不透明的白色 + Color.White.copy(alpha = 0.52f + (0.48f * smoothProgress)) + } + } + Row( modifier = Modifier .height(24.dp) .background( - color = Color.White.copy(alpha = 0.52f), + color = cardBackgroundColor, shape = RoundedCornerShape(16.dp) ) .border( @@ -797,25 +789,25 @@ fun TopNavigationBar( } Spacer(modifier = Modifier.width(16.dp)) + + // 中间:分享图标(仅自己的界面显示) + Image( + painter = painterResource(id = R.mipmap.menu_icon), + contentDescription = "分享", + modifier = Modifier + .size(24.dp) + .noRippleClickable { + onShareClick() + }, + colorFilter = ColorFilter.tint(iconColor) // 根据背景透明度改变颜色 + ) + + Spacer(modifier = Modifier.width(16.dp)) } - // 中间:分享图标 + // 右侧:菜单图标(三点图标) Image( - painter = painterResource(id = R.mipmap.menu_icon), - contentDescription = "分享", - modifier = Modifier - .size(24.dp) - .noRippleClickable { - onShareClick() - }, - colorFilter = ColorFilter.tint(iconColor) // 根据背景透明度改变颜色 - ) - - Spacer(modifier = Modifier.width(16.dp)) - - // 右侧:菜单图标 - Image( - painter = painterResource(id = R.mipmap.menu_ico), + painter = painterResource(id = R.drawable.rider_pro_more_horizon), contentDescription = "菜单", modifier = Modifier .size(24.dp) diff --git a/app/src/main/java/com/aiosman/ravenow/ui/points/PointsBottomSheet.kt b/app/src/main/java/com/aiosman/ravenow/ui/points/PointsBottomSheet.kt index a4d9862..302bd63 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/points/PointsBottomSheet.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/points/PointsBottomSheet.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.clickable @@ -38,8 +40,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged 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.rotate import androidx.compose.ui.geometry.CornerRadius @@ -111,7 +121,7 @@ fun PointsBottomSheet( containerColor = AppColors.background, dragHandle = null // 移除拖动手柄 ) { - Column( + Box( modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.95f) @@ -122,6 +132,11 @@ fun PointsBottomSheet( bottom = 8.dp ) ) { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { // 头部 - 使用 Box 实现绝对居中布局 Box( modifier = Modifier @@ -329,6 +344,7 @@ fun PointsBottomSheet( } else { HowToEarnList(onRecharge = onRecharge) } + } } } } @@ -443,22 +459,72 @@ private fun PointsHistoryList( ) { val AppColors = LocalAppTheme.current val numberFormat = remember { NumberFormat.getNumberInstance(Locale.getDefault()) } - - // 创建 NestedScrollConnection 来阻止滚动事件传播到弹窗 + 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 onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - // 消费剩余的滚动事件,防止传播到 ModalBottomSheet 导致弹窗关闭 + 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( + state = listState, modifier = Modifier .fillMaxWidth() .nestedScroll(nestedScrollConnection) @@ -499,12 +565,37 @@ private fun PointsHistoryList( ) } } - if (hasNext) { + // 显示加载状态 + if (loading && items.isNotEmpty()) { item { - Button(onClick = onLoadMore, modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp)) { - Text(stringResource(R.string.load_more)) + Box( + modifier = Modifier + .fillMaxWidth() + .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 + ) } } } @@ -581,15 +672,26 @@ private fun HowToEarnList(onRecharge: () -> Unit) { ) ) - // 创建 NestedScrollConnection 来阻止滚动事件传播到弹窗 + // 创建 NestedScrollConnection 来阻止滚动事件向上传播到 ModalBottomSheet val nestedScrollConnection = remember { object : NestedScrollConnection { - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - // 消费剩余的滚动事件,防止传播到 ModalBottomSheet 导致弹窗关闭 + 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 } }