Fonix
Fonix

Reputation: 11597

iOS equivalent of Androids ValueAnimator

I would like something that mimics the behaviour in iOS of the ValueAnimator in Android. Under normal circumstances I would use a UIView animation, but unfortunately the objects value im trying to interpolate over time isnt working with a normal animation.

In my particular case im using a Lottie animation, and trying to change the progress of the animation via a UIView animation, but instead of the animation changing the value of the progress over time, it just jumps to the final value. eg:

let lottieAnim = ...
lottieAnim.animationProgress = 0

UIView.animate(withDuration: 2) {
    lottieAnim.animationProgress = 1
}

This example does not animate the lottie animation over time, but simply jumps to the end. I know lottie has methods to play the animation, but Im trying to use a custom animation curve to set the progress (its a progress animation that has no finite duration) which is why I need to use a UIView animation to interpolate the value.

I need something that updates intermittently to mimic an animation, the first thing that comes to mind is Android ValueAnimator class.

Upvotes: 2

Views: 1263

Answers (1)

Fonix
Fonix

Reputation: 11597

I put together a class that will mimic a ValueAnimator from android somewhat, and will allow you to specify your own animation curve if need be (default is just linear)

Simple usage

let valueAnimator = ValueAnimator(duration: 2) { value in
  animationView.animationProgress = value
}

valueAnimator.start()

Advanced usage

let valueAnimator = ValueAnimator(from: 0, to: 100, duration: 60, animationCurveFunction: { time, duration in
  return atan(time)*2/Double.pi
}, valueUpdater: { value in
  animationView.animationProgress = value
})

valueAnimator.start()

You can cancel it at any point as well using:

valueAnimator.cancel()

Value Animator class in Swift 5

// swiftlint:disable:next private_over_fileprivate
fileprivate var defaultFunction: (TimeInterval, TimeInterval) -> (Double) = { time, duration in
  return time / duration
}

class ValueAnimator {

  let from: Double
  let to: Double
  var duration: TimeInterval = 0
  var startTime: Date!
  var displayLink: CADisplayLink?
  var animationCurveFunction: (TimeInterval, TimeInterval) -> (Double)
  var valueUpdater: (Double) -> Void

  init (from: Double = 0, to: Double = 1, duration: TimeInterval, animationCurveFunction: @escaping (TimeInterval, TimeInterval) -> (Double) = defaultFunction, valueUpdater: @escaping (Double) -> Void) {
    self.from = from
    self.to = to
    self.duration = duration
    self.animationCurveFunction = animationCurveFunction
    self.valueUpdater = valueUpdater
  }

  func start() {
    displayLink = CADisplayLink(target: self, selector: #selector(update))
    displayLink?.add(to: .current, forMode: .default)
  }

  @objc
  private func update() {

    if startTime == nil {
      startTime = Date()
      valueUpdater(from + (to - from) * animationCurveFunction(0, duration))
      return
    }

    var timeElapsed = Date().timeIntervalSince(startTime)
    var stop = false

    if timeElapsed > duration {
      timeElapsed = duration
      stop = true
    }

    valueUpdater(from + (to - from) * animationCurveFunction(timeElapsed, duration))

    if stop {
      cancel()
    }
  }

  func cancel() {
    self.displayLink?.remove(from: .current, forMode: .default)
    self.displayLink = nil
  }
}

You could probably improve this to be more generic but this serves my purpose.

Upvotes: 6

Related Questions