我使用Animator课程在我的Android应用中制作了一个相当复杂的动画。我想将此动画移植到iOS。最好是有点像Android Animator。我环顾四周,似乎没有什么是我想要的。我得到的最接近的是CAAnimation。但不幸的是,如果他们被安排在一个小组中,所有儿童代表都会被忽视。
让我先从我在Android上制作的动画开始。我动画了三个视图组(包含ImageView和TextView)。每个按钮我有一个动画,它将视图转换为左边,同时将alpha动画为0.在该动画之后,还有另一个动画,它将同一视图从右侧转换为原始位置,并将alpha动画回1。除了翻译和alpha动画之外,还有一个视图还具有缩放动画。所有视图都使用不同的计时功能(缓动)。动画和动画输出是不同的,一个视图具有不同的缩放计时功能,而alpha和translate动画使用相同的。第一个动画结束后,我设置值以准备第二个动画。缩放动画的持续时间也比翻译和alpha动画短。我将单个动画(translate和alpha)放在AnimatorSet(基本上是动画组)中。这个AnimatorSet放在另一个AnimatorSet中,以便在彼此之后运行动画(首先是动画,然后是动画)。这个AnimatorSet放在另一个AnimatorSet中,它同时运行所有3个按钮的动画。
很抱歉很长的解释。但是这样你才能理解我是如何尝试将其移植到iOS的。这个对于UIView.animate()来说太复杂了。如果放入CAAnimationGroup,CAAnimation会覆盖委托。根据我的知识,ViewPropertyAnimator不允许自定义计时功能,也无法协调多个动画。
有人知道我可以使用什么吗?我也可以使用自定义实现,每次动画都会给我回调,这样我就可以相应地更新视图。
修改
Android动画代码:
fun setState(newState: State) {
if(state == newState) {
return
}
processing = false
val prevState = state
state = newState
val reversed = newState.ordinal < prevState.ordinal
val animators = ArrayList<Animator>()
animators.add(getMiddleButtonAnimator(reversed, halfAnimationDone = {
displayMiddleButtonState()
}))
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(leftButton, leftButton, leftButton.imageView.width.toFloat(), reversed, halfAnimationDone = {
displayLeftButtonState()
}))
}
if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
animators.add(getButtonAnimator(
if(newState == State.TAKE_PICTURE) rightButton else null,
if(newState == State.CROP_PICTURE) rightButton else null,
rightButton.imageView.width.toFloat(),
reversed,
halfAnimationDone = {
displayRightButtonState(inAnimation = true)
}))
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
animatorSet.start()
}
fun getButtonAnimator(animateInView: View?, animateOutView: View?, maxTranslationXValue: Float, reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animators = ArrayList<Animator>()
if(animateInView != null) {
val animateInAnimator = getSingleButtonAnimator(animateInView, maxTranslationXValue, true, reversed)
if(animateOutView == null) {
animateInAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
halfAnimationDone()
}
})
}
animators.add(animateInAnimator)
}
if(animateOutView != null) {
val animateOutAnimator = getSingleButtonAnimator(animateOutView, maxTranslationXValue, false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
animators.add(animateOutAnimator)
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(animators)
return animatorSet
}
private fun getSingleButtonAnimator(animateView: View, maxTranslationXValue: Float, animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val fadeDuration = translateDuration
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(animateView, "translationX", *translateValues)
val fadeAnimator = ObjectAnimator.ofFloat(animateView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, fadeAnimator)
return animateSet
}
fun getMiddleButtonAnimator(reversed: Boolean, halfAnimationDone: () -> Unit): Animator {
val animateInAnimator = getMiddleButtonSingleAnimator(true, reversed)
val animateOutAnimator = getMiddleButtonSingleAnimator(false, reversed)
animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
halfAnimationDone()
}
})
val animatorSet = AnimatorSet()
animatorSet.playTogether(animateInAnimator, animateOutAnimator)
return animatorSet
}
private fun getMiddleButtonSingleAnimator(animateIn: Boolean, reversed: Boolean): Animator {
val translateDuration = 140L
val scaleDuration = 100L
val fadeDuration = translateDuration
val maxTranslationXValue = middleButtonImageView.width.toFloat()
val translateValues =
if(animateIn) {
if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
else floatArrayOf(maxTranslationXValue, 0f)
} else {
if(reversed) floatArrayOf(0f, maxTranslationXValue)
else floatArrayOf(0f, -maxTranslationXValue)
}
val scaleValues =
if(animateIn) floatArrayOf(0.8f, 1f)
else floatArrayOf(1f, 0.8f)
val alphaValues =
if(animateIn) {
floatArrayOf(0f, 1f)
} else {
floatArrayOf(1f, 0f)
}
val translateAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "translationX", *translateValues)
val scaleXAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleX", *scaleValues)
val scaleYAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleY", *scaleValues)
val fadeAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "alpha", *alphaValues)
translateAnimator.duration = translateDuration
scaleXAnimator.duration = scaleDuration
scaleYAnimator.duration = scaleDuration
fadeAnimator.duration = fadeDuration
if(animateIn) {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
} else {
translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
}
if(animateIn) {
val scaleStartDelay = translateDuration - scaleDuration
val scaleStartValue = scaleValues[0]
middleButtonImageView.scaleX = scaleStartValue
middleButtonImageView.scaleY = scaleStartValue
scaleXAnimator.startDelay = scaleStartDelay
scaleYAnimator.startDelay = scaleStartDelay
}
val animateSet = AnimatorSet()
if(animateIn) {
animateSet.startDelay = translateDuration
}
animateSet.playTogether(translateAnimator, scaleXAnimator, scaleYAnimator)
return animateSet
}
修改2
以下是动画在Android上的外观视频:
答案 0 :(得分:1)
这是我认为你正在寻找的动画的开始。如果您不喜欢幻灯片的时间安排,那么您可以将UIView.animate
与.curveEaseInOut
切换为CAKeyframeAnimation
,您可以更精细地控制每一帧。对于要动画的每个视图,您需要CAKeyFrameAnimation
。
这是一个游乐场,您可以将其复制并粘贴到空的操场上,以便查看它的实际效果。
import UIKit
import Foundation
import PlaygroundSupport
class ViewController: UIViewController {
let bottomBar = UIView()
let orangeButton = UIButton(frame: CGRect(x: 0, y: 10, width: 75, height: 75))
let yellow = UIView(frame: CGRect(x: 20, y: 20, width: 35, height: 35))
let magenta = UIView(frame: CGRect(x: 80, y: 30, width: 15, height: 15))
let cyan = UIView(frame: CGRect(x: 50, y: 20, width: 35, height: 35))
let brown = UIView(frame: CGRect(x: 150, y: 30, width:
15, height: 15))
let leftBox = UIView(frame: CGRect(x: 15, y: 10, width: 125, height: 75))
func setup() {
let reset = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
reset.backgroundColor = .white
reset.addTarget(self, action: #selector(resetAnimation), for: .touchUpInside)
self.view.addSubview(reset)
bottomBar.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
bottomBar.backgroundColor = .purple
self.view.addSubview(bottomBar)
orangeButton.backgroundColor = .orange
orangeButton.center.x = bottomBar.frame.size.width / 2
orangeButton.addTarget(self, action: #selector(orangeTapped(sender:)), for: .touchUpInside)
orangeButton.clipsToBounds = true
bottomBar.addSubview(orangeButton)
yellow.backgroundColor = .yellow
orangeButton.addSubview(yellow)
magenta.backgroundColor = .magenta
magenta.alpha = 0
orangeButton.addSubview(magenta)
// Left box is an invisible bounding box to get the effect that the view appeared from nowhere
// Clips to bounds so you cannot see the view when it has not been animated
// Try setting to false
leftBox.clipsToBounds = true
bottomBar.addSubview(leftBox)
cyan.backgroundColor = .cyan
leftBox.addSubview(cyan)
brown.backgroundColor = .brown
brown.alpha = 0
leftBox.addSubview(brown)
}
@objc func orangeTapped(sender: UIButton) {
// Perform animation
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
self.yellow.frame = CGRect(x: -20, y: 30, width: 15, height: 15)
self.yellow.alpha = 0
self.magenta.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
self.magenta.alpha = 1
self.cyan.frame = CGRect(x: -150, y: 30, width: 15, height: 15)
self.cyan.alpha = 0
self.brown.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
self.brown.alpha = 1
}, completion: nil)
}
@objc func resetAnimation() {
// Reset the animation back to the start
yellow.frame = CGRect(x: 20, y: 20, width: 35, height: 35)
yellow.alpha = 1
magenta.frame = CGRect(x: 80, y: 30, width: 15, height: 15)
magenta.alpha = 0
cyan.frame = CGRect(x: 50, y: 20, width: 35, height: 35)
cyan.alpha = 1
brown.frame = CGRect(x: 150, y: 30, width: 15, height: 15)
brown.alpha = 0
}
}
let viewController = ViewController()
viewController.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667)
viewController.view.backgroundColor = .blue
viewController.setup()
PlaygroundPage.current.liveView = viewController
答案 1 :(得分:1)
所以我一直在使用CADisplayLink
开发自己的解决方案。这就是文档描述CADisplayLink
:
CADisplayLink是一个允许您的应用程序使用的计时器对象 将其绘图与显示的刷新率同步。
它基本上提供了何时执行绘图代码的回调(因此您可以顺利地运行动画)。
我不会在这个答案中解释所有内容,因为它会成为很多代码,而且大部分代码都应该清楚。如果某些内容不清楚或您有疑问,可以在此答案下方发表评论。
此解决方案为动画提供了完全的自由,并提供了协调它们的能力。我在Android上的Animator
课上看了很多,想要类似的语法,这样我们就可以轻松地将动画从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 boolean。这个布尔值是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
。默认情况下,它会同时运行所有动画,但如果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()
}
}
设置为true,它将按顺序运行所有动画。
演示时间:
onAnimationUpdate
我认为大部分内容都不言自明。我创建了一个转换x并淡出的视图。对于每个动画,您实现onAnimationUpdate
回调以更改视图中使用的值,例如转换x和alpha。
注意:与Android相反,持续时间和延迟以秒为单位,而不是毫秒。
我们正在使用此代码,效果很好!我已经在我们的Android应用中写了一些动画内容。我可以轻松地将动画移植到iOS,并进行一些最小的重写,动画的工作方式完全相同!我可以复制我的问题中写的代码,将Kotlin代码更改为Swift,应用{{1}},将持续时间和延迟更改为秒,动画就像魅力一样。
我想将其作为开源库发布,但我还没有这样做。当我发布它时,我会更新这个答案。
如果您对代码或代码有任何疑问,请随时提问。