Android Compose 应用进阶:Navigation 组件、共享元素与预测性返回的深度实践
Android Compose 应用进阶:Navigation 组件、共享元素与预测性返回的深度实践
Development of Shared element, Navigation, Predictive back component using Compose on Android
简述:
- 打开软件预测性返回选项后,使用Navigation组件组合页面间的跳转 即可实现页面的预测性返回
- 使用Navigation组件组合的页面,设置共享元素 即可实现共享元素间的预测性返回。
支持预测性返回手势
查看官方教程即可:
https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture
Navigation导航 结合 Shared element共享元素
Navigation导航学习:
查看官方教程学习:
https://developer.android.com/develop/ui/compose/navigation?hl=zh-cn
导航主干-伪代码:
// 1. 定义导航路由
sealed class PhotoScreenRouteForColumn(val route: String) {
object Grid : PhotoScreenRouteForColumn("photo_grid") // 网格视图
// 详情路由需要两个参数
object Detail : PhotoScreenRouteForColumn("photo_detail/{outerIndex}/{innerIndex}") {
fun createRoute(outerIndex: Int, innerIndex: Int) = "photo_detail/$outerIndex/$innerIndex"
}
}
// 2.主屏幕 Composable
@Composable
fun RespondCollectScreenSimplified(
collectId: Long,
appNavController: NavController,
// ... (其他状态初始化和数据获取逻辑)
) {
// 创建内部导航控制器
val photoNavController = rememberNavController()
// 定义导航宿主
NavHost(
navController = photoNavController,
startDestination = PhotoScreenRouteForColumn.Grid.route
) {
// 路由 1: 网格视图
composable(PhotoScreenRouteForColumn.Grid.route) {
// 模拟点击照片后导航到详情
val onPhotoClick = { outerIndex: Int, innerIndex: Int ->
photoNavController.navigate(
PhotoScreenRouteForColumn.Detail.createRoute(outerIndex, innerIndex)
)
}
// 假设这里会显示一个照片列表,点击后触发 onPhotoClick
println("显示照片网格视图")
}
// 路由 2: 详情视图
composable(
route = PhotoScreenRouteForColumn.Detail.route,
arguments = listOf(
navArgument("outerIndex") { type = NavType.IntType },
navArgument("innerIndex") { type = NavType.IntType }
)
) { navBackStackEntry ->
// 获取导航参数
val outerIndexArg = navBackStackEntry.arguments?.getInt("outerIndex")
val innerIndexArg = navBackStackEntry.arguments?.getInt("innerIndex")
// 假设这里会根据 outerIndexArg 和 innerIndexArg 显示照片详情
println("显示照片详情,outerIndex: $outerIndexArg, innerIndex: $innerIndexArg")
// 返回操作
val onBackClick = {
photoNavController.popBackStack()
}
// 假设有一个按钮点击后 调用 onBackClick,即可实现返回
}
}
}
提示🌟
Navigation的路由之间的跳转是在路由包含的组件(composable(){xx}包含的内容)的跳转。也就是说如果某些compose组件 在路由外,跳转后这些组件还会继续显示。比如说:
- 将composable{}包含Scaffold脚手架,跳转后会展示新的完整的页面,
- 但是 将composable{}放在Scaffold脚手架的content里面,跳转后Scaffold的topBar等元素还是会在页面显示。
Shared element共享元素学习:
查看官方教程学习:
https://developer.android.com/develop/ui/compose/animation/shared-elements
主干 -kt 伪代码
//设置实验性标识
@file:OptIn(ExperimentalSharedTransitionApi::class)
// --- 统一动画定义(可有可无) ---
@OptIn(ExperimentalSharedTransitionApi::class)
val photoBoundsTransform = androidx.compose.animation.BoundsTransform { _, _ ->
spring(stiffness = Spring.StiffnessMedium)
}
// --- Key 辅助函数 ---
private fun photoContainerKey(index: Int) = "photo_container_$index"
/**
* 图片网格查看器 (共享元素页面1).
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun PhotoGridViewer(
photos: List<File>,
onPhotoClick: (Int) -> Unit,
sharedScope: SharedTransitionScope, // 用于协调共享元素动画的 Scope
animScope: AnimatedVisibilityScope // 来自 NavHost composable lambda 的 Scope
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp)
) {
itemsIndexed(photos) { index, photo ->
with(sharedScope) { // 在 SharedTransitionScope 中执行共享元素相关的操作
Box(
modifier = Modifier
.aspectRatio(1f)
.sharedBounds( // 标记此 Box 的边界为共享的
rememberSharedContentState(key = photoContainerKey(index)), // 为共享内容创建一个稳定的 State,使用基于索引的 Key
animatedVisibilityScope = animScope, // 将此共享元素与导航动画的可见性 Scope 关联
boundsTransform = photoBoundsTransform // 应用预定义的边界变换动画
)
.clickable { onPhotoClick(index) }
) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedBounds( // 标记此 Box 的边界为共享的 (代表图片元素)
rememberSharedContentState(key = "photo_element_${photo.path}"), // 为共享内容创建一个稳定的 State,使用图片路径作为 Key
animatedVisibilityScope = animScope, // 将此共享元素与导航动画的可见性 Scope 关联
boundsTransform = photoBoundsTransform // 应用预定义的边界变换动画
)
)
}
}
}
}
}
/**
* 全屏图片查看器 (共享元素页面2).
*/
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun FullScreenPhotoViewer(
photos: List<File>,
initialPhotoIndex: Int,
onClose: () -> Unit,
sharedScope: SharedTransitionScope, // 用于协调共享元素动画的 Scope
animScope: AnimatedVisibilityScope // 来自 NavHost composable lambda 的 Scope
) {
val pagerState = rememberPagerState(initialPage = initialPhotoIndex) { photos.size }
with(sharedScope) { // 在 SharedTransitionScope 中执行共享元素相关的操作
HorizontalPager(state = pagerState) { pageIndex ->
val photo = photos[pageIndex]
Box(
modifier = Modifier
.fillMaxSize()
.sharedBounds( // 标记此 Box 的边界为共享的
rememberSharedContentState(key = photoContainerKey(pageIndex)), // 为共享内容创建一个稳定的 State,使用基于页面索引的 Key
animatedVisibilityScope = animScope, // 将此共享元素与导航动画的可见性 Scope 关联
boundsTransform = photoBoundsTransform // 应用预定义的边界变换动画
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedBounds( // 标记此 Box 的边界为共享的 (代表图片元素)
rememberSharedContentState(key = "photo_element_${photo.path}"), // 为共享内容创建一个稳定的 State,使用图片路径作为 Key
animatedVisibilityScope = animScope, // 将此共享元素与导航动画的可见性 Scope 关联
boundsTransform = photoBoundsTransform // 应用预定义的边界变换动画
)
)
}
}
}
}
结合实战:
@file:OptIn(ExperimentalSharedTransitionApi::class)
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.sharedBounds // 用于定义共享元素的边界动画
import androidx.compose.animation.rememberSharedContentState // 用于在导航之间保持共享元素的状态
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost // 用于定义导航图
import androidx.navigation.compose.composable // 用于定义导航图中的单个路由
import androidx.navigation.compose.rememberNavController // 用于创建和持有 NavController 实例
// 定义导航屏幕的路由
sealed class Screen(val route: String) {
object Grid : Screen("grid") // 网格屏幕路由
object Detail : Screen("detail/{itemId}") { // 详情屏幕路由,带有一个 itemId 参数
fun createRoute(itemId: Int) = "detail/$itemId" // 用于创建带参数的详情路由
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun NavigationWithSharedElements() {
val navController = rememberNavController() // 创建 NavController 实例,用于管理应用内的导航
SharedTransitionScope { sharedScope -> // 包裹 NavHost 的 Composable,启用共享元素动画
NavHost(navController = navController, startDestination = Screen.Grid.route) { // 定义导航图,设置起始屏幕为 Grid
composable(Screen.Grid.route) { entry -> // 定义 Grid 屏幕的路由
val animScope = this // 获取 AnimatedVisibilityScope,用于共享元素动画
GridScreen(navController = navController, sharedScope = sharedScope, animScope = animScope)
}
composable(Screen.Detail.route) { entry -> // 定义 Detail 屏幕的路由,并接收参数
val animScope = this // 获取 AnimatedVisibilityScope
val itemId = entry.arguments?.getString("itemId")?.toIntOrNull() ?: -1 // 从导航参数中获取 itemId
DetailScreen(itemId = itemId, navController = navController, sharedScope = sharedScope, animScope = animScope)
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun GridScreen(navController: NavHostController, sharedScope: SharedTransitionScope, animScope: AnimatedVisibilityScope) {
// 模拟一些数据
val items = remember { (1..5).toList() }
androidx.compose.foundation.layout.Column {
items.forEach { itemId ->
Box(
modifier = Modifier
.padding(16.dp)
.background(Color.LightGray)
.clickable {
navController.navigate(Screen.Detail.createRoute(itemId)) // 点击后导航到详情屏幕,并传递 itemId
}
) {
with(sharedScope) { // 在 SharedTransitionScope 中定义共享元素
Box(
modifier = Modifier
.size(50.dp)
.sharedBounds( // 标记此 Box 为共享元素,使用唯一的 key
rememberSharedContentState(key = "item_container_$itemId"),
animatedVisibilityScope = animScope // 将共享元素与导航动画的可见性关联
)
.background(Color.Gray)
) {
androidx.compose.material3.Text(text = "Item $itemId", modifier = Modifier.padding(8.dp))
}
}
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun DetailScreen(itemId: Int, navController: NavHostController, sharedScope: SharedTransitionScope, animScope: AnimatedVisibilityScope) {
androidx.compose.foundation.layout.Column {
androidx.compose.material3.Button(onClick = { navController.popBackStack() }) { // 点击按钮返回上一个屏幕
androidx.compose.material3.Text("Go Back")
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.background(Color.DarkGray)
) {
with(sharedScope) { // 在 SharedTransitionScope 中定义共享元素
Box(
modifier = Modifier
.size(100.dp)
.sharedBounds( // 标记此 Box 为共享元素,使用与 GridScreen 中相同的 key
rememberSharedContentState(key = "item_container_$itemId"),
animatedVisibilityScope = animScope // 将共享元素与导航动画的可见性关联
)
.background(Color.Black)
.align(Alignment.Center)
) {
androidx.compose.material3.Text(text = "Details for Item $itemId", color = Color.White, modifier = Modifier.padding(16.dp))
}
}
}
}
}