diff --git a/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt b/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt index c0fdab5..a128bf2 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/Navi.kt @@ -8,6 +8,8 @@ import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn 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.WindowInsets import androidx.compose.foundation.layout.navigationBars @@ -163,16 +165,32 @@ fun NavigationController( navArgument("initImagePagerIndex") { type = NavType.IntType } ), enterTransition = { - fadeIn(animationSpec = tween(durationMillis = 200)) + // iOS push: new screen slides in from the right + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 280) + ) }, exitTransition = { - fadeOut(animationSpec = tween(durationMillis = 200)) + // iOS push: previous screen shifts slightly left (parallax) + slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth / 3 }, + animationSpec = tween(durationMillis = 280) + ) }, 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 = { - fadeOut(animationSpec = tween(durationMillis = 200)) + // iOS pop: current screen slides out to the right + slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 280) + ) } ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") @@ -448,6 +466,30 @@ fun NavigationController( composable( 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() } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt index 049ba19..95ad19d 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/AiAgentListScreen.kt @@ -91,7 +91,10 @@ fun AiAgentListScreen( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(filteredAgents) { agent -> + items( + items = filteredAgents, + key = { it.id } + ) { agent -> MemberItem( member = agent, isSelected = selectedMemberIds.contains(agent.id), diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt index 873f553..33eb85e 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/CreateGroupChatScreen.kt @@ -88,7 +88,7 @@ fun CreateGroupChatScreen() { } } - val navigationBarPaddings = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp + val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() LaunchedEffect(Unit) { systemUiController.setNavigationBarColor(Color.Transparent) @@ -97,7 +97,7 @@ fun CreateGroupChatScreen() { Column( modifier = Modifier .fillMaxSize() - .padding(bottom = navigationBarPaddings) + .background(AppColors.background) ) { // 错误提示 CreateGroupChatViewModel.errorMessage?.let { error -> @@ -177,7 +177,7 @@ fun CreateGroupChatScreen() { color = AppColors.inputBackground, shape = RoundedCornerShape(8.dp) ) - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding(horizontal = 16.dp, vertical = 13.dp) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -247,44 +247,50 @@ fun CreateGroupChatScreen() { // ) // } - // 群聊名称输入框 - Row( + // 群聊名称输入:同一圆角灰色矩形容器 + Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = 16.dp, vertical = 8.dp) + .background( + color = AppColors.inputBackground, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 12.dp, vertical = 8.dp) ) { - Text( - text = stringResource(R.string.group_name), - fontSize = 14.sp, - color = AppColors.text, - modifier = Modifier.width(80.dp) - ) - BasicTextField( - value = groupName, - onValueChange = { groupName = it }, - textStyle = androidx.compose.ui.text.TextStyle( + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.group_name), + fontSize = 14.sp, color = AppColors.text, - fontSize = 14.sp - ), - modifier = Modifier - .weight(1f) - .background( - color = AppColors.inputBackground, - shape = RoundedCornerShape(8.dp) - ) - .padding(horizontal = 12.dp, vertical = 8.dp), - decorationBox = { innerTextField -> - if (groupName.text.isEmpty()) { - Text( - text = stringResource(R.string.group_name_hint), - color = Color(0xFF999999), - fontSize = 14.sp - ) + modifier = Modifier.width(80.dp) + ) + BasicTextField( + value = groupName, + onValueChange = { groupName = it }, + textStyle = androidx.compose.ui.text.TextStyle( + color = AppColors.text, + fontSize = 14.sp + ), + modifier = Modifier + .weight(1f), + decorationBox = { innerTextField -> + Box(Modifier.fillMaxWidth()) { + if (groupName.text.isEmpty()) { + Text( + text = stringResource(R.string.group_name_hint), + color = AppColors.inputHint, + fontSize = 14.sp + ) + } + innerTextField() + } } - innerTextField() - } - ) + ) + } } // 已选成员列表 @@ -313,6 +319,9 @@ fun CreateGroupChatScreen() { context = context, imageUrl = member.avatar, contentDescription = member.name, + defaultRes = R.drawable.default_avatar, + placeholderRes = R.drawable.default_avatar, + errorRes = R.drawable.default_avatar, modifier = Modifier .size(48.dp) .clip(CircleShape) @@ -336,7 +345,7 @@ fun CreateGroupChatScreen() { ) { Text( text = "×", - color = Color.White, + color = AppColors.mainText, fontSize = 14.sp, fontWeight = FontWeight.Bold ) @@ -393,12 +402,12 @@ fun CreateGroupChatScreen() { ) } - // 内容区域 + // 内容区域 - 自适应填满剩余高度 HorizontalPager( state = pagerState, modifier = Modifier .fillMaxWidth() - .weight(1f) + .weight(1f) // 这里让列表占据剩余空间 ) { when (it) { 0 -> { @@ -432,7 +441,7 @@ fun CreateGroupChatScreen() { } } - // 创建群聊按钮 + // 创建群聊按钮 - 固定在底部 Button( onClick = { // 创建群聊逻辑 @@ -451,7 +460,7 @@ fun CreateGroupChatScreen() { }, modifier = Modifier .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( containerColor = AppColors.main, contentColor = AppColors.mainText diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt index 15ec82b..a0e28f9 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/FriendListScreen.kt @@ -91,7 +91,10 @@ fun FriendListScreen( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(filteredFriends) { friend -> + items( + items = filteredFriends, + key = { it.id } + ) { friend -> MemberItem( member = friend, isSelected = selectedMemberIds.contains(friend.id), diff --git a/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt b/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt index b73894d..6f92684 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/group/MemberItem.kt @@ -2,8 +2,6 @@ package com.aiosman.ravenow.ui.group import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.sp import com.aiosman.ravenow.LocalAppTheme +import com.aiosman.ravenow.R import com.aiosman.ravenow.ui.composables.CustomAsyncImage +import com.aiosman.ravenow.ui.composables.Checkbox import com.aiosman.ravenow.ui.modifiers.noRippleClickable @Composable @@ -29,14 +29,18 @@ fun MemberItem( Row( modifier = Modifier .fillMaxWidth() + .height(56.dp) // 固定高度防止跳动 .noRippleClickable { onSelect() } - .padding(vertical = 8.dp), + .padding(horizontal = 0.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { CustomAsyncImage( context = context, imageUrl = member.avatar, contentDescription = member.name, + defaultRes = R.drawable.default_avatar, + placeholderRes = R.drawable.default_avatar, + errorRes = R.drawable.default_avatar, modifier = Modifier .size(40.dp) .clip(CircleShape) @@ -54,12 +58,9 @@ fun MemberItem( Spacer(modifier = Modifier.width(8.dp)) Checkbox( + size = 20, checked = isSelected, - onCheckedChange = { onSelect() }, - colors = CheckboxDefaults.colors( - checkedColor = AppColors.main, - uncheckedColor = AppColors.secondaryText - ) + onCheckedChange = { onSelect() } ) } } diff --git a/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt b/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt index 1b2b684..f5408cb 100644 --- a/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt +++ b/app/src/main/java/com/aiosman/ravenow/ui/index/Index.kt @@ -100,12 +100,7 @@ fun IndexScreen() { val drawerState = rememberDrawerState(DrawerValue.Closed) val context = LocalContext.current - // 页面退出时清理所有资源 - DisposableEffect(Unit) { - onDispose { - ResourceCleanupManager.cleanupAllResources(context) - } - } + // 注意:不要在离开 Index 路由时全量清理资源,以免返回后列表被重置 LaunchedEffect(Unit) { systemUiController.setNavigationBarColor(Color.Transparent) } @@ -378,12 +373,7 @@ fun Home() { systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode) } - // 页面退出时清理动态相关资源 - DisposableEffect(Unit) { - onDispose { - ResourceCleanupManager.cleanupPageResources("moment") - } - } + // 注意:避免在离开 Home 时清理动态资源,防止返回详情后触发重新加载 Column( modifier = Modifier 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 0d6b5b7..7b2e527 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 @@ -6,10 +6,8 @@ import android.net.Uri import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.systemBars 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.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.shape.CircleShape @@ -34,8 +31,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi 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.pullRefresh 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.LocalDensity import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource 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.sp 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.ui.NavigationRoute 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.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.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.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.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.NewPostViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/aiosman/ravenow/utils/Utils.kt b/app/src/main/java/com/aiosman/ravenow/utils/Utils.kt index b4d097b..a089c69 100644 --- a/app/src/main/java/com/aiosman/ravenow/utils/Utils.kt +++ b/app/src/main/java/com/aiosman/ravenow/utils/Utils.kt @@ -16,6 +16,8 @@ import java.util.UUID import java.util.concurrent.TimeUnit object Utils { + // 全局共享的 ImageLoader,避免每次创建导致内存缓存不共享 + private var sharedImageLoader: ImageLoader? = null fun generateRandomString(length: Int): String { val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') return (1..length) @@ -24,23 +26,18 @@ object Utils { } fun getImageLoader(context: Context): ImageLoader { + val appContext = context.applicationContext + val existing = sharedImageLoader + if (existing != null) return existing + val okHttpClient = getUnsafeOkHttpClient(authInterceptor = AuthInterceptor()) - return ImageLoader.Builder(context) + val loader = ImageLoader.Builder(appContext) .okHttpClient(okHttpClient) .memoryCachePolicy(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() + sharedImageLoader = loader + return loader } fun getTimeAgo(date: Date): String {