在settings.gradle添加 Groovy使用
maven {
url 'https://jitpack.io'
}Kotlin使用
maven {
url = uri("https://jitpack.io")
}添加依赖,版本以 Release 的 Tag 为准
implementation("com.github.Chiu-xaH:SharedNav:<version>")每个页面对应一个 Destination 对象,继承抽象类并实现 key 与 Content():
object HomeDestination : Destination() {
override val key = "home" // 全局唯一,同时用作容器共享的匹配 Key
@Composable
override fun Content() {
HomeScreen()
}
}在 Activity 或顶层 Composable 中启动导航:
写法 1:
@Composable
fun App() {
SharedNavHost(
startDestination = HomeDestination
)
}写法 2:手动控制 NavController
val navController = rememberNavController(startDestination = HomeDestination)
SharedNavHost(
navController = navController,
)在任意 Composable 中通过 LocalNavController 或 LocalNavControllerSafely 获取控制器:
提示:如果无法保证 Composable 函数一定在
SharedNav下调用,请使用LocalNavControllerSafely,它返回可空对象;LocalNavController获取不到控制器时会直接抛出异常导致 Crash。
@Composable
fun HomeScreen() {
val navController = LocalNavController.current
Button(onClick = { navController.push(DetailDestination) }) {
Text("进入详情")
}
Button(onClick = { navController.pop() }) {
Text("返回")
}
}用 SharedContainer 包裹触发跳转的组件,key 与目标 Destination.key 保持一致,即可获得类 Launcher 的展开/收起动画。
写法 1:
@Composable
fun HomeScreen() {
val navController = LocalNavController.current
val dest = DetailDestination
SharedContainer(
key = dest.key,
shape = MaterialTheme.shapes.medium,
) {
// 内层组件 shape 必须为 RoundedCornerShape(0.dp) 或 RectangleShape
Card(
shape = RectangleShape,
onClick = { navController.push(dest) }
) { /* 内容 */ }
}
}写法 2:Modifier 扩展
@Composable
fun HomeScreen() {
val navController = LocalNavController.current
val dest = DetailDestination
Card(
modifier = Modifier.sharedContainer(
key = dest.key,
shape = MaterialTheme.shapes.medium
),
shape = RectangleShape,
onClick = { navController.push(dest) }
) { /* 内容 */ }
}注意:
SharedContainer内层组件的shape必须设置为无圆角,圆角统一由外层SharedContainer管理,否则在提取 1 像素时会缺失边角。
SharedContainer 除必填的 key 和 shape 外,还有以下可选参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
shadow |
Dp |
0.dp |
阴影 |
containerColor |
Color? |
null |
null 时:SDK 33+ 使用像素提取填充,低版本使用裁切填充;指定颜色时:SDK 33+ 使用像素提取填充,低版本使用颜色填充 |
containerFilledStrategy |
ContainerFilledStrategy |
Pixel() |
更精细地指定填充方式,与 containerColor 二选一,不同时使用 |
| 模块 | 职责 |
|---|---|
navigation |
页面路由、返回栈管理、导航时的前景、背景过渡动画 |
shared-container |
容器共享动效核心:记录源与目标的Rect与GraphicsLayer,驱动容器共享动画 |
common |
两个模块共用的代码 |
app |
示例应用, |
所有页面的基类,开发者继承后实现以下成员:
| 成员 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key |
String |
必填 | 全局唯一,同时作为容器共享的匹配 Key |
Content() |
@Composable |
必填 | 页面 UI 内容 |
PlaceHolder |
(@Composable () -> Unit)? |
null |
启动屏内容,动画期间先显示,结束后切换为真实内容 |
enforcePlaceHolder |
Boolean |
false |
强制在动画期间显示 PlaceHolder,不依赖全局 enableSplashScreen 开关 |
带参数的 Destination(推荐用 data class):
data class SecondDestination(val userId: Int) : Destination() {
override val key = "second_$userId"
@Composable
override fun Content() { SecondScreen() }
}
// 使用
navController.push(SecondDestination(userId = 42))创建并记住一个 NavigationController 实例:
val navController = rememberNavController(startDestination = HomeDestination)| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
startDestination |
Destination |
必填 | 初始页面 |
navController |
NavigationController |
内部自动创建 | 传入已有实例以手动控制 |
modifier |
Modifier |
Modifier |
应用到宿主容器 |
dependencies |
Dependencies |
Dependencies() |
跨页面注入的依赖数据 |
customBackHandler |
@Composable () -> Unit |
DefaultBackHandler() |
自定义返回手势/按键处理 |
SharedNavHost 在内部自动包裹 SharedContainerRoot,启用容器共享功能。不需要容器共享时可直接使用 NavHost。
// 跳转到目标页面
fun push(destination: Destination, launchMode: LaunchMode = LaunchMode.Push(reuse = true))
// 返回上一页
fun pop()
// 获取当前栈顶
fun current(): StackEntry?
// 是否可以返回
fun canPop(): Boolean| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
transitionLevel |
EffectLevel |
FULL |
全局动效等级,可运行时动态切换 |
enableSplashScreen |
Boolean |
false |
动画期间是否对前景页显示 PlaceHolder |
isTransitioning |
Boolean |
false(只读) |
当前是否正在播放过渡动画 |
stack |
List<StackEntry> |
只读 | 当前导航返回栈 |
包裹触发跳转的卡片/按钮,作为容器共享的「源端」。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key |
Any |
必填 | 与目标 Destination.key 完全一致 |
shape |
Shape |
必填 | 容器圆角形状,过渡时会对此圆角做插值 |
containerColor |
Color? |
null |
容器背景色;null 时自动使用 1 像素填充策略 |
containerFilledStrategy |
ContainerFilledStrategy |
Pixel() |
容器填充策略,详见 4.3 节,与 containerColor 二选一 |
shadow |
Dp |
0.dp |
容器阴影 |
modifier |
Modifier |
Modifier |
外层修饰符 |
SharedContainer(
key = dest.key,
shape = RoundedCornerShape(20.dp),
containerColor = MaterialTheme.colorScheme.primaryContainer,
) {
Card(shape = RectangleShape, ...) { ... }
}效果等同于 SharedContainer 包裹,可直接挂载到已有组件上:
Card(
modifier = Modifier.sharedContainer(
key = dest.key,
shape = MaterialTheme.shapes.medium
),
onClick = { navController.push(dest) }
) { /* 内容 */ }控制容器展开过渡时「未填充区域」的视觉处理方式:
| 策略 | 效果 | 优缺点 |
|---|---|---|
Pixel(spareStrategy) |
取容器底部或右侧 1 像素拉伸填充, | 效果好,但适配度不广,需 SDK 33+,低版本自动降级到 spareStrategy |
Color(color) |
用指定纯色填充,兼容所有版本,适合有明确主色的卡片 | 适配度广,对于纯色卡片与像素填充的效果完全一致,但需手动为每个容器指定颜色,开发成本高 |
Clip |
裁切放大,开发成本低,无需手动指定颜色 | 适配度高,开发成本低,但无法营造出上面两者方案的分层效果 |
为达到开发效率和效果的平衡,SharedContainer有containerColor字段,SDK33+时使用Pixel方案,否则读取containerColor,为null则使用Clip方案,不为null则使用Color方案。
@Composable
fun SharedContainer(
key : Any,
shape : Shape,
modifier : Modifier = Modifier,
shadow : Dp = 0.dp,
containerColor : Color?,
content : @Composable () -> Unit
) = SharedContainer(
key,
shape,
modifier,
shadow,
if(containerColor == null) {
ContainerFilledStrategy.Pixel(ContainerFilledStrategy.Clip)
} else {
ContainerFilledStrategy.Pixel(ContainerFilledStrategy.Color(containerColor))
},
content
)容器共享的核心注册表,通过 LocalSharedRegistry.current 访问,可在运行时动态调整动画参数:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled |
Boolean |
true |
false 时关闭容器共享动画 |
extensionDouble |
Boolean |
false |
false 时底部或右侧填充,否则双向(上下或左右)填充 |
控制背景页与前景页在过渡时所应用的视觉效果层级,可运行时通过 navController.transitionLevel 切换:
| 等级 | 背景页效果 | 前景页效果 |
|---|---|---|
FULL (3) |
模糊 + 压暗 + 缩放 | 模糊 + 缩放 + 圆角插值 |
NO_BLUR (2) |
压暗 + 缩放 | 缩放 + 圆角插值 |
NO_SCALE (1) |
仅压暗 | 缩放 + 圆角插值 |
NONE (0) |
无效果 | 轻缩放 + 透明度淡入 |
描述某一帧页面的视觉状态,由 NavHost 根据 transitionProgress(0f → 1f)自动插值计算。
| 字段 | Full(展开态) | Background(背景态) | None(收起态) |
|---|---|---|---|
scale |
1.0f |
0.875f |
0.0f |
blur |
0.dp |
25.dp |
20.dp |
mask |
0.0f |
0.25f |
0.0f |
corner |
屏幕圆角 | 0.dp |
屏幕圆角 × 2 |
LaunchMode 决定 push() 时如何操作导航返回栈:
| 模式 | 行为 |
|---|---|
Push(reuse) |
压入栈顶。reuse = true 时若栈顶已是目标则复用,不重复入栈 |
PopToExisting |
若栈中已有目标实例,清除其上所有页面并执行 pop 回到它;否则正常 push |
Single(reuse, actionType) |
保证栈中只有一个目标实例。reuse = true 时复用并清除其余所有项;actionType = ActionType.POP 可使过渡动画呈现返回效果 |
示例:从深层页面一键回到首页,并使用返回动画
navController.push(
destination = navController.startDestination,
launchMode = LaunchMode.Single(
reuse = true,
actionType = ActionType.POP
)
)Dependencies 是一个轻量键值容器,用于在 NavHost 树范围内跨页面传递数据(如外部配置、回调等),无需 ViewModel:
// 在入口处构建依赖,keys 变化时自动重建
val deps = rememberNavDependencies(userId) {
put(userId, tag = "userId")
put("admin", tag = "role")
}
SharedNavHost(
startDestination = HomeDestination,
dependencies = deps
)
// 在任意子页面中读取
@Composable
fun HomeScreen() {
val userId = LocalNavDependencies.current.get<Int>("userId")
val role = LocalNavDependencies.current.get<String>("role")
}提示:
rememberNavDependencies(vararg keys)当keys发生变化时会重新执行 builder 并更新依赖,适合将外部状态透传给整个导航树。
示例 App 内置了贝塞尔曲线可视化调节器(BezierSettingsDestination),可在运行时实时调节曲线。
ScreenCornerHelper 负责读取设备实际物理圆角,保证页面过渡边角与屏幕对齐:
示例 App 内置了屏幕圆角可视化调节器(CornerSettingsDestination),可在运行时实时修改,供SDK低于33的设备手动校准。
当页面初始化较慢(如相机、大量数据加载)时,在 Destination 中提供 PlaceHolder,动画期间先展示占位内容,避免卡顿:
object CameraDestination : Destination() {
override val key = "camera"
// 强制启用,不受navController.enableSplashScreen限制
override val enforcePlaceHolder = true
override val PlaceHolder: (@Composable () -> Unit) = {
Box(Modifier.fillMaxSize().background(Color.Black)) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
@Composable
override fun Content() { CameraScreen() }
}SharedNavHost 接受 customBackHandler 参数,可替换默认的系统返回拦截逻辑:
SharedNavHost(
startDestination = HomeDestination,
backHandler = {
// 默认返回逻辑
DefaultBackHandler()
}
)- 圆角必须交给 SharedContainer 管理:内层组件的
shape必须设为无圆角,否则过渡动画中会导致 1 像素提取缺失边角。 - key 全局唯一:同一
key的SharedContainer与Destination.key必须完全一致(大小写敏感),否则共享动画不触发。 - SharedNavHost 是容器共享的前提:
SharedContainerRoot仅由SharedNavHost提供,直接使用NavHost时容器共享不生效。
| 特性 | 最低版本 | 低版本方案 |
|---|---|---|
| 背景模糊 | API 31 | 无模糊 |
| 1 像素填充 | API 33 | 使用 Color 或 Clip 填充 |
| 自动获取屏幕圆角 | API 33 | 无圆角,可手动指定(app模块有具体示例) |
| 镜面缩放(共享容器运行时) | API 33 | 背景直接做 scale 缩放 |
Q:点击后没有容器共享动画?
- 检查
SharedContainer的key与目标Destination.key是否完全一致。 - 确认使用的是
SharedNavHost而非NavHost。 - 检查
registry.enabled是否为true,以及navController.transitionLevel是否为NONE(NONE等级会跳过容器共享)。
Q:多个容器共用同一个页面?
- 为
Destination增加一个来源字段(如origin),将key区分开,保证每个容器对应唯一的key,不可重复。




