简体   繁体   中英

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

I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:

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()
}

For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.

Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?

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

Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.

You'll need a special layer:

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!
    }
}

The layer must actually be in the interface, though it can be impossible for the user to see:

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

Subsequently, we can initialize the spirality of the layer:

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

Now when we want to "animate" the spirality , this is what we do:

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

The console shows the arrival of a succession of intermediate values over the course of 1 second!

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

Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.

Personally, in more complicated animations I would use 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 which means the complicated process of creating the animation is only done once.

Lottie Files has some examples animations as well for you to look.

@Matt provided the answer and gets the checkmark. I'll recap some points for emphasis:

  • UIView animation is great for commonly animated properties, but if you need to vary a property not on UIView's animatable list, you can't use it. You must create a new CALayer and add a CABasicAnimation(keyPath:) to it.
  • I tried but was unable to get my CABasicAnimations to fire by adding them to the default UIView.layer. I needed to create a custom CALayer sublayer to the UIView.layer - something like myView.layer.addSublayer(myLayer)
  • Leave the custom sublayer installed and re-add the CABasicAnimation to that sublayer when (and only when) you want to animate drawing.
  • In the custom CALayer object, be sure to override class func needsDisplay(forKey key: String) -> Bool with your key property (as @Matt's example shows), and also override func draw(in cxt: CGContext) to do your drawing. Be sure to decorate your key property with @objc . And reference the key property within the drawing code.
  • A "gotcha" to avoid: in the UIView object, be sure to null out the usual draw method ( override func draw(_ rect: CGRect) { } ) to avoid conflict between animated and non-animated drawing on the separate layers. For coordinating animated and non-animated content in the same UIView, it's good (necessary?) to do all your drawing from your custom layer.
  • When doing that, use myLayer.setNeedsDisplay() to update the non-animated content within the custom layer; use myLayer.add(myBasicAnimation, forKey:nil) to trigger animated drawing within the custom layer.

As I said above, @Matt answered - but these items seemed worth emphasizing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM