feat: 新增导航过渡动画和透明化导航栏组件

- 新增NavigationTransitions导航过渡动画
- 新增TransparentTopAppBar透明化导航栏组件
- 更新AppNavigation集成新组件
- 添加透明化导航栏使用指南文档
This commit is contained in:
iammm0
2026-01-22 17:58:14 +08:00
parent 3f899aa16c
commit 2a1449c8ac
4 changed files with 470 additions and 16 deletions

View File

@@ -1,5 +1,6 @@
package com.huaga.life_echo.navigation
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.NavType
@@ -26,6 +27,7 @@ sealed class Screen(val route: String) {
object About : Screen("about")
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppNavigation(
navController: NavHostController,
@@ -37,7 +39,13 @@ fun AppNavigation(
navController = navController,
startDestination = if (isLoggedIn) Screen.ConversationList.route else Screen.Login.route
) {
composable(Screen.ConversationList.route) {
composable(
route = Screen.ConversationList.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
ConversationListScreen(
onConversationClick = { conversationId ->
navController.navigate(Screen.CreateMemory.createRoute(conversationId))
@@ -46,7 +54,11 @@ fun AppNavigation(
}
composable(
route = Screen.CreateMemory.route,
arguments = listOf(navArgument("conversationId") { type = NavType.StringType })
arguments = listOf(navArgument("conversationId") { type = NavType.StringType }),
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) { backStackEntry ->
val conversationId = backStackEntry.arguments?.getString("conversationId") ?: "new"
CreateMemoryScreen(
@@ -54,34 +66,94 @@ fun AppNavigation(
navController = navController
)
}
composable(Screen.MyMemoir.route) {
composable(
route = Screen.MyMemoir.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
MyMemoirScreen(navController = navController)
}
composable(Screen.Profile.route) {
composable(
route = Screen.Profile.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
ProfileScreen(navController = navController)
}
composable(Screen.UpgradePlan.route) {
composable(
route = Screen.UpgradePlan.route,
enterTransition = { slideInVertically() },
exitTransition = { slideOutVertically() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
UpgradePlanScreen(navController = navController)
}
composable(Screen.MyOrders.route) {
composable(
route = Screen.MyOrders.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
MyOrdersScreen(navController = navController)
}
composable(Screen.ExportData.route) {
composable(
route = Screen.ExportData.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
ExportDataScreen(navController = navController)
}
composable(Screen.PlanDetails.route) {
composable(
route = Screen.PlanDetails.route,
enterTransition = { scaleInTransition() },
exitTransition = { scaleOutTransition() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
PlanDetailsScreen(navController = navController)
}
composable(Screen.FAQ.route) {
composable(
route = Screen.FAQ.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
FAQScreen(navController = navController)
}
composable(Screen.Feedback.route) {
composable(
route = Screen.Feedback.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
FeedbackScreen(navController = navController)
}
composable(Screen.About.route) {
composable(
route = Screen.About.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
AboutScreen(navController = navController)
}
composable(Screen.Login.route) {
composable(
route = Screen.Login.route,
enterTransition = { fadeInTransition() },
exitTransition = { fadeOutTransition() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
LoginScreen(
onLoginSuccess = {
// 登录成功后导航到主界面
@@ -95,13 +167,16 @@ fun AppNavigation(
}
)
}
composable(Screen.Register.route) {
composable(
route = Screen.Register.route,
enterTransition = { slideInHorizontally() },
exitTransition = { slideOutHorizontally() },
popEnterTransition = { slideInHorizontallyFromLeft() },
popExitTransition = { slideOutHorizontallyToRight() }
) {
RegisterScreen(
onRegisterSuccess = {
navController.popBackStack()
},
onNavigateBack = {
navController.popBackStack()
}
)
}

View File

@@ -0,0 +1,137 @@
package com.huaga.life_echo.navigation
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.navigation.NavBackStackEntry
/**
* 导航转场动画配置
* 提供流畅的页面切换动画效果
*/
// 动画时长配置
private const val ANIMATION_DURATION = 300
private const val FADE_DURATION = 250
/**
* 水平滑动进入动画(从右侧滑入)
*/
fun slideInHorizontally(): EnterTransition {
return slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(FADE_DURATION))
}
/**
* 水平滑动退出动画(向左滑出)
*/
fun slideOutHorizontally(): ExitTransition {
return slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(FADE_DURATION))
}
/**
* 水平滑动进入动画(从左侧滑入,用于返回)
*/
fun slideInHorizontallyFromLeft(): EnterTransition {
return slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(FADE_DURATION))
}
/**
* 水平滑动退出动画(向右滑出,用于返回)
*/
fun slideOutHorizontallyToRight(): ExitTransition {
return slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(FADE_DURATION))
}
/**
* 垂直滑动进入动画(从底部滑入)
*/
fun slideInVertically(): EnterTransition {
return slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(FADE_DURATION))
}
/**
* 垂直滑动退出动画(向上滑出)
*/
fun slideOutVertically(): ExitTransition {
return slideOutVertically(
targetOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(FADE_DURATION))
}
/**
* 淡入淡出动画
*/
fun fadeInTransition(): EnterTransition {
return fadeIn(animationSpec = tween(FADE_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing))
}
fun fadeOutTransition(): ExitTransition {
return fadeOut(animationSpec = tween(FADE_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing))
}
/**
* 缩放进入动画
*/
fun scaleInTransition(): EnterTransition {
return scaleIn(
initialScale = 0.9f,
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeIn(animationSpec = tween(FADE_DURATION))
}
/**
* 缩放退出动画
*/
fun scaleOutTransition(): ExitTransition {
return scaleOut(
targetScale = 0.9f,
animationSpec = tween(ANIMATION_DURATION, easing = androidx.compose.animation.core.FastOutSlowInEasing)
) + fadeOut(animationSpec = tween(FADE_DURATION))
}
/**
* 根据导航方向智能选择转场动画
* 前进:从右侧滑入
* 返回:从左侧滑入
*/
@OptIn(ExperimentalAnimationApi::class)
fun getEnterTransition(backStackEntry: NavBackStackEntry): EnterTransition {
// 检查是否是首次进入(没有前一个页面)
val previousEntry = backStackEntry.savedStateHandle.get<NavBackStackEntry>("previous")?.let {
// 这里可以根据需要实现更复杂的逻辑
null
}
// 默认使用水平滑动动画
return slideInHorizontally()
}
@OptIn(ExperimentalAnimationApi::class)
fun getExitTransition(backStackEntry: NavBackStackEntry): ExitTransition {
return slideOutHorizontally()
}
@OptIn(ExperimentalAnimationApi::class)
fun getPopEnterTransition(backStackEntry: NavBackStackEntry): EnterTransition {
return slideInHorizontallyFromLeft()
}
@OptIn(ExperimentalAnimationApi::class)
fun getPopExitTransition(backStackEntry: NavBackStackEntry): ExitTransition {
return slideOutHorizontallyToRight()
}

View File

@@ -0,0 +1,73 @@
package com.huaga.life_echo.ui.components.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
/**
* 透明化顶部导航栏组件
*
* 提供多种透明化选项:
* 1. 完全透明
* 2. 半透明(带透明度)
* 3. 渐变透明(从顶部到底部)
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TransparentTopAppBar(
title: @Composable () -> Unit,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
containerColor: Color = Color.Transparent,
titleContentColor: Color = MaterialTheme.colorScheme.onSurface,
navigationIconContentColor: Color = MaterialTheme.colorScheme.onSurface,
actionsContentColor: Color = MaterialTheme.colorScheme.onSurface,
modifier: Modifier = Modifier,
// 透明化选项
transparencyType: TransparencyType = TransparencyType.FULLY_TRANSPARENT,
backgroundColor: Color = Color.White,
alpha: Float = 0.5f, // 用于半透明模式
gradientColors: List<Color> = listOf(Color.White.copy(alpha = 0.9f), Color.White.copy(alpha = 0.0f)) // 用于渐变模式
) {
val backgroundModifier = when (transparencyType) {
TransparencyType.FULLY_TRANSPARENT -> Modifier
TransparencyType.SEMI_TRANSPARENT -> Modifier.background(backgroundColor.copy(alpha = alpha))
TransparencyType.GRADIENT -> Modifier.background(
Brush.verticalGradient(
colors = gradientColors
)
)
}
TopAppBar(
title = title,
navigationIcon = navigationIcon,
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = containerColor,
titleContentColor = titleContentColor,
navigationIconContentColor = navigationIconContentColor,
actionIconContentColor = actionsContentColor
),
modifier = modifier
.windowInsetsPadding(WindowInsets.statusBars)
.then(backgroundModifier)
)
}
/**
* 透明化类型枚举
*/
enum class TransparencyType {
/** 完全透明 */
FULLY_TRANSPARENT,
/** 半透明(带透明度) */
SEMI_TRANSPARENT,
/** 渐变透明(从顶部到底部) */
GRADIENT
}

View File

@@ -0,0 +1,169 @@
# 透明化顶部导航栏使用指南
## 概述
本项目提供了两种方式来实现透明化顶部导航栏:
1. **TransparentTopAppBar** - 用于标准 Material3 TopAppBar 的透明化组件
2. **ChatHeader** - 自定义聊天头部组件,已支持透明化选项
## 使用方法
### 1. 使用 TransparentTopAppBar推荐
`TransparentTopAppBar` 是一个可复用的透明化 TopAppBar 组件,提供了三种透明化模式:
#### 完全透明模式
```kotlin
Scaffold(
topBar = {
TransparentTopAppBar(
title = { Text("页面标题") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回"
)
}
},
transparencyType = TransparencyType.FULLY_TRANSPARENT,
titleContentColor = MaterialTheme.colorScheme.onSurface,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface
)
}
) { paddingValues ->
// 页面内容
}
```
#### 半透明模式
```kotlin
TransparentTopAppBar(
title = { Text("页面标题") },
transparencyType = TransparencyType.SEMI_TRANSPARENT,
backgroundColor = MaterialTheme.colorScheme.surface,
alpha = 0.7f, // 透明度值0.0-1.0
titleContentColor = MaterialTheme.colorScheme.onSurface
)
```
#### 渐变透明模式(推荐)
```kotlin
TransparentTopAppBar(
title = { Text("页面标题") },
transparencyType = TransparencyType.GRADIENT,
backgroundColor = MaterialTheme.colorScheme.surface,
gradientColors = listOf(
MaterialTheme.colorScheme.surface.copy(alpha = 0.95f), // 顶部较不透明
MaterialTheme.colorScheme.surface.copy(alpha = 0.0f) // 底部完全透明
),
titleContentColor = MaterialTheme.colorScheme.onSurface
)
```
### 2. 使用 ChatHeader 透明化
`ChatHeader` 组件已支持透明化选项:
#### 完全透明
```kotlin
ChatHeader(
title = "聊天标题",
isOnline = true,
onBackClick = { navController?.popBackStack() },
isTransparent = true,
transparencyType = 0 // 完全透明
)
```
#### 半透明
```kotlin
ChatHeader(
title = "聊天标题",
isOnline = true,
onBackClick = { navController?.popBackStack() },
isTransparent = true,
transparencyType = 1, // 半透明
alpha = 0.6f // 透明度
)
```
#### 渐变透明
```kotlin
ChatHeader(
title = "聊天标题",
isOnline = true,
onBackClick = { navController?.popBackStack() },
isTransparent = true,
transparencyType = 2 // 渐变透明
)
```
## 透明化类型说明
### TransparencyType 枚举
- **FULLY_TRANSPARENT** - 完全透明,背景完全透明
- **SEMI_TRANSPARENT** - 半透明,可以设置 alpha 值控制透明度
- **GRADIENT** - 渐变透明,从顶部到底部逐渐变透明,视觉效果更自然
## 注意事项
1. **状态栏处理**:组件已自动处理状态栏的内边距(`windowInsetsPadding(WindowInsets.statusBars)`),确保内容不会被状态栏遮挡。
2. **文字颜色**:使用透明化时,需要根据背景调整文字颜色以确保可读性:
- 透明背景:使用 `MaterialTheme.colorScheme.onSurface`
- 深色背景:使用 `Color.White`
- 浅色背景:使用 `Color.Black``MaterialTheme.colorScheme.onSurface`
3. **性能考虑**:渐变透明模式使用了 `Brush.verticalGradient`,性能略低于其他模式,但视觉效果更好。
4. **主题适配**:建议使用 `MaterialTheme.colorScheme` 中的颜色,以确保在不同主题(亮色/暗色)下都能正常显示。
## 示例
参考 `AboutScreen.kt` 中的实现示例。
## 应用到其他屏幕
要将透明化应用到其他使用 TopAppBar 的屏幕,只需:
1. 导入 `TransparentTopAppBar``TransparencyType`
2.`TopAppBar` 替换为 `TransparentTopAppBar`
3. 选择合适的 `transparencyType`
4. 调整文字颜色以确保可读性
例如,更新 `FeedbackScreen`
```kotlin
import com.huaga.life_echo.ui.components.common.TransparentTopAppBar
import com.huaga.life_echo.ui.components.common.TransparencyType
// 在 Scaffold 中
topBar = {
TransparentTopAppBar(
title = { Text("反馈与客服") },
navigationIcon = {
IconButton(onClick = { navController?.popBackStack() }) {
Icon(
imageVector = AppIcons.ArrowBack,
contentDescription = "返回",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
transparencyType = TransparencyType.GRADIENT,
gradientColors = listOf(
MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
MaterialTheme.colorScheme.surface.copy(alpha = 0.0f)
)
)
}
```