3D卡片
想要实现这个效果,需要处理以下几点:
-
手势处理
-
手势转换为3D角度
-
角度影响视图
1. 手势处理
这种3D卡片本身没有什么技术难度,也就是多层的图像堆积,想要实现这种跟随手势的3D效果,需要获取手势移动的距离,同时需要注意在手势终止之后需要将视图恢复到初始状态。
-
手势监听 手势监听只需要将手势的移动距离保存起来,在SwiftUI中使用一个@State变量保存即可
-
手势终止后将视图恢复到初始状态 手势终止后将移动距离变量恢复为0即可
具体代码如下:
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.32, blendDuration: 0.32)) {
offset = .zero
}
}
)
2. 将手势转换为3D角度
当手指在页面上向下拖动时,卡片对应的向下方旋转(对应旋转轴是x);当手指在视图上左右拖动时,卡片对应左右旋转(对应旋转轴是y);
至于拖动的距离与旋转角度的关系就随你定义。这里取 10/屏幕长宽。
func offset2Angle(_ isVertical: Bool = false) -> Angle {
let progress = (isVertical ? -offset.height : offset.width) / (isVertical ? screenSize.height : screenSize.width)
return Angle(degress: progress * 10)
}
这里因为需要计算两个维度的角度,然后通过组合来实现立体的旋转,所以增加一个条件以区别两个角度。
这里还有一个辅助函数用于获取屏幕尺寸,就不展开说了。
var screenSize: CGSize = {
guard let window = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return .zero }
return window.screen.bounds.size
}()
3. 角度影响视图
最后就是最简单的一个步骤:给视图添加3D旋转
.rotation3DEffect(offset2Angle(true), axis: (x: 1, y: 0, z: 0)) // 旋转轴为x
.rotation3DEffect(offset2Angle(), axis: (x: 0, y: 1, z: 0)) // 旋转轴为y
为了使得3D效果更明显,还可以在前景视图上添加offset位移,让卡片旋转时带动前景移动
.offset(x: offset2Angle().degress * 5, y: -offset2Angle(true).degress * 5)
总结
这种3D效果看起来很厉害,实际上运用到的知识却很简单,关键在于空间理解能力。
完整代码
struct ContentView: View {
@State var offset: CGSize = .zero
var body: some View {
GeometryReader { proxy in
let size = proxy.size
let imageSize = size.width * 0.7
VStack {
Image("img")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: imageSize)
.zIndex(1)
.offset(x: offset2Angle().degrees * 5, y: -offset2Angle(true).degrees * 5)
}
.padding(.top, 65)
.padding(.horizontal, 15)
.padding(.bottom, 30)
.frame(width: imageSize)
.background {
ZStack {
Rectangle()
.fill(.blue)
}
.clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
}
.rotation3DEffect(offset2Angle(true), axis: (x: 1, y: 0, z: 0))
.rotation3DEffect(offset2Angle(), axis: (x: 0, y: 1, z: 0))
.scaleEffect(0.9)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.32, blendDuration: 0.32)) {
offset = .zero
}
}
)
}
}
func offset2Angle(_ isVertical: Bool = false) -> Angle {
let progress = (isVertical ? -offset.height : offset.width) / (isVertical ? screenSize.height : screenSize.width)
return .init(degrees: progress * 10)
}
var screenSize: CGSize = {
guard let window = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return .zero }
return window.screen.bounds.size
}()
}