
SwiftUI 为视图提供了丰富的3D变换能力,其中 rotation3DEffect(_:axis:) 可以沿着某个坐标轴旋转视图,并自动应用透视投影。不过它的 axis 参数只接受 (x: CGFloat, y: CGFloat, z: CGFloat),通常我们会传入 (1, 0, 0) 或 (0, 1, 0) 来绕x轴或y轴旋转。一旦想绕任意向量——比如 (0.5, 0.5, 0) 代表x-y平面对角线方向——旋转,直接使用 rotation3DEffect 依然可行,但理解其背后的数学会让我们能更灵活地控制动画,并构建可复用、可自定义轴线的旋转转场。rotation3DEffect 内部其实是将角度与轴单位向量转换为四元数或旋转矩阵,再配合相机投影矩阵完成绘制。我们要做的,就是把这些概念用SwiftUI的方式封装起来,让任意轴旋转像内置API一样易用。
理解3D旋转:轴向量、角度与透视
任何一个三维旋转都可以用“绕某个单位向量 u 旋转 θ 角度”来描述,即轴-角表示。在图形学中,这个旋转通常用四元数或旋转矩阵表示。SwiftUI 的 rotation3DEffect 内部采用的就是这种方法:它要求你提供一个轴向量 (x, y, z),但并非归一化版本,因为内部会自动归一化。因此,你可以传入 (1, 1, 0),效果就等于绕该向量方向旋转,只不过真正的旋转轴是它的单位向量。这恰好为绕“任意轴”旋转提供了可行性——我们只需计算出目标方向的单位向量即可。
但仅靠旋转不够,要让2D屏幕产生纵深感,必须引入透视投影。SwiftUI 在应用 rotation3DEffect 时,会依据视图的锚点(默认中心)和系统相机位置来模拟透视效果。透视强度的控制可以通过 .projectionEffect 或修改 .transform3D 中的 m34 元素来实现,而 rotation3DEffect 实际上已经内置了一个合理的透视值(通常 m34 = -1/500 左右)。理解这一点对后续自定义动画很重要:如果我们完全自己用 .transform3D 构建绕任意轴旋转,就必须手动设置透视矩阵的分量,否则旋转会变成正交投影,视觉上缺乏真实感。
举个具体的例子:要让一个视图绕从左上角到右下角的对角线翻转(即轴向量为 (1, 1, 0)),只需调用 rotation3DEffect(.degrees(angle), axis: (1, 1, 0))。SwiftUI 会将 (1,1,0) 归一化为 (√2/2, √2/2, 0),然后围绕这个轴旋转。透视效果仍然以视图中心为基准,看起来就像纸张沿对角线卷起。这种便利性使得我们无需手动计算矩阵就能实现大多数自定义轴旋转。接下来的挑战是,如何将这种效果封装成一个可复用的修改器,并利用 SwiftUI 的动画系统和转场机制,创建更为流畅的过渡效果。
封装绕任意轴旋转的视图修改器
为了项目中的可复用性,我们通常会创建一个 ViewModifier,将轴方向、角度和透视参数抽象出来。SwiftUI 的 rotation3DEffect 已经可以接收任意轴,因此我们只需包装一层,添加一些便利的初始化方式。例如,定义一个 ArbitraryAxisRotationModifier,它保存角度、轴向量和透视值,然后在 body 中调用 content.rotation3DEffect(angle, axis: axis, anchor: anchor, perspective: perspective)。其中透视值可以通过 ProjectionTransform 或直接利用 .transformEffect 来微调,但 rotation3DEffect 的第四个参数 perspective 默认就是系统默认透视,通常已经足够。
下面的代码展示了一个基础的任意轴旋转修改器,并支持动画。它允许开发者指定轴向量(不必归一化)、角度以及动画时长。我们利用 AnimatableModifier 协议,让角度参数可动画化,这样在改变角度时,SwiftUI 会自动插值生成平滑的旋转动画。
struct ArbitraryRotationModifier: AnimatableModifier {
var angle: Double
var axis: (CGFloat, CGFloat, CGFloat)
var perspective: CGFloat = 0 // 0 表示使用系统默认透视
var animatableData: Double {
get { angle }
set { angle = newValue }
}
func body(content: Content) -> some View {
content
.rotation3DEffect(.degrees(angle), axis: axis, perspective: perspective)
}
}
extension View {
func arbitraryRotation(_ angle: Double, axis: (CGFloat, CGFloat, CGFloat), perspective: CGFloat = 0) -> some View {
modifier(ArbitraryRotationModifier(angle: angle, axis: axis, perspective: perspective))
}
}
这里的 AnimatableModifier 要求实现 animatableData,我们选择角度作为唯一动画数据。当外部状态驱动角度变化时(比如绑定一个 @State angle,然后在手势或按钮中修改它),SwiftUI 会自动在旧值和新值之间插值,并调用 body 进行重绘,形成连续动画。透视参数虽然可以动态变化,但这里没有将其设为可动画属性,因为通常透视在动画过程中保持不变。
如果要更精确地控制透视效果,可以利用 .rotation3DEffect 的 perspective 参数。传入 0 代表使用系统默认透视,通常大约 1/500 左右。也可以传入自定义值,例如 0.3 会产生更夸张的透视变形。需要注意的是,透视值如果设置过大会导致视图在垂直屏幕方向时严重扭曲,且可能与 anchor 产生交互,需要实际调试找到合适数值。此外,在构建转场时,透视参数应当固定,或根据视图尺寸动态计算以保持视觉一致性。
构建自定义旋转转场 Transition
在视图出现和消失时,我们希望使用绕任意轴的翻转作为过渡动画,这就是自定义转场(Custom Transition)的用武之地。SwiftUI 允许我们通过 AnyTransition 的静态方法结合 modifier 来定义进入和退出时的效果。我们可以利用上面封装的 ArbitraryRotationModifier,在出现时将视图从某个角度旋转到 0°,消失时从 0° 旋转到指定角度,从而实现翻转进出。
定义一个 AnyTransition 扩展,让调用者传入轴向量和透视值,生成对应的转场。
extension AnyTransition {
static func arbitraryRotation(axis: (CGFloat, CGFloat, CGFloat), perspective: CGFloat = 0) -> AnyTransition {
.modifier(
active: ArbitraryRotationModifier(angle: 90, axis: axis, perspective: perspective),
identity: ArbitraryRotationModifier(angle: 0, axis: axis, perspective: perspective)
)
}
}
这里将进入前的状态定义为 90° 旋转,即视图开始时像是“背面”朝外(如果配上透明度淡入会更自然)。实际上进入动画是从 90° 插值到 0°,退出则相反。你可以根据需求调整角度,比如 180° 可实现完全翻面,但需要配合 opacity 或双面视图来避免背面显示问题。
使用起来非常简洁:
struct ContentView: View {
@State private var showDetail = false
var body: some View {
VStack {
if showDetail {
DetailView()
.transition(.arbitraryRotation(axis: (0.7, 0.7, 0), perspective: 0.3))
} else {
MainView()
.transition(.arbitraryRotation(axis: (0.7, 0.7, 0), perspective: 0.3))
}
}
.animation(.spring(), value: showDetail)
}
}
我们传入轴向量 (0.7, 0.7, 0) 让翻转发生在接近对角线的方向。透视值设为 0.3 增强立体感。当 showDetail 切换时,两个视图会以自定义旋转方式互换,就像页面斜对角翻开一样。若要实现卡片翻面效果,可以在翻转到 90° 时替换内容,这时通常需要两个视图分别负责正面和背面,并分别应用旋转且让背面视图初始旋转 180°,再通过 rotation3DEffect 和 .opacity 配合来避免视觉穿帮。
如果想更进一步控制翻转的锚点,可以在 rotation3DEffect 中加入 anchor 参数。例如,卡片从左侧边缘翻开,可以设置 anchor 为 .leading。在转场修饰器中,我们需要确保进入和退出使用相同的锚点。可以通过在 ArbitraryRotationModifier 中添加 anchor: UnitPoint 属性,并传递给 rotation3DEffect,这样就能实现更加多样的翻转效果。
最后,注意透视参数与锚点的组合可能会导致旋转过程中视图超出父视图边界,可以通过 .clipped() 或调整容器尺寸来控制。综合运用这些工具,我们就能在 SwiftUI 中实现从简单到复杂的任意轴3D旋转动画,打造富有质感的界面转场。
SwiftUI3D旋转Custom_Rotation_Transition修改时间:2026-08-12 16:55:07