底层架构原理
react-native-shared-hero 专为 React Native 新架构(Fabric)设计,保持了极简的 TypeScript 包装层,并在 iOS 与 Android 原生层实现了完全对称的注册配对与飞行引擎。
整体数据流
┌────────────────────────────────────────────────────────────────────────┐
│ React Native JS (Fabric) │
│ <SharedHero id="photo-1" namespace="default" mode="morph" /> │
└──────────────────────────────────┬─────────────────────────────────────┘
│ Codegen C++ ShadowNode / Props
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 原生端点视图 (Native Endpoint View) │
│ iOS: SharedHeroView.mm (ObjC++) -> SharedHeroViewImpl.swift │
│ Android: SharedHeroViewManager.kt -> SharedHeroView.kt │
│ │
│ • 维护 heroId、heroNamespace、mode、timing、borderRadius、bgColor │
│ • 布局稳定后按需捕获 HeroSnapshot 与实时窗口坐标框 │
└──────────────────────────────────┬─────────────────────────────────────┘
│ register / notifyLayout / unregister
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 原生单例注册表与配对引擎 (HeroRegistry) │
│ iOS: HeroRegistry.shared (HeroRegistry.swift) │
│ Android: HeroRegistry (HeroRegistry.kt) │
│ │
│ • live[key]: 每个 namespace::id 下已挂载视图的有序列表 │
│ • recentlyUnregistered[key]: 暂存刚卸载的源快照(约 300 ms 窗口) │
│ • lastKnownSnapshots: 容量受限的 LRU 兜底快照缓存 │
│ • lastFlightSourceFrame: 推入/返回对称坐标提示(destFrameHint) │
└──────────────────────────────────┬─────────────────────────────────────┘
│ 启动 / 半空接管飞行
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 飞行引擎与窗口级浮层 (FlightEngine & OverlayHost) │
│ iOS: FlightEngine.swift + OverlayHost.swift (UIWindow .alert + 1)│
│ Android: FlightEngine.kt + OverlayHost.kt (ViewGroupOverlay) │
└────────────────────────────────────────────────────────────────────────┘HeroRegistry 的两条配对路径
不同于依赖路由转场钩子的方案,HeroRegistry 完全通过原生视图生命周期信号在两条路径上完成配对:
路径 1:源视图保持挂载时挂载新孪生视图(Stack 推入、Modal、半屏面板)
- 页面 A 已挂载,
live["default::photo-1"]中包含[viewA]。 - 页面 B 挂载具有相同键的
viewB。register(viewB)会在栈转场移动页面前立即捕获viewA的快照与窗口坐标框,并将飞向viewB的任务加入待定队列。 - 当
viewB完成 Fabric 布局(notifyLayout)且放大场景下的目标<Image>完成位图解码(最多等待约 120 ms)后,HeroRegistry将两端快照与几何参数交给FlightEngine启动飞行。
路径 2:视图注销(返程飞行)与同屏原地状态切换
- Pop 返回 / 关闭弹窗:当
viewB脱离窗口而底层viewA仍然存活(或正在重新挂载)时,unregister(viewB)捕获viewB最后的实时窗口坐标框(包含下拉关闭手势产生的transform偏移与缩放),飞回viewA的静止目标框(destFrameHint)。 - 同屏原地切换(In-place toggle):当
viewA卸载使live[key]变空时,unregister(viewA)将viewA的快照暂存到recentlyUnregistered[key](保留300 ms匹配窗口并记录sourceViewId防止同实例重挂载产生幽灵残影)。当下一帧viewB以相同键挂载时,runMatchPass将暂存的源快照与viewB配对并启动飞行。
双端实现细节
iOS(ios/*.swift + SharedHeroView.mm)
OverlayHost.swift:按需创建透明、不可交互的UIWindow(windowLevel = .alert + 1,isUserInteractionEnabled = false)及OverlayRootViewController。浮层窗口在首次注册时即完成渲染服务预热并保持常驻可见,消除首次飞行时的 1 帧白闪,同时将状态栏与屏幕旋转查询委托给应用主窗口。FlightEngine.swift:固定时长动画基于UIViewPropertyAnimator(UICubicTimingParameters),弹簧动画基于UISpringTimingParameters(mass:stiffness:damping:initialVelocity:)。通过读取flightView.layer.presentation()支持半空打断与无缝接管(canTakeOver/interrupt)。InteractiveStackPop.swift与InteractiveModalReturn.swift:通过CADisplayLink监听UINavigationController的interactivePopGestureRecognizer以及 UIKit 弹窗/半屏面板的下拉关闭手势,使浮层 1:1 跟随手指进度并自动补偿底层列表的视差滑动(parallax)。
Android(android/src/main/java/com/sharedhero/*.kt)
OverlayHost.kt:优先从目标视图自身的view.rootView(DecorView)解析ViewGroupOverlay,回退到activity.window.decorView.overlay。因此无论是在主 Activity 窗口内,还是跨越 React Native<Modal>的独立Dialog窗口,浮层都能处于正确的窗口层级。FlightEngine.kt:在ViewGroupOverlay中挂载自定义FrameLayout,通过动态ViewOutlineProvider(outline.setRoundRect(0, 0, w, h, currentRadius))实现morph圆角变形。三次贝塞尔曲线动画由Choreographer+PathInterpolator驱动,弹簧动画由androidx.dynamicanimation.animation.SpringAnimation(FloatValueHolder(0f..1000f))驱动。HeroRegistry.kt与HeroSnapshot.kt:维护严格受限的lastKnownSnapshotsLRU 缓存,确保即使连续打开上百个不同的详情页,原生Bitmap堆内存占用也始终保持恒定。