请稍侯

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实现高级动画