UIView replicate CAAnimation of another view, in real time?

184 Views Asked by At

So I've got a background view with a gradient sublayer, animating continuously to change the colors slowly. I'm doing it with a CATransaction, because I need to animate other properties as well:

CATransaction.begin()

gradientLayer.add(colorAnimation, forKey: "colors")
// other animations

CATransaction.setCompletionBlock({
    // start animation again, loop forever
}

CATransaction.commit()

Now I want to replicate this gradient animation, let's say, for the title of a button for instance.

Desired result

Note 1: I can't just "make a hole" in the button, if such a thing is possible, because I might have other opaque views between the button and the background.

Note 2: The gradient position on the button is not important. I don't want the text gradient to replicate the exact colors underneath, but rather to mimic the "mood" of the background.

So when the button is created, I add its gradient sublayer to a list of registered layers, that the background manager will update as well:

func register(layer: CAGradientLayer) {
    let pointer = Unmanaged.passUnretained(layer).toOpaque()
    registeredLayers.addPointer(pointer)
}

So while it's easy to animate the text gradient at the next iteration of the animation, I would prefer that the button starts animating as soon as it's added, since the animation usually takes a few seconds. How can I copy the background animation, i.e. set the text gradient to the current state of the background animation, and animate it with the right duration left and timing function?

1

There are 1 best solutions below

0
Morpheus On BEST ANSWER

The solution was indeed to use the beginTime property, as suggested by @Shivam Gaur's comment. I implemented it as follows:

// The background layer, with the original animation
var backgroundLayer: CAGradientLayer!

// The animation
var colorAnimation: CABasicAnimation!

// Variable to store animation begin time
var animationBeginTime: CFTimeInterval!

// Registered layers replicating the animation
private var registeredLayers: NSPointerArray = NSPointerArray.weakObjects()

...

// Somewhere in our code, the setup function
func setup() {
    colorAnimation = CABasicAnimation(keyPath: "colors")
    // do the animation setup here
    ...
}
...

// Called by an external class when we add a view that should replicate the background animation
func register(layer: CAGradientLayer) {

    // Store a pointer to the layer in our array
    let pointer = Unmanaged.passUnretained(layer).toOpaque()
    registeredLayers.addPointer(pointer)

    layer.colors = colorAnimation.toValue as! [Any]?

    // HERE'S THE KEY: We compute time elapsed since the beginning of the animation, and start the animation at that time, using 'beginTime'
    let timeElapsed = CACurrentMediaTime() - animationBeginTime
    colorAnimation.beginTime = -timeElapsed

    layer.add(colorAnimation, forKey: "colors")
    colorAnimation.beginTime = 0
}

// The function called recursively for an endless animation
func animate() {

    // Destination layer
    let toLayer = newGradient() // some function to create a new color gradient
    toLayer.frame = UIScreen.main.bounds

    // Setup animation
    colorAnimation.fromValue = backgroundLayer.colors;
    colorAnimation.toValue = toLayer.colors;

    // Update background layer
    backgroundLayer.colors = toLayer.colors

    // Update registered layers (iterate is a custom function I declared as an extension of NSPointerArray)
    registeredLayers.iterate() { obj in
        guard let layer = obj as? CAGradientLayer else { return }
        layer.colors = toLayer.colors
    }

    CATransaction.begin()

    CATransaction.setCompletionBlock({
        animate()
    })

    // Add animation to background
    backgroundLayer.add(colorAnimation, forKey: "colors")

    // Store starting time
    animationBeginTime = CACurrentMediaTime();

    // Add animation to registered layers
    registeredLayers.iterate() { obj in
        guard let layer = obj as? CAGradientLayer else { return }
        layer.add(colorAnimation, forKey: "colors")
    }

    CATransaction.commit()

}