Итак, я работаю над своим собственным решением, используя CADisplayLink
.Вот как описывается в документации CADisplayLink
:
CADisplayLink - это объект таймера, который позволяет вашему приложению синхронизировать чертеж с частотой обновления дисплея.
Itв основном обеспечивает обратный вызов, когда нужно выполнить код рисования (чтобы вы могли плавно запускать анимацию).
Я не собираюсь все объяснять во время этого ответа, потому что это будет много кода, и большая часть его должнабыть ясным.Если что-то неясно или у вас есть вопрос, вы можете прокомментировать этот ответ ниже.
Это решение дает полную свободу анимации и дает возможность координировать их.Я много смотрел на класс Animator
на Android и хотел подобный синтаксис, чтобы мы могли легко переносить анимацию с Android на iOS или наоборот.Я проверил это в течение нескольких дней и также удалил некоторые причуды.Но достаточно разговоров, давайте посмотрим некоторый код!
Это класс Animator
, который является базовой структурой для классов анимации:
class Animator {
internal var displayLink: CADisplayLink? = nil
internal var startTime: Double = 0.0
var hasStarted: Bool = false
var hasStartedAnimating: Bool = false
var hasFinished: Bool = false
var isManaged: Bool = false
var isCancelled: Bool = false
var onAnimationStart: () -> Void = {}
var onAnimationEnd: () -> Void = {}
var onAnimationUpdate: () -> Void = {}
var onAnimationCancelled: () -> Void = {}
public func start() {
hasStarted = true
startTime = CACurrentMediaTime()
if(!isManaged) {
startDisplayLink()
}
}
internal func startDisplayLink() {
stopDisplayLink() // make sure to stop a previous running display link
displayLink = CADisplayLink(target: self, selector: #selector(animationTick))
displayLink?.add(to: .main, forMode: .commonModes)
}
internal func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
@objc internal func animationTick() {
}
public func cancel() {
isCancelled = true
onAnimationCancelled()
if(!isManaged) {
animationTick()
}
}
}
Он содержит все жизненно важные функции, такие как запускCADisplayLink
, предоставляя возможность остановки CADisplayLink
(когда анимация завершена), логические значения, которые указывают состояние и некоторые обратные вызовы.Вы также заметите логическое значение isManaged.Это логическое значение, когда Animator
контролируется группой.Если это так, группа будет предоставлять анимационные тики, и этот класс не должен начинать CADisplayLink
.
Следующим будет ValueAnimator
:
class ValueAnimator : Animator {
public internal(set) var progress: Double = 0.0
public internal(set) var interpolatedProgress: Double = 0.0
var duration: Double = 0.3
var delay: Double = 0
var interpolator: Interpolator = EasingInterpolator(ease: .LINEAR)
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed: Double = (isCancelled) ? self.duration : CACurrentMediaTime() - startTime - delay
if(elapsed < 0) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
if(duration <= 0) {
progress = 1.0
} else {
progress = min(elapsed / duration, 1.0)
}
interpolatedProgress = interpolator.interpolate(elapsedTimeRate: progress)
updateAnimationValues()
onAnimationUpdate()
if(elapsed >= duration) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
internal func updateAnimationValues() {
}
}
Этот класс являетсябазовый класс для всех значений аниматоров.Но он также может быть использован для анимации, если вы хотите сделать вычисления самостоятельно.Вы, наверное, заметили Interpolator
и interpolatedProgress
здесь.Класс Interpolator
будет показан чуть позже.Этот класс обеспечивает ослабление анимации.Вот где приходит interpolatedProgress
. progress
- это просто линейный прогресс от 0,0 до 1,0, но interpolatedProgress
может иметь другое значение для замедления.Например, когда progress
имеет значение 0,2, interpolatedProgress
может уже иметь 0,4 в зависимости от того, какое смягчение вы будете использовать.Также обязательно используйте interpolatedProgress
для вычисления правильного значения.Ниже приведен пример первого подкласса ValueAnimator
.
Ниже приведен CGFloatValueAnimator
, который, как следует из названия, анимирует значения CGFloat:
class CGFloatValueAnimator : ValueAnimator {
private let startValue: CGFloat
private let endValue: CGFloat
public private(set) var animatedValue: CGFloat
init(startValue: CGFloat, endValue: CGFloat) {
self.startValue = startValue
self.endValue = endValue
self.animatedValue = startValue
}
override func updateAnimationValues() {
animatedValue = startValue + CGFloat(Double(endValue - startValue) * interpolatedProgress)
}
}
Это примеро том, как создать подкласс ValueAnimator
, и вы можете сделать многое другое, например, если вам нужны другие, например, двойные или целые числа.Вы просто указываете начальное и конечное значение, и Animator
вычисляет на основе interpolatedProgress
, что является текущим animatedValue
.Вы можете использовать это animatedValue
, чтобы обновить ваш взгляд.В конце я приведу пример.
Поскольку я уже упоминал Interpolator
уже пару раз, мы перейдем к Interpolator
сейчас:
protocol Interpolator {
func interpolate(elapsedTimeRate: Double) -> Double
}
Это простопротокол, который вы можете реализовать самостоятельно.Я покажу вам часть класса EasingInterpolator
, который я использую сам.Я могу предоставить больше, если кому-то это нужно.
class EasingInterpolator : Interpolator {
private let ease: Ease
init(ease: Ease) {
self.ease = ease
}
func interpolate(elapsedTimeRate: Double) -> Double {
switch (ease) {
case Ease.LINEAR:
return elapsedTimeRate
case Ease.SINE_IN:
return (1.0 - cos(elapsedTimeRate * Double.pi / 2.0))
case Ease.SINE_OUT:
return sin(elapsedTimeRate * Double.pi / 2.0)
case Ease.SINE_IN_OUT:
return (-0.5 * (cos(Double.pi * elapsedTimeRate) - 1.0))
case Ease.CIRC_IN:
return -(sqrt(1.0 - elapsedTimeRate * elapsedTimeRate) - 1.0)
case Ease.CIRC_OUT:
let newElapsedTimeRate = elapsedTimeRate - 1
return sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate)
case Ease.CIRC_IN_OUT:
var newElapsedTimeRate = elapsedTimeRate * 2.0
if (newElapsedTimeRate < 1.0) {
return (-0.5 * (sqrt(1.0 - newElapsedTimeRate * newElapsedTimeRate) - 1.0))
}
newElapsedTimeRate -= 2.0
return (0.5 * (sqrt(1 - newElapsedTimeRate * newElapsedTimeRate) + 1.0))
default:
return elapsedTimeRate
}
}
}
Это всего лишь несколько примеров расчетов для конкретных послаблений.Я фактически перенес все настройки, сделанные для Android, расположенные здесь: https://github.com/MasayukiSuda/EasingInterpolator.
Перед тем, как показать пример, у меня есть еще один класс для показа.Какой класс позволяет группировать аниматоры:
class AnimatorSet : Animator {
private var animators: [Animator] = []
var delay: Double = 0
var playSequential: Bool = false
override func start() {
super.start()
}
override func animationTick() {
// In case this gets called after we finished
if(hasFinished) {
return
}
let elapsed = CACurrentMediaTime() - startTime - delay
if(elapsed < 0 && !isCancelled) {
return
}
if(!hasStartedAnimating) {
hasStartedAnimating = true
onAnimationStart()
}
var finishedNumber = 0
for animator in animators {
if(!animator.hasStarted) {
animator.start()
}
animator.animationTick()
if(animator.hasFinished) {
finishedNumber += 1
} else {
if(playSequential) {
break
}
}
}
if(finishedNumber >= animators.count) {
endAnimation()
}
}
private func endAnimation() {
hasFinished = true
if(!isManaged) {
stopDisplayLink()
}
onAnimationEnd()
}
public func addAnimator(_ animator: Animator) {
animator.isManaged = true
animators.append(animator)
}
public func addAnimators(_ animators: [Animator]) {
for animator in animators {
animator.isManaged = true
self.animators.append(animator)
}
}
override func cancel() {
for animator in animators {
animator.cancel()
}
super.cancel()
}
}
Как вы можете видеть, здесь я установил логическое значение isManaged
.Вы можете поместить несколько аниматоров, которые вы делаете внутри этого класса, чтобы координировать их.И поскольку этот класс также расширяет Animator
, вы также можете добавить еще AnimatorSet
или несколько.По умолчанию он запускает все анимации одновременно, но если для playSequential
установлено значение true, он будет запускать все анимации по порядку.
Время для демонстрации:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let animView = UIView()
animView.backgroundColor = UIColor.yellow
self.view.addSubview(animView)
animView.snp.makeConstraints { maker in
maker.width.height.equalTo(100)
maker.center.equalTo(self.view)
}
let translateAnimator = CGFloatValueAnimator(startValue: 0, endValue: 100)
translateAnimator.delay = 1.0
translateAnimator.duration = 1.0
translateAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
translateAnimator.onAnimationStart = {
animView.backgroundColor = UIColor.blue
}
translateAnimator.onAnimationEnd = {
animView.backgroundColor = UIColor.green
}
translateAnimator.onAnimationUpdate = {
animView.transform.tx = translateAnimator.animatedValue
}
let alphaAnimator = CGFloatValueAnimator(startValue: animView.alpha, endValue: 0)
alphaAnimator.delay = 1.0
alphaAnimator.duration = 1.0
alphaAnimator.interpolator = EasingInterpolator(ease: .CIRC_IN_OUT)
alphaAnimator.onAnimationUpdate = {
animView.alpha = alphaAnimator.animatedValue
}
let animatorSet = AnimatorSet()
// animatorSet.playSequential = true // Uncomment this to play animations in order
animatorSet.addAnimator(translateAnimator)
animatorSet.addAnimator(alphaAnimator)
animatorSet.start()
}
}
Я думаюбольшая часть этого будет говорить сама за себя.Я создал вид, который переводит х и исчезает.Для каждой анимации вы реализуете обратный вызов onAnimationUpdate
для изменения значений, используемых в представлении, как в этом случае для перевода x и alpha.
Примечание: В отличие от Android, продолжительность изадержка здесь в секундах вместо миллисекунд.
Мы работаем с этим кодом прямо сейчас, и он отлично работает!Я уже написал некоторые анимационные материалы в нашем приложении для Android.Я мог бы легко перенести анимацию на iOS с минимальным переписыванием, и анимация работает точно так же!Я мог скопировать код, написанный в моем вопросе, изменив код Котлина на Swift, применив onAnimationUpdate
, изменил продолжительность и задержки на секунды, и анимация работала как чудо.
Я хочу выпустить это какбиблиотека с открытым исходным кодом, но я еще не сделал этого.Я обновлю этот ответ, когда выпущу его.
Если у вас есть какие-либо вопросы о коде или его работе, не стесняйтесь спрашивать.