简体   繁体   中英

Implicit CALayer animations for custom property in Swift

I've created a animatable Core Graphics drawing using PaintCode. It's basically a circle meter (not unlike the Apple Watch rings), which basically fills up as a timer counts down. The meterLevel controls the fill level of the circle meter, from 0 to 100. Basically, if a timer is set to 10 seconds, I set the meterLevel every 1 seconds to 90, 80, 70, etc...

This works good, however the animation is only drawn ever 1 second, and looks quite choppy. Instead, I'd like it be a smooth continuous filling meter.

Looking around it seemed like subclassing CALayer and creating an implicit animation for the meterLevel property might be the way to go. So here is what I have:

import UIKit

class MeterControlView: UIView
{
var meterLevel: Int = 0 {
    didSet {
        self.layer.setValue(meterLevel, forKey: "meterLevel")
    }
}
var meterText: String = "00:00:00" {
    didSet {
        self.layer.setValue(meterText, forKey: "meterText")
    }
}


override class func layerClass() -> AnyClass {
    return MeterControlLayer.self
}

override func drawRect(rect: CGRect) {
    // Do nothing
    }
}

class MeterControlLayer: CALayer
{
@NSManaged
var meterLevel: Int
var meterText: String = "00:00:00"

override class func needsDisplayForKey(key: String) -> Bool {
    if (key == "meterLevel") {
        return true
    }
    return super.needsDisplayForKey(key)
}

override func actionForKey(key: String) -> CAAction? {
    if (key == "meterLevel") {
        let anim: CABasicAnimation = CABasicAnimation.init(keyPath: key)
        anim.fromValue = self.presentationLayer()?.meterLevel
        anim.duration = 1.0
        return anim
    } else {
        return super.actionForKey(key)
    }
}

override func drawInContext(ctx: CGContext) {
    super.drawInContext(ctx)
    UIGraphicsPushContext(ctx)
    XntervalStyleKit.drawMeterControl(frame: self.bounds, meterTime: meterText, meterLevelValue: CGFloat(meterLevel))
    UIGraphicsPopContext()
    }
}

This unfortunately, doesn't work exactly the way I would expect. The animation is still a bit choppy, though closer to what I want.

My question is more general though, is this the right way to go about accomplishing what I want to do? I couldn't figure out the right way to set the layer properties meterLevel and meterText, without using setValueForKey:. Is that the right way to do this?

Animation/Graphics is definitely new to me. I'm an embedded C software guy trying to get into iOS development as a hobby.

The animation is still a bit choppy, though closer to what I want.

Given this, it seems as if Core Animation is actually drawing your layer every frame (or trying to, anyway).

Unfortunately, once you perform custom layer drawing each frame, your performance becomes main thread-bound: that is, normally, for properties that Core Animation can natively animate (such as bounds), the animation itself is rendered in the render server, which operates out-of-process from your application and has its own, high-priority render thread. The main thread of your application is free to do whatever it wants during these types of animation without any interruption to the animation.

drawInContext(_:) , however, is called on the main thread of your application. If you put a log statement or breakpoint there, is it called many times over the course of your animation duration? If so, then the layer is properly animating. Your drawing operations within this function may be what's holding up the animation.

Try setting drawsAsynchronously to true on your layer. This defers drawing commands to a background thread, and it can improve performance sometimes. (Note that most, if not all, UIGraphics and Core Graphics functions are thread safe as of iOS 4, so background drawing is safe.)

Additionally, depending on how complex your animation is, you may want to draw several intermediate representations in advance (in the background and in parallel, if possible). Store these somewhere in memory if they aren't too large so you can simply display some of these bitmaps in your layer instead of drawing them just-in-time.

I wrote a UIView subclass called ConcentricProgressRingView which does something similar to what you're trying to do.

https://github.com/lionheart/ConcentricProgressRingView

Here's an example of what it looks like:

Usage

At the top of your UIViewController , import the module:

import ConcentricProgressRingView

Then, in your viewDidLoad :

let rings = [
    ProgressRing(color: UIColor(.RGB(232, 11, 45)), backgroundColor: UIColor(.RGB(34, 3, 11))),
    ProgressRing(color: UIColor(.RGB(137, 242, 0)), backgroundColor: UIColor(.RGB(22, 33, 0))),
    ProgressRing(color: UIColor(.RGB(0, 200, 222)), backgroundColor: UIColor(.RGB(0, 30, 28)))
]
let progressRingView = try! ConcentricProgressRingView(center: view.center, radius: radius, margin: margin, rings: rings, defaultColor: UIColor.clearColor(), defaultWidth: 18)
view.addSubview(progressRingView)

Once you've instantiated your ConcentricProgressRingView instance, animate a specific ring to a percentage with setProgress .

ring.arcs[1].setProgress(0.5, duration: 2)

How it works

I'm not exactly sure what I'm doing differently from your example code in the question, but I'm creating a CABasicAnimation and setting a few parameters on it to tweak the animation behavior. The code is open source, so check it out. Hope this helps!

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