iOS Rotation Animation 旋转动画的几种实现
21 August 2022
iOS Rotation Animation 旋转动画的几种实现
Swift 实现抽奖轮盘动画:
- 绘制轮盘:
RaflleWheel
;- 根据指定的Rect与角度旋转轮盘图片;
- 利用Easing时间曲线函数自定义淡入淡出动画;
- 视图动画、视图动画、属性动画师;
- 旋转(沿着Z轴)CABasicAnimation 与 CASpringAnimation两种实现方式;
//抽奖轮盘
class RaflleWheel: UIView {
var angle: CGFloat = 0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
Self.drawWheel(rect: self.bounds, angle: angle)
}
//根据指定的Rect与角度旋转图片
public class func drawWheel(rect: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100), angle: CGFloat = 0) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()!
let width = rect.size.width
let height = rect.size.height
let radius_h = rect.size.width / 2.0
let radius_v = rect.size.height / 2.0
//// Image Declarations
let image = UIImage(named: "image.png")!
//// Oval Drawing
context.saveGState()
context.translateBy(x: rect.minX + radius_h, y: rect.minY + radius_v)
context.rotate(by: -angle * CGFloat.pi / 180)
let ovalRect = CGRect(x: -radius_h, y: -radius_v, width: width, height: height)
let ovalPath = UIBezierPath(ovalIn: ovalRect)
context.saveGState()
ovalPath.addClip()
context.translateBy(x: floor(ovalRect.minX + 0.5), y: floor(ovalRect.minY + 0.5))
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -height)
context.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: width, height: height))
context.restoreGState()
context.restoreGState()
}
}
class ViewController: UIViewController {
@IBOutlet weak var loop: UITextField!
@IBOutlet weak var stiff: UITextField!
@IBOutlet weak var damping: UITextField!
@IBOutlet weak var velocity: UITextField!
@IBOutlet weak var duration_F: UITextField!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var raffleWheel: RaflleWheel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func btnAction(_ sender: Any) {
self.manualAnimation()
// self.viewAnimate()
// self.animateKeyframes()
// self.propertyAnimator()
// self.addBaseAnimation()
}
@IBAction func springAction(_ sender: Any) {
self.addSpringAnimation()
}
private var idx = 1
private var startValue = 0
private var endValue = 20 * 360 + 180 //n圈 + 半圈
private var total = 100
private let duration_t: CGFloat = 2 //秒
private var currentValue: CGFloat = 0
//手动 动画
func manualAnimation() {
idx = 1
self.updateWhell(angle: CGFloat(currentValue))
}
func updateWhell(angle: CGFloat) {
print("==== angle:\(angle)")
self.raffleWheel.angle = angle
if(idx == total) {
return
}
let ang = easeInOutQuad(elapsed: CGFloat(idx))
idx += 1
DispatchQueue.main.asyncAfter(deadline: .now() + duration_t/100) { [weak self] in
self?.updateWhell(angle: ang)
}
}
//曲线函数 ease timing curve function
func easeInOutQuad(elapsed: CGFloat) -> CGFloat {
var newElapsed = elapsed
newElapsed /= CGFloat(total) / 2
if newElapsed < 1 {
return CGFloat(endValue) / 2 * newElapsed * newElapsed + CGFloat(startValue)
}
newElapsed = newElapsed - 1
return -CGFloat(endValue) / 2 * ((newElapsed) * (newElapsed - 2) - 1) + CGFloat(startValue)
}
// 视图动画
func viewAnimate() {
self.imageView.transform = .identity
UIView.animate(withDuration: 2.0, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 10,
options: UIView.AnimationOptions.curveEaseOut) {
self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
}
}
// 关键帧动画
func animateKeyframes() {
UIView.animateKeyframes(withDuration: 5.0, delay: 0, options: .calculationModeLinear) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 4.5, animations: {
self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2 * 3)
})
}
}
//属性动画师
func propertyAnimator() {
/*
let animator = UIViewPropertyAnimator(duration: 3.0, dampingRatio: 0.4) {
}
animator.addAnimations {
self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}
animator.addCompletion { _ in
self.imageView.transform = .identity
}
animator.startAnimation(afterDelay: 0.5)
// animator.pauseAnimation()
// animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
*/
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 10, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 5, animations: {
self.imageView.transform = CGAffineTransform(rotationAngle: .pi / 2)
})
UIView.addKeyframe(withRelativeStartTime: 5, relativeDuration: 5, animations: {
self.imageView.transform = CGAffineTransform(rotationAngle: .pi + 0.1 * .pi )
})
} completion: { position in
if position == .end {
self.imageView.transform = .identity
}
}
}
//基础动画 - 旋转(沿着Z轴) transform.rotation.z
func addBaseAnimation() {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
//旋转360度 = PI*2
animation.toValue = CGFloat.pi * (Double(loop.text!) ?? 2)
animation.duration = Double(duration_F.text!) ?? 3
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.delegate = self
self.imageView.layer.add(animation, forKey: "basic_rotation_animation")
}
//弹性动画 - 旋转(沿着Z轴) transform.rotation.z
func addSpringAnimation() {
let animation = CASpringAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = CGFloat.pi * (Double(loop.text!) ?? 2)
animation.stiffness = Double(stiff.text!) ?? 100
animation.damping = Double(damping.text!) ?? 5
animation.initialVelocity = Double(velocity.text!) ?? 10
animation.duration = Double(duration_F.text!) ?? animation.settlingDuration
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.delegate = self
self.imageView.layer.add(animation, forKey: "spring_rotation_animation")
}
}
// 动画回调代理
extension ViewController: CAAnimationDelegate {
func animationDidStart(_ anim: CAAnimation) {
print("==== animationDidStart")
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if flag {
self.imageView.layer.removeAllAnimations()
self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi * (Double(loop.text!) ?? 2))
//
print("==== end \(Date().timeIntervalSince1970)")
}
}
}
轮盘交互动画再封装
@IBAction func btnAction(_ sender: Any) {
animation.index = 3
animation.loopSize = 7
animation.turnCallback = { (n: Int) in
print("== ran around \(n) times")
}
animation.startCallback = {
print("== animation start")
}
animation.stopCallback = {
print("== animation stop")
}
if animation.running {
animation.stopAnimate()
}else {
animation.startAnimate()
}
// var animation = ManualAnimation(raffleWheel: self.raffleWheel)
// animation.manualAnimation()
// self.viewAnimate()
// self.animateKeyframes()
// self.propertyAnimator()
// self.addBaseAnimation()
}
class GameAnimation: NSObject {
var index: Int = 0 //第几片
var loopSize: Int = 8 //蛋糕片数
var running: Bool = false //正在运行动画
var stop: Bool = false //停止标志
var retry: Bool = false //重试标志
var interval: Int = 10 // 20毫秒
var unitFactor: CGFloat = 0.618
var unit: CGFloat { //1片蛋糕的弧度 * 0.618
2 * CGFloat.pi / CGFloat(loopSize) * unitFactor
}
var upLen: CGFloat { //加速距离
2 * CGFloat.pi * 3
}
var downLen: CGFloat { //减速距离
2 * CGFloat.pi * 5 + unit * (CGFloat(index) + (retry ? 0.5 : 0.0)) / unitFactor
}
var raffleWheel: RaflleWheel
var turnCallback: ((Int) -> Void)?
var startCallback: (() -> Void)?
var stopCallback: (() -> Void)?
var currentAngle: CGFloat = 0
func startAnimate() {
if stop {
return
}
running = true
updateWheel(angle: 0)
}
func stopAnimate() {
stop = true
running = false
}
init(raffleWheel: RaflleWheel){
self.raffleWheel = raffleWheel
super.init()
}
func updateWheel(angle: CGFloat, i: Int = 1, remain: CGFloat? = nil, factor: CGFloat = 0.25) {
//print("==== angle:\(angle)")
self.raffleWheel.angle = angle
if i == 1 { //start
startCallback?()
}
if i % Int(CGFloat(loopSize)/unitFactor) == 0 {
turnCallback?(i/Int(CGFloat(loopSize)/unitFactor))
}
var currentAngle: CGFloat = 0
/*
//start - easing in
if unit * CGFloat(i) * CGFloat(loopSize) < upLen {
let factor = factor * ( 1 + pow(factor, 2))
let offset = unit * (factor >= 1 ? factor : factor)
currentAngle = (angle + offset).remainder(dividingBy: 2 * CGFloat.pi)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(interval)) { [weak self] in
self?.updateWheel(angle: currentAngle, i: i + 1)
}
return
}
*/
currentAngle = (angle + unit).truncatingRemainder(dividingBy: 2 * CGFloat.pi)
//running
if stop == false {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(interval)) { [weak self] in
self?.updateWheel(angle: currentAngle, i: i + 1)
}
return
}
//stop - easing out
let factor: CGFloat = 1 - pow(factor, 2)
let offset = unit * factor
currentAngle = (angle + offset).remainder(dividingBy: 2 * CGFloat.pi)
var rem = downLen
if let remain = remain {
if remain <= 0 {
stopCallback?()
stop = false
return
}
rem = remain - offset
if rem <= 0 {
currentAngle = (angle + remain).remainder(dividingBy: 2 * CGFloat.pi)
rem = 0
}
}else {
let currentOffset = angle.remainder(dividingBy: 2 * CGFloat.pi)
rem = downLen - currentOffset - offset
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(interval)) {[weak self] in
self?.updateWheel(angle: currentAngle, i: i + 1, remain: rem, factor: factor)
}
}
}
struct ManualAnimation {
var raffleWheel: RaflleWheel
var startValue = 0
var endValue = 12 * CGFloat.pi + 180 //n圈 + 半圈
var total = 100
let duration_t: CGFloat = 2 //秒
var currentValue: CGFloat = 0
//手动 动画
func manualAnimation() {
self.updateWhell(angle: CGFloat(currentValue), idx: 1)
}
func updateWhell(angle: CGFloat, idx: Int) {
print("==== angle:\(angle)")
self.raffleWheel.angle = angle
if(idx == total) {
return
}
let ang = easeInOutQuad(elapsed: CGFloat(idx))
DispatchQueue.main.asyncAfter(deadline: .now() + duration_t/100) {
updateWhell(angle: ang, idx: idx + 1)
}
}
//曲线函数 ease timing curve function
func easeInOutQuad(elapsed: CGFloat) -> CGFloat {
var newElapsed = elapsed
newElapsed /= CGFloat(total) / 2
if newElapsed < 1 {
return CGFloat(endValue) / 2 * newElapsed * newElapsed + CGFloat(startValue)
}
newElapsed = newElapsed - 1
return -CGFloat(endValue) / 2 * ((newElapsed) * (newElapsed - 2) - 1) + CGFloat(startValue)
}
}
参考: Core Animation总结
UI Animations With Swift
使用UIViewPropertyAnimator实现高级动画