facebookarchive / Keyframes

A library for converting Adobe AE shape based animations to a data format and playing it back on Android and iOS devices.
https://facebookincubator.github.io/Keyframes/
Other
5.33k stars 302 forks source link

Help with an optimal method for instantiating multiple KFVectorLayer animations? #69

Open markjeschke opened 7 years ago

markjeschke commented 7 years ago

Below is my code for instantiating three KFVectorLayer animations, which you can check out in my Keyframes-Test repo. There must be a more efficient way to do this dynamically, and with fewer lines of code, right?

override func viewDidLoad() {
        super.viewDidLoad()

        // Create JSON data extractor object for reuse
        let jsonDataExtractor = JsonDataExtractor()

        // Calculated dimensions, relating to the screen's dimensions
        let shortSide = min(self.view.bounds.width, self.view.bounds.height)
        let longSide = max(self.view.bounds.width, self.view.bounds.height)

        ///=============== Animate Keyframe logo on the top to be played twice

        // Top Keyframe animated logo
        let keyframeLogoVector : KFVector!

        // Create animation from JSON vector file.
        do {
            keyframeLogoVector = try jsonDataExtractor.loadVectorFromDisk(assetName: "sample_logo")
        } catch {
            print("Vector file could not be loaded, aborting")
            return
        }

        // Create vector layer
        let keyframeLogoVectorLayer = KFVectorLayer()

        keyframeLogoVectorLayer.frame = CGRect(x: shortSide / 4, y: 20, width: shortSide / 2, height: shortSide / 2)

        // The repeatCount parameter *MUST* be declared prior to the faceModel, and not after. Otherwise, the animation will not stop. If you want the animation to repeat indefinitely, just comment the following line out.
        keyframeLogoVectorLayer.repeatCount = 2

        // Attach the animation to the faceModel.
        keyframeLogoVectorLayer.faceModel = keyframeLogoVector!

        // Add the VectorLayer as a sublayer for the main view.
        self.view.layer.addSublayer(keyframeLogoVectorLayer)

        // Start animation halfway through.
        //keyframeLogoVectorLayer.seek(toProgress: 0.5) //<== Seek to Progress doesn't seem to work

        // Start the animation, which will loop by default.
        keyframeLogoVectorLayer.startAnimation()

        ///=============== Animate "S" Letter to be played once

        let sLetterVector : KFVector!
        do {
            sLetterVector = try jsonDataExtractor.loadVectorFromDisk(assetName: "keyframes")
        } catch {
            print("Vector file could not be loaded, aborting")
            return
        }

        let sLetterVectorLayer = KFVectorLayer()
        sLetterVectorLayer.frame = CGRect(x: shortSide / 4, y: longSide / 2 - shortSide / 4, width: shortSide / 2, height: shortSide / 2)
        sLetterVectorLayer.repeatCount = 1
        sLetterVectorLayer.faceModel = sLetterVector!
        self.view.layer.addSublayer(sLetterVectorLayer)
        sLetterVectorLayer.startAnimation()

        ///=============== Animate Keyframe logo on the bottom to be played infinitely

        // Bottom Keyframe animated logo
        let sampleLogoVector : KFVector!
        do {
            sampleLogoVector = try jsonDataExtractor.loadVectorFromDisk(assetName: "sample_logo")
        } catch {
            print("Vector file could not be loaded, aborting")
            return
        }
        let sampleLogoVectorLayer = KFVectorLayer()
        sampleLogoVectorLayer.frame = CGRect(x: shortSide / 4, y: longSide - 180, width: shortSide / 2, height: shortSide / 2)
        sLetterVectorLayer.repeatCount = 0
        sampleLogoVectorLayer.faceModel = sampleLogoVector!
        self.view.layer.addSublayer(sampleLogoVectorLayer)
        sampleLogoVectorLayer.startAnimation()

    }
markjeschke commented 7 years ago

If you're curious, here's my JsonDataExtractor class:

import UIKit

class JsonDataExtractor: KFVector {

    func loadVectorFromDisk(assetName:String) throws -> KFVector {
        let filePath : String = Bundle(for: type(of: self)).path(forResource: assetName, ofType: "json")!
        let data : Data = try String(contentsOfFile: filePath).data(using: .utf8)!
        let vectorDictionary : Dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:Any]

        return KFVectorFromDictionary(vectorDictionary)
    }

}
renyu-io commented 7 years ago

Hi @markjeschke, I would do pretty much the same as you did. Maybe encapsulate do catch into your extractor too? or have a func keyframesLayerFromAsset(assetName:String) -> KFVectorLayer if those layers share the same setup such as frame, repeatCount etc. All steps are required though.

markjeschke commented 7 years ago

Thanks for your help. How would you encapsulate the do catch into the extractor? It would be great if that could also be included in the keyFramesLayerFromAsset function that you proposed.

For instance, if I added a new animation via a UIButton action, couldn't a single method call could create an object that handles the KFVector, the JSON File path, and the KFVectorLayers, all in one declaration with tuples or instanceType arguments? I'm sorry if this is trivial.

Thanks in advance, Mark

markjeschke commented 7 years ago

I finally came up with a solution, based upon @LazyChild's recommendations. I still need to optimize the code's functionality into separate classes or extensions and remove the print statements. But, I'm hoping that it will help others. Any advice on making the code more efficient is welcome. Here's the ViewController:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let shortSide = min(self.view.bounds.width, self.view.bounds.height)
        let longSide = max(self.view.bounds.width, self.view.bounds.height)

        // ===== Top Animation
        let topKeyframeVector = keyframesLayerFromAsset(assetName: "sample_logo",
                                                       repeatCount: 2,
                                                       x: shortSide / 4 + 25,
                                                       y: 20,
                                                       width: 100,
                                                       height: 100)
        // Add the animation layer to the view.
        self.view.layer.addSublayer(topKeyframeVector)
        topKeyframeVector.startAnimation()

        // ===== Middle Animation
        let sVector = keyframesLayerFromAsset(assetName: "keyframes",
                                                       repeatCount: 3,
                                                       x: shortSide / 4,
                                                       y: longSide / 2 - shortSide / 4,
                                                       width: shortSide / 2,
                                                       height: shortSide / 2)
        // Add the animation layer to the view.
        self.view.layer.addSublayer(sVector)
        sVector.startAnimation()

        // ===== Bottom Animation
        let bottomKeyframeVector = keyframesLayerFromAsset(assetName: "sample_logo",
                                                       repeatCount: 1,
                                                       x: shortSide / 4 + 20,
                                                       y: longSide - 140,
                                                       width: 120,
                                                       height: 120)
        // Add the animation layer to the view.
        self.view.layer.addSublayer(bottomKeyframeVector)
        bottomKeyframeVector.startAnimation()

    }

    // ===== Extract JSON data
    func loadVectorFromDisk(assetName:String) throws -> KFVector {
        let filePath : String = Bundle(for: type(of: self)).path(forResource: assetName, ofType: "json")!
        let data : Data = try String(contentsOfFile: filePath).data(using: .utf8)!
        let vectorDictionary : Dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:Any]

        return KFVectorFromDictionary(vectorDictionary)
    }

    // ===== Call the animation layer and pass in parameters
    func keyframesLayerFromAsset(assetName: String,
                                 repeatCount: Float,
                                 x: CGFloat,
                                 y: CGFloat,
                                 width: CGFloat,
                                 height: CGFloat) -> KFVectorLayer {

        var myVector : KFVector!
        do {
            myVector = try loadVectorFromDisk(assetName: assetName)
            print("assetName: \(assetName)")
        } catch {
            print("Vector file could not be loaded, aborting")
        }

        let myVectorLayer = KFVectorLayer()
        myVectorLayer.frame = CGRect(x: x, y: y, width: width, height: height)

        // For some reason setting a repeatCount = 0 doesn't work on its own.
        if repeatCount != 0 {
           myVectorLayer.repeatCount = repeatCount
        }
        myVectorLayer.faceModel = myVector!

        print("repeatCount: \(repeatCount)\n-----")

        return myVectorLayer
    }

}
markjeschke commented 7 years ago

@lozzle Please feel free to close this issue if it shouldn't be in the Issues section. :)