如何在 Swift 中为绘图设置动画,同时更改 UIImageView 比例?

How to animate drawing in Swift, but also change a UIImageView's scale?

我想为绘图序列制作动画。我的代码将螺旋绘制成 UIImageView.image。该序列改变了图像内容,但也改变了周围 UIImageView 的 scale。代码为螺旋的转数参数化:

func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations)   // do some math to figure the best scale

UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale)      // some animation prohibits changes!
// ...  drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}

例如,我想在 1.0 秒的持续时间内以 20 个增量从 drawSpiral(2.0)drawSpiral(2.75) 设置动画。

我可以设置 UIView.annimate(withDuration...) 以使用连续的中间值调用我的方法吗?如何?有没有更好的动画方法?

就个人而言,在更复杂的动画中我会使用 lottie the animation itself is built with Adobe After Effect and exported as a JSON file which you will manage using the lottie library this approach will save you time and effort when you port your app to another platform like Android as they also have an Android Lottie 这意味着创建动画的复杂过程只需要完成一次。

Lottie Files还有一些示例动画供您查看。

Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values

动画仅仅是一系列定时的中间值被扔向某物。要求将它们扔到您的代码中是完全合理的,这样您就可以随心所欲地使用它们。方法如下。

你需要一个特殊的图层:

class MyLayer : CALayer {
    @objc var spirality : CGFloat = 0
    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(spirality) {
            return true
        }
        return super.needsDisplay(forKey:key)
    }
    override func draw(in con: CGContext) {
        print(self.spirality) // in real life, this is our signal to draw!
    }
}

该层实际上必须在界面中,但用户可能看不到:

let lay = MyLayer()
lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.view.layer.addSublayer(lay)

接下来,我们可以初始化层的spirality

lay.spirality = 2.0
lay.setNeedsDisplay() // prints: 2.0

现在,当我们想要 "animate" spirality 时,这就是我们所做的:

let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
ba.fromValue = lay.spirality
ba.toValue = 2.75
ba.duration = 1
lay.add(ba, forKey:nil)
CATransaction.setDisableActions(true)
lay.spirality = 2.75

控制台显示在 1 秒内连续到达中间值!

2.03143266495317
2.04482554644346
2.05783333256841
2.0708108600229
2.08361491002142
2.0966724678874
2.10976020619273
2.12260236591101
2.13551922515035
2.14842618256807
2.16123360767961
2.17421661689878
2.18713565543294
2.200748950243
2.21360073238611
2.2268518730998
2.23987507075071
2.25273013859987
2.26560932397842
2.27846492826939
2.29135236144066
2.30436328798532
2.31764804571867
2.33049770444632
2.34330793470144
2.35606706887484
2.36881992220879
2.38163591921329
2.39440815150738
2.40716737508774
2.42003352940083
2.43287514150143
2.44590276479721
2.45875595510006
2.47169743478298
2.48451870679855
2.49806520342827
2.51120449602604
2.52407149970531
2.53691896796227
2.54965999722481
2.56257836520672
2.57552136480808
2.58910304307938
2.60209316015244
2.6151298135519
2.62802086770535
2.64094598591328
2.6540260463953
2.6669240295887
2.6798157542944
2.69264766573906
2.70616912841797
2.71896715462208
2.73285858333111
2.74564798176289
2.75
2.75
2.75

这些正是将在动画 属性 中抛出的数字,例如当您将视图的框架原点 x2 更改为 2.75 时一个持续时间为 1 秒的动画。但是现在这些数字以数字的形式出现,因此您现在可以用这一系列数字做任何您喜欢的事情。如果您想在每个新值到达时调用您的方法,请继续。

@Matt 提供了答案并获得了复选标记。我将重述一些重点:

  • UIView 动画非常适合常用的动画属性,但是如果 你需要改变 属性 不在 UIView 的动画列表中,你不能使用它。你必须 创建一个新的 CALayer 并向其添加 CABasicAnimation(keyPath:)
  • 我试过了,但无法通过将它们添加到默认设置来触发我的 CABasicAnimations UIView.layer。我需要创建一个自定义 CALayer UIView.layer 的子层 - 类似于 myView.layer.addSublayer(myLayer)
  • 保留安装的自定义子层,并在(且仅当)您想要动画绘图时将 CABasicAnimation 重新添加到该子层。
  • 在自定义 CALayer 对象中,确保 override class func needsDisplay(forKey key: String) -> Bool 使用您的密钥 属性(如 @Matt 的示例所示),并且 override func draw(in cxt: CGContext) 进行绘图。请务必用 @objc 修饰您的密钥 属性。 并在绘图代码中引用键属性。
  • A "gotcha" 避免:在 UIView 对象中,一定要取消通常的绘制方法 (override func draw(_ rect: CGRect) { }),以避免在单独的图层上动画和非动画绘制之间发生冲突。为了在同一个 UIView 中协调动画和非动画内容,最好(有必要?)从自定义图层中绘制 all
  • 执行此操作时,使用 myLayer.setNeedsDisplay() 更新自定义图层中的非动画内容;使用 myLayer.add(myBasicAnimation, forKey:nil) 在自定义图层中触发动画绘图。

正如我上面所说,@Matt 回答了 - 但这些项目似乎值得强调。