大多数用户已经习惯了点击深色模式按钮后界面瞬间变色的体验。但当你想做出让人记住的产品时,这种"啪"一下的切换反而成了减分项。

开发者Mario Prieta最近开源的解决方案,用一张当前界面的"快照"做遮罩,让主题在底层悄悄完成替换。整个过程不需要预构建(prebuild),在Expo Go里直接跑通。

为什么瞬间切换显得廉价

手机系统的主题切换早就不是新鲜事。iOS和Android的深色模式切换都有渐变动画,用户潜意识里已经建立了"好产品应该有过渡"的预期。

React Native的生态长期缺这块。原生端有解决方案,但要么要改原生代码,要么和Expo Go不兼容。对用Expo做快速迭代的团队来说,这意味着要在"开发效率"和"体验精致度"之间二选一。

Prieta的切入点是Skia的makeImageFromView方法。这个API能把当前视图转成图片,正好用来做遮罩层。主题在遮罩底下换完,再让遮罩以动画形式消失——视觉上就是一次平滑的转场。

他把这个模式打包成了react-native-theme-transition。安装后要处理三件事:Skia依赖、Worklets插件、以及用Provider包裹应用。

Expo SDK 55以上的版本有个细节省事了:babel-preset-expo已经内置了react-native-reanimated/plugin,不需要再手动加。但react-native-worklets/plugin得放在babel配置的最后一位,这个顺序不能错。

九种转场动画的取舍

库内置了九种过渡效果,每种对应不同的参数设计。调用setTheme时传一个transition字段就能指定:

circularReveal和heart、star这类形状动画,需要指定origin——可以是某个视图的ref,也可以是坐标对象。还有个inverted参数控制动画方向。

wipe和slide是方向驱动的,直接传direction。split稍微复杂点,要指定mode和inverted。pixelize和dissolve则涉及着色器参数,调起来更像在做视觉特效。

所有动画共享三个基础参数:duration、easing、animated。这意味着你可以全局统一风格,也可以在单次调用时覆盖。

一个典型的circularReveal调用长这样:把按钮的ref传给origin,点击时从按钮位置向外扩散。这种"从操作点出发"的反馈,比全屏统一动画更符合直觉。

Snack上有实时演示,iOS可以直接看效果。Android的支持情况文档里没细说,需要实际测试。

和Expo Router的集成坑

用Expo Router的项目要注意:ThemeTransitionProvider的包裹位置得放在路由外层,但又不能太外层——系统外观监听和持久化逻辑都依赖这个Provider的生命周期。

Prieta的文档里专门提了system appearance的处理。如果跟随系统主题,要在Provider里配好监听逻辑。持久化则依赖你选的存储方案,库本身不强制绑定。

有个容易被忽略的点:makeImageFromView在视图很复杂时可能有性能开销。虽然Skia的渲染管线比纯JS方案快,但如果页面有大量图片或视频,快照生成还是会掉帧。文档建议在这种场景下用更简单的dissolve或fade,避开需要全屏抓图的动画。

给AI编程工具用户的捷径

如果你用Cursor、Windsurf这类AI编程助手,Prieta准备了一个skill文件。一行命令就能让AI处理完安装、babel配置、Provider包裹和示例代码:

npx skills add https://skills.sh/marioprieta/skills/react-native-theme-transition

这个设计很符合当下的开发习惯。主题切换的集成步骤虽然不多,但涉及多个文件的修改顺序,对AI来说正好是擅长处理的确定性任务。人类开发者可以省掉看文档的时间,直接跳到调动画参数。

不过skill的自动化也有边界。如果你的项目有自定义的babel配置,或者用了非标准的主题结构,AI可能会按默认模板生成冲突代码。这时候还是需要人工介入。

技术实现的核心权衡

快照遮罩的方案不是唯一的解法。另一种思路是用React Native的Animated API逐元素过渡,让每个组件自己处理颜色插值。这种方案更"React",但实现成本极高——需要遍历所有受主题影响的节点,还要处理嵌套组件的时序同步。

Prieta选择了"作弊":视觉上骗过眼睛,工程上保持简单。makeImageFromView生成的是位图,主题切换发生在遮罩背后,用户看到的只是两张图之间的转场动画。这对大多数场景足够好,除非你的界面有实时视频或Canvas绘制内容——这些在快照里是静态的,切过去会有跳变。

Worklets的作用是把动画逻辑放到UI线程,避免JS线程卡顿。这是React Native动画的常规优化,但配置起来总有各种版本兼容问题。库把这部分封装了,开发者只需要关心调哪个转场效果。

为什么现在做这个

Expo Go的"零原生代码"承诺让很多团队放弃了eject,但代价是放弃一些需要原生模块的能力。主题动画正好卡在这个中间地带——原生端有成熟方案,纯JS端长期空白。

Skia在Expo里的可用性改变了这个局面。makeImageFromView不是为主题切换设计的,但被 repurposed 之后意外地合适。这种"挪用现有API解决相邻问题"的思路,在React Native生态里越来越常见。

对产品经理来说,这个库的价值在于"可演示性"。深色模式切换是用户高频触发的操作,加上动画后,App的精致感会明显提升。而实现成本从"需要排期做原生开发"降到了"npm install加几行配置"。

对开发者来说,它提供了一个可扩展的模板。九种内置动画覆盖不了所有需求,但基于同样的快照机制,可以自定义着色器实现更复杂的效果。Prieta的文档里留了这块的接口说明。

实际落地的建议

如果你正在用Expo做新项目,主题切换动画现在可以进MVP了。配置时间在一小时以内,视觉回报很明显。

已有项目接入前要评估两点:一是当前的主题实现是否和库的Provider模式兼容,二是关键页面有没有无法快照的内容(如实时地图、视频通话)。后者可能需要为这些页面单独禁用动画,或换用简单的fade过渡。

性能测试要覆盖低端Android机。Skia的渲染效率在iOS和新款Android上很稳,但老旧设备的GPU可能成为瓶颈。文档里没给具体的帧率数据,需要自行测试。

最后,如果你团队在用AI编程工具,优先试skill安装。省下的时间可以用来调origin点和easing曲线——这些细节才是让动画从"能用"变"好用"的关键。