Swift 仿自如 App 裸眼 3D 效果
前两天看了,感觉实现的 banner 设计的很有创意,效果很是惊艳。又看到,然后决定使用 Swift 实现一下。
原理
平时的 banner 都是使用的一张图片,为了达到 3D 的效果,需要将一张图片分成三张,也就是将原来的一张 banner 图片分成三个图层,再将图层分别导出为相同大小的图片。将这三张图片叠加在一起,从后往前分为背景、中景和前景。

当手机的旋转时,中景始终保持不变,给背景和中景位置添加相反的偏移量,就可以达到 3D 的效果了。

原理很简单,接下里就是背景和前景根据什么进行偏移?加速计?还是磁场?陀螺仪?还是重力?
我们先来看各个名词的定义:
为设备在三维空间的瞬时加速度;
为设备相对于地球磁场的方向;
为设备在三个主轴上的瞬时旋转;
为重力在设备各个方向上的分量。重力始终指向地球,通过设备三个方向上的不同分量来表示重力的方向,分量的范围为 0 到 1。

明白这几个定义之后,很明显我们应该选择 的变化来修改图片的位移。再具体点就是,通过重力在设备 x 轴上的变化来修改图片左右的位移距离,通过重力在设备 y 轴上的变化来修改图片上下的位移距离。
实现
CMMotionManager 负责获取和处理手机的运动信息,是 Core Motion 库的核心类,可以使用 CMMotionManager 来获取重力信息,
import CoreMotion
lazy var motionManager: CMMotionManager = {
let manager = CMMotionManager()
manager.deviceMotionUpdateInterval = deviceMotionUpdateInterval
return manager
}()
使用 motionManager 时,有几点需要注意:
开启获取设备信息更新回调,获取重力方向。
private func startMotion() {
// 检查是否可用
guard motionManager.isDeviceMotionAvailable else {
return
}
motionManager.startDeviceMotionUpdates(to: OperationQueue.main) { [weak self] deviceMotion, error in
guard let motion = deviceMotion else {
return
}
// motion.gravity 为重力方向
self?.update(gravityX: motion.gravity.x,
gravityY: motion.gravity.y,
gravityZ: motion.gravity.z)
}
}
根据重力方向给前景和背景添加位移动画。
func update(gravityX: Double, gravityY: Double, gravityZ: Double) {
let timeInterval = sqrt(pow((gravityX - lastGravityX), 2) + pow((gravityY - lastGravityY), 2)) * deviceMotionUpdateInterval
let animationKey = "positionAnimation"
let newBackImageViewCenter = self.center.with {
$0.x += CGFloat(gravityX) * maxOffset
$0.y += CGFloat(gravityY) * maxOffset
}
let backImageViewAnimation = CABasicAnimation(keyPath: "position").then {
$0.fromValue = NSValue(cgPoint: self.backImageViewCenter)
$0.toValue = NSValue(cgPoint: newBackImageViewCenter)
$0.duration = timeInterval
$0.fillMode = .forwards
$0.isRemovedOnCompletion = false
}
backImageView.layer.do {
$0.removeAnimation(forKey: animationKey)
$0.add(backImageViewAnimation, forKey: animationKey)
}
let newFrontImageViewCenter = self.center.with {
$0.x -= CGFloat(gravityX) * maxOffset
$0.y -= CGFloat(gravityY) * maxOffset
}
let frontImageViewAnimation = CABasicAnimation(keyPath: "position").then {
$0.fromValue = NSValue(cgPoint: self.frontImageViewCenter)
$0.toValue = NSValue(cgPoint: newFrontImageViewCenter)
$0.duration = timeInterval
$0.fillMode = .forwards
$0.isRemovedOnCompletion = false
}
frontImageView.layer.do {
$0.removeAnimation(forKey: animationKey)
$0.add(frontImageViewAnimation, forKey: animationKey)
}
self.backImageViewCenter = newBackImageViewCenter
self.frontImageViewCenter = newFrontImageViewCenter
}
到此就可以实现 3D 的效果了。