导航切换动画调整

将默认的淡入淡出动画效果替换为更接近iOS风格的侧滑动画,提升页面切换的流畅度和视觉体验。

**具体变更:**

*   **页面进入:** 新页面从右侧滑入。
*   **页面退出:** 当前页面向右侧滑出,前一页面从左侧轻微偏移处滑回。
*   **动画时长:** 统一设置为280毫秒。

**影响范围:**

*   图片详情页 (`ImagePagerScreen`)
*   创建群聊页 (`CreateGroupChatScreen`)

**其他优化:**

*   **创建群聊页UI调整:**
    *   群聊名称输入框样式统一,采用圆角灰色背景。
    *   底部创建按钮适配导航栏高度。
    *   列表区域自适应填满剩余空间,防止内容被遮挡。
    *   选择成员列表项固定高度,避免选中状态变化时布局跳动。
    *   为头像和选择框添加默认图和占位图。
*   **ImageLoader优化:**
    *   实现全局共享的 `ImageLoader` 实例,避免重复创建,提高内存缓存利用率。
*   **列表性能优化:**
    *   为好友列表和AI助手列表的 `items` 添加 `key`,提升列表项更新效率。
*   **资源清理调整:**
    *   移除了在离开首页和动态页时全量清理资源的操作,以避免返回时列表重置或不必要的重新加载。
*   **ProfileV3页代码清理:**
    *   移除未使用的导入。
This commit is contained in:
2025-08-31 21:13:06 +08:00
parent 21200910c1
commit 5759d4ec95
8 changed files with 126 additions and 93 deletions

View File

@@ -8,6 +8,8 @@ import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
@@ -163,16 +165,32 @@ fun NavigationController(
navArgument("initImagePagerIndex") { type = NavType.IntType } navArgument("initImagePagerIndex") { type = NavType.IntType }
), ),
enterTransition = { enterTransition = {
fadeIn(animationSpec = tween(durationMillis = 200)) // iOS push: new screen slides in from the right
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(durationMillis = 280)
)
}, },
exitTransition = { exitTransition = {
fadeOut(animationSpec = tween(durationMillis = 200)) // iOS push: previous screen shifts slightly left (parallax)
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(durationMillis = 280)
)
}, },
popEnterTransition = { popEnterTransition = {
fadeIn(animationSpec = tween(durationMillis = 200)) // iOS pop: previous screen slides back from slight left offset
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(durationMillis = 280)
)
}, },
popExitTransition = { popExitTransition = {
fadeOut(animationSpec = tween(durationMillis = 200)) // iOS pop: current screen slides out to the right
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(durationMillis = 280)
)
} }
) { backStackEntry -> ) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id") val id = backStackEntry.arguments?.getString("id")
@@ -448,6 +466,30 @@ fun NavigationController(
composable( composable(
route = NavigationRoute.CreateGroupChat.route, route = NavigationRoute.CreateGroupChat.route,
enterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(durationMillis = 280)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(durationMillis = 280)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth / 3 },
animationSpec = tween(durationMillis = 280)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(durationMillis = 280)
)
}
) { ) {
CreateGroupChatScreen() CreateGroupChatScreen()
} }

View File

@@ -91,7 +91,10 @@ fun AiAgentListScreen(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(filteredAgents) { agent -> items(
items = filteredAgents,
key = { it.id }
) { agent ->
MemberItem( MemberItem(
member = agent, member = agent,
isSelected = selectedMemberIds.contains(agent.id), isSelected = selectedMemberIds.contains(agent.id),

View File

@@ -88,7 +88,7 @@ fun CreateGroupChatScreen() {
} }
} }
val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent) systemUiController.setNavigationBarColor(Color.Transparent)
@@ -97,7 +97,7 @@ fun CreateGroupChatScreen() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(bottom = navigationBarPaddings) .background(AppColors.background)
) { ) {
// 错误提示 // 错误提示
CreateGroupChatViewModel.errorMessage?.let { error -> CreateGroupChatViewModel.errorMessage?.let { error ->
@@ -177,7 +177,7 @@ fun CreateGroupChatScreen() {
color = AppColors.inputBackground, color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.padding(horizontal = 12.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 13.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -247,11 +247,19 @@ fun CreateGroupChatScreen() {
// ) // )
// } // }
// 群聊名称输入 // 群聊名称输入:同一圆角灰色矩形容器
Row( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp)
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
@@ -268,24 +276,22 @@ fun CreateGroupChatScreen() {
fontSize = 14.sp fontSize = 14.sp
), ),
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f),
.background(
color = AppColors.inputBackground,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp),
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
Box(Modifier.fillMaxWidth()) {
if (groupName.text.isEmpty()) { if (groupName.text.isEmpty()) {
Text( Text(
text = stringResource(R.string.group_name_hint), text = stringResource(R.string.group_name_hint),
color = Color(0xFF999999), color = AppColors.inputHint,
fontSize = 14.sp fontSize = 14.sp
) )
} }
innerTextField() innerTextField()
} }
}
) )
} }
}
// 已选成员列表 // 已选成员列表
if (selectedMembers.isNotEmpty()) { if (selectedMembers.isNotEmpty()) {
@@ -313,6 +319,9 @@ fun CreateGroupChatScreen() {
context = context, context = context,
imageUrl = member.avatar, imageUrl = member.avatar,
contentDescription = member.name, contentDescription = member.name,
defaultRes = R.drawable.default_avatar,
placeholderRes = R.drawable.default_avatar,
errorRes = R.drawable.default_avatar,
modifier = Modifier modifier = Modifier
.size(48.dp) .size(48.dp)
.clip(CircleShape) .clip(CircleShape)
@@ -336,7 +345,7 @@ fun CreateGroupChatScreen() {
) { ) {
Text( Text(
text = "×", text = "×",
color = Color.White, color = AppColors.mainText,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -393,12 +402,12 @@ fun CreateGroupChatScreen() {
) )
} }
// 内容区域 // 内容区域 - 自适应填满剩余高度
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f) // 这里让列表占据剩余空间
) { ) {
when (it) { when (it) {
0 -> { 0 -> {
@@ -432,7 +441,7 @@ fun CreateGroupChatScreen() {
} }
} }
// 创建群聊按钮 // 创建群聊按钮 - 固定在底部
Button( Button(
onClick = { onClick = {
// 创建群聊逻辑 // 创建群聊逻辑
@@ -451,7 +460,7 @@ fun CreateGroupChatScreen() {
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp), .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = navigationBarPadding + 16.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = AppColors.main, containerColor = AppColors.main,
contentColor = AppColors.mainText contentColor = AppColors.mainText

View File

@@ -91,7 +91,10 @@ fun FriendListScreen(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(filteredFriends) { friend -> items(
items = filteredFriends,
key = { it.id }
) { friend ->
MemberItem( MemberItem(
member = friend, member = friend,
isSelected = selectedMemberIds.contains(friend.id), isSelected = selectedMemberIds.contains(friend.id),

View File

@@ -2,8 +2,6 @@ package com.aiosman.ravenow.ui.group
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -14,7 +12,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.LocalAppTheme import com.aiosman.ravenow.LocalAppTheme
import com.aiosman.ravenow.R
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.Checkbox
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
@Composable @Composable
@@ -29,14 +29,18 @@ fun MemberItem(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp) // 固定高度防止跳动
.noRippleClickable { onSelect() } .noRippleClickable { onSelect() }
.padding(vertical = 8.dp), .padding(horizontal = 0.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CustomAsyncImage( CustomAsyncImage(
context = context, context = context,
imageUrl = member.avatar, imageUrl = member.avatar,
contentDescription = member.name, contentDescription = member.name,
defaultRes = R.drawable.default_avatar,
placeholderRes = R.drawable.default_avatar,
errorRes = R.drawable.default_avatar,
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
@@ -54,12 +58,9 @@ fun MemberItem(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Checkbox( Checkbox(
size = 20,
checked = isSelected, checked = isSelected,
onCheckedChange = { onSelect() }, onCheckedChange = { onSelect() }
colors = CheckboxDefaults.colors(
checkedColor = AppColors.main,
uncheckedColor = AppColors.secondaryText
)
) )
} }
} }

View File

@@ -100,12 +100,7 @@ fun IndexScreen() {
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
val context = LocalContext.current val context = LocalContext.current
// 页面退出时清理所有资源 // 注意:不要在离开 Index 路由时全量清理资源,以免返回后列表被重置
DisposableEffect(Unit) {
onDispose {
ResourceCleanupManager.cleanupAllResources(context)
}
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
systemUiController.setNavigationBarColor(Color.Transparent) systemUiController.setNavigationBarColor(Color.Transparent)
} }
@@ -378,12 +373,7 @@ fun Home() {
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode) systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
} }
// 页面退出时清理动态相关资源 // 注意:避免在离开 Home 时清理动态资源,防止返回详情后触发重新加载
DisposableEffect(Unit) {
onDispose {
ResourceCleanupManager.cleanupPageResources("moment")
}
}
Column( Column(
modifier = Modifier modifier = Modifier

View File

@@ -6,10 +6,8 @@ import android.net.Uri
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@@ -22,11 +20,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -34,8 +31,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
@@ -60,9 +55,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.aiosman.ravenow.AppState import com.aiosman.ravenow.AppState
@@ -76,25 +69,20 @@ import com.aiosman.ravenow.entity.AgentEntity
import com.aiosman.ravenow.entity.MomentEntity import com.aiosman.ravenow.entity.MomentEntity
import com.aiosman.ravenow.ui.NavigationRoute import com.aiosman.ravenow.ui.NavigationRoute
import com.aiosman.ravenow.ui.composables.CustomAsyncImage import com.aiosman.ravenow.ui.composables.CustomAsyncImage
import com.aiosman.ravenow.ui.composables.DropdownMenu
import com.aiosman.ravenow.ui.composables.MenuItem
import com.aiosman.ravenow.ui.composables.StatusBarSpacer import com.aiosman.ravenow.ui.composables.StatusBarSpacer
import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher import com.aiosman.ravenow.ui.composables.pickupAndCompressLauncher
import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffold import com.aiosman.ravenow.ui.composables.toolbar.CollapsingToolbarScaffold
import com.aiosman.ravenow.ui.composables.toolbar.ScrollStrategy import com.aiosman.ravenow.ui.composables.toolbar.ScrollStrategy
import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState import com.aiosman.ravenow.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState
import com.aiosman.ravenow.ui.index.IndexViewModel import com.aiosman.ravenow.ui.index.IndexViewModel
import com.aiosman.ravenow.ui.index.tabs.profile.composable.EmptyMomentPostUnit
import com.aiosman.ravenow.ui.index.tabs.profile.composable.MomentPostUnit
import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction import com.aiosman.ravenow.ui.index.tabs.profile.composable.SelfProfileAction
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsRow
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserAgentsList
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.UserContentPageIndicator
import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserItem import com.aiosman.ravenow.ui.index.tabs.profile.composable.UserItem
import com.aiosman.ravenow.ui.modifiers.noRippleClickable import com.aiosman.ravenow.ui.modifiers.noRippleClickable
import com.aiosman.ravenow.ui.navigateToPost import com.aiosman.ravenow.ui.navigateToPost
import com.aiosman.ravenow.ui.post.NewPostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -16,6 +16,8 @@ import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object Utils { object Utils {
// 全局共享的 ImageLoader避免每次创建导致内存缓存不共享
private var sharedImageLoader: ImageLoader? = null
fun generateRandomString(length: Int): String { fun generateRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length) return (1..length)
@@ -24,23 +26,18 @@ object Utils {
} }
fun getImageLoader(context: Context): ImageLoader { fun getImageLoader(context: Context): ImageLoader {
val appContext = context.applicationContext
val existing = sharedImageLoader
if (existing != null) return existing
val okHttpClient = getUnsafeOkHttpClient(authInterceptor = AuthInterceptor()) val okHttpClient = getUnsafeOkHttpClient(authInterceptor = AuthInterceptor())
return ImageLoader.Builder(context) val loader = ImageLoader.Builder(appContext)
.okHttpClient(okHttpClient) .okHttpClient(okHttpClient)
.memoryCachePolicy(CachePolicy.ENABLED) .memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED) .diskCachePolicy(CachePolicy.ENABLED)
// .memoryCache {
// MemoryCache.Builder(context)
// .maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25%
// .build()
// }
// .diskCache {
// DiskCache.Builder()
// .directory(context.cacheDir.resolve("image_cache"))
// .maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2%
// .build()
// }
.build() .build()
sharedImageLoader = loader
return loader
} }
fun getTimeAgo(date: Date): String { fun getTimeAgo(date: Date): String {