In iOS, normal animations can be animated using the methods provided by UIKit, but if you want to achieve complex animations, using the animations provided by the CoreAnimation framework is the best choice. Compared with the two animation schemes, the main advantages of the latter include not only the following:

  • Lightweight data structure that can animate hundreds of layers at once
  • Have a separate thread to execute our animation interface
  • After completing the animation configuration, the core animation will complete the corresponding animation frame instead of us
  • Improve application performance. Redraw content only when it changes, eliminating running code on the frame rate of the animation

Under the framework of CoreAnimation, the two most important parts are the layer CALayer and the animation CAAnimation class. The former manages a bitmap context that can be used to animate; The latter is an abstract animation base class that provides support for CAMediaTiming and CAAction protocols, making it easy for subclass instances to animate directly on CALayer itself. Next, I will describe each of these classes in sections, using information from Apple’s official documentation and objC China

CALayer

CALayer class structure

If you like animation and see CALayer and its subclasses used in open source animation implementations on the web, the first step to understanding this layer category is to look at its structure (some properties are listed here and comments removed) :

public class CALayer : NSObject, NSCoding, CAMediaTiming {

    public func presentationLayer() -> AnyObject?
    public func modelLayer() -> AnyObject

    public var bounds: CGRect
    public var position: CGPoint
    public var anchorPoint: CGPoint
    public var transform: CATransform3D
    public var frame: CGRect
    public var hidden: Bool

    public var superlayer: CALayer? { get }
    public func removeFromSuperlayer()
    public func addSublayer(layer: CALayer)
    public func insertSublayer(layer: CALayer, below sibling: CALayer?)
    public func insertSublayer(layer: CALayer, above sibling: CALayer?)
    public func replaceSublayer(layer: CALayer, with layer2: CALayer)
    public var sublayerTransform: CATransform3D

    public var mask: CALayer?
    public var masksToBounds: Bool

    public func hitTest(p: CGPoint) -> CALayer?
    public func containsPoint(p: CGPoint) -> Bool

    public var shadowColor: CGColor?
    public var shadowOpacity: Float
    public var shadowOffset: CGSize
    public var shadowRadius: CGFloat

    public var contents: AnyObject?
    public var contentsRect: CGRect

    public var cornerRadius: CGFloat
    public var borderWidth: CGFloat
    public var borderColor: CGColor?
    public var opacity: Float
}
Copy the code

According to the CALayer Class Reference, behind each UIView there is a CALayer object to help it display content, which itself manages the bitmap context we provide to the view display and holds the geometric information of these bitmap contexts. As can be seen from the above code:

  • CALayerisNSObjectIs a subclass ofUIResponderSo the layer itself does not respond to user action events but doesEvent response chainSimilar judgment methods, soCALayerIt needs to be packaged into oneUIViewContainer to accomplish this function.
  • eachUIViewThere is one in itselfCALayerTo display the content. In the latter attribute we can see that there are multiple sumsUIViewInterface properties corresponding to the variable, so we are modifyingUIViewChanged this when the interface properties of theUIViewThe correspondinglayerProperties.
  • CALayerOwned andUIViewSame tree hierarchy, same thingUIViewAdd a subviewaddSublayerThese are similar methods.CALayerCan be independent ofUIViewThe external is displayed on the screen, but we need to override the event method to complete the response to it

There’s a very detailed article on the Internet about why Apple distinguishes UIView from CALayer. Why UIView when you have CALayer

Layer trees and implicit animations

Within each CALayer, there are three important hierarchical trees that coordinate the rendering and presentation of layers. The three hierarchical trees are:

  • Model tree. throughlayer.modelLayerGet when we modifyCALayerThe property of the model tree is immediately modified to the corresponding value
  • The render tree. throughlayer. presentationLayerThe render tree holds display data for the current layer state, which is updated as the animation progresses
  • Render tree, iOS does not provide any API to retrieve this hierarchical tree. As the name suggests, it combinesmodelLayerwithpresentationLayerTo render the content onto the screen

The display data in CALayer is almost always animatable, and this feature provides a great practical foundation for making core animations. In a separate CALayer (that is, the layer is not bound to any UIView), when we change its display properties, we trigger a simple animation from the old value to the new value. This animation is called implicit animation:

class ViewController: UIViewController {

    let layer = CAShapeLayer()

    override func viewDidLoad() {
        super.viewDidLoad()
        layer.strokeEnd = 0
        layer.lineWidth = 6
        layer.fillColor = UIColor.clearColor().CGColor
        layer.strokeColor = UIColor.redColor().CGColor
        self.view.layer.addSublayer(layer)
    }

    @IBAction func actionToAnimate() {
        layer.path = UIBezierPath(arcCenter: self.view.center, radius: 100, startAngle: 0, endAngle: 2*CGFloat(M_PI), clockwise: true).CGPath
        layer.strokeEnd = 1
    }
}
Copy the code

You can see from the code above that I created a separate CALayer and added it to the layer of the current controller view. The strokeEnd property represents the fill percentage. When this property is changed, a looped animation is created:

Behind the implementation of implicit animation lies one of the most important actors, the CAAction protocol, a process discussed in detail below. So what happens to the model tree and the render tree during the above implicit animation? Since the default animation duration of the system is 0.25 seconds, I set a timer of 0.05 seconds to view the information of the two layers on each callback:

@ibAction func actionToAnimate() {timer = NSTimer(timeInterval: 0.05, target: self, selector: #selector(timerCallback), userInfo: nil, repeats: true) NSRunLoop.currentRunLoop().addTimer(timer! , forMode: NSRunLoopCommonModes) layer.path = UIBezierPath(arcCenter: self.view.center, radius: 100, startAngle: 0, endAngle: 2*CGFloat(M_PI), clockwise: true).CGPath layer.strokeEnd = 1 } @objc private func timerCallback() { print("========================\nmodelLayer: \t\(layer.modelLayer().strokeEnd)\ntpresentationLayer: \t\(layer.presentationLayer()! .strokeEnd)") if fabs((layer.presentationLayer()? .strokeEnd)! - 1) < 0.01 {if let _ = timer {timer? .invalidate() timer = nil } } }Copy the code

The console output is as follows:

= = = = = = = = = = = = = = = = = = = = = = = = modelLayer: presentationLayer 1.0:0.294064253568649 = = = = = = = = = = = = = = = = = = = = = = = = modelLayer: 1.0 presentationLayer: 0.676515340805054 ======================== modelLayer: 1.0 presentationLayer: 0.883405208587646 = = = = = = = = = = = = = = = = = = = = = = = = modelLayer: 1.0 presentationLayer: 0.974191427230835 = = = = = = = = = = = = = = = = = = = = = = = = modelLayer: presentationLayer 1.0:0.999998211860657Copy the code

You can see that when an implicit animation occurs, the modelLayer property is modified to the resulting value of the animation. The system will calculate the value of each frame in the animation according to the animation length and the final effect value, and then update the Settings to the presentationLayer. Finally, after these calculations are complete, the Render Tree renders the animation to the screen based on these values.

So what can we make with hierarchical trees? Suppose I need to create a sticky pinball animation, I add a CALayer to the far left, right, and center of the interface, a constant position drop animation to the left and right layers when the button is clicked, and a spring animation to the centerLayer in the middle. Draw the region by using timer update to obtain the Y-axis coordinates of the presentation tree of the three layers to form an animation like this:

Other attributes

In addition to the attributes highlighted above, I only give a brief introduction to the following attributes, and the detailed use and animation functions will be explained in more detail in the corresponding animation in the future:

  • positionandanchorPoint
  • AnchorPoint is a CGPoint type with x and Y values between 0 and 1, which determines the origin of coordinates based on which the layer is subjected to geometric affine transformation. The default is 0.5, 0.5, which is computed by anchorPoint and frame to obtain the layer’s position value. More articles on these two properties are available in a thorough understanding of position and anchorPoint

  • maskandmaskToBounds
  • MaskToBounds (true) indicates that all sublayers outside the scope of the layer will not be rendered. When we set UIView clipsToBounds we are actually changing the property of maskToBounds. The mask property represents a mask layer. Anything outside of this mask is not rendered. The maskView used in the last shard animation actually changes this property as well

    • CornerRadius, borderWidth and borderColor

      BorderWidth and borderColor set the color and width of the layer’s edge lines, and are not normally used at the Layer level. The latter cornerRadius sets the cornerRadius, which affects the shape of the edge lines

    • ShadowColor, shadowOpacity, shadowOffset and shadowRadius

    These four attributes combine to create a shadow effect. The default value of shadow-opacity is 0, which means that even if you set the other three properties, your shadow effect will be transparent as long as it remains unchanged. Second, don’t worry about why shadowOffset, which determines the position offset of the shadow effect, is CGSize instead of CGPoint. I set the shadow to look like this:

      layer.shadowColor = UIColor.grayColor().CGColor
      layer.shadowOffset = CGSize(width: 2, height: 5)
      layer.shadowOpacity = 1
    Copy the code

  • Other attributes
  • This includes the affine transform property of the Transform, which can set values on the Z-axis for more geometry than UIView’s namesake property. There are also bound and frame properties that affect the display range of layers

    CAAnimation

    A subclass of CAAnimation

    CAAnimation is an encapsulated base class whose most important purpose is to follow two important animation-related protocols, so resolving the animation type starts with its subclass dependencies. In Apple documentation, the direct subclasses of CAAnimation include these:

    From the diagram, we can see that there are three subclasses:

    • CAAnimationGroupAnimate group objects whose role is to combine multipleCAAnimationAnimation instances are grouped together to allow layers to perform multiple animation effects simultaneously. I won’t go into more detail in this article
    • CAPropertyAnimationProperty animation, which is the parent of many core animation classes, is also abstractCAAnimationSubclasses (both are abstract) it provides the important function of animating layer critical path properties, and many subclasses derived from it are important tools for animation
    • CATransitionOver animation class, I have to say this class is very awkward in the current version. inCATransform3DAnd custom transition apis are all the rage these days, but it’s too little. On the other hand, it could be becausePrivate API“, but it is also possible to learn about this class in this articleCATransition usageCan you learn how to use itCATransitionanimation

    Class structure attribute

    As you can see from the figure above, CAAnimation follows two protocols and does not have many properties in its own properties. Most of these animation-related properties are declared in the protocol, and setters and getters are dynamically generated in the implementation

    public class CAAnimation : NSObject, NSCoding, NSCopying, CAMediaTiming, CAAction {
    
        public class func defaultValueForKey(key: String) -> AnyObject?
        public func shouldArchiveValueForKey(key: String) -> Bool
    
        public var timingFunction: CAMediaTimingFunction?
    
        public var delegate: AnyObject?
    
        public var removedOnCompletion: Bool
    }
    Copy the code

    The class structure of CAAnimation can be divided into two parts: attribute and method, among which attribute is the one we need to focus on

    • defaultValueForKeyandshouldArchiveValueForKey

    These two methods are named after the NSCoding protocol, which serializes an animated object for local storage by passing in a keyword and returns success or failure. The former is then called using the same keyword to retrieve the persistent animated object

  • timingFunction
  • This is an interesting property that determines the visual effect of the animation. I mentioned the visual effects of animations when I started with UIView animations, including fades, fade-out, and so on, which are represented by strings:

      public let kCAMediaTimingFunctionLinear: String
      public let kCAMediaTimingFunctionEaseIn: String
      public let kCAMediaTimingFunctionEaseOut: String
      public let kCAMediaTimingFunctionEaseInEaseOut: String
      public let kCAMediaTimingFunctionDefault: String
    
      let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    Copy the code

  • delegate
  • NSObject implements CAAnimation’s callback methods, including but not limited to animationDidStart and animationDidStop. So any object in iOS can be the agent of CAAnimation. With this agent we can remove animation effects at the end of the animation and so on

  • removedOnCompletion
  • Determines whether to remove the animation from the corresponding layer after the animation is finished. Default is true. Since layer animation is essentially a smoke screen (CAAnimaiton doesn’t actually change the properties of the animation), at the end of the animation the layer returns to where it started. By setting this value to false and other configurations, you can prevent this from happening

    Animation Protocol Properties

    In addition to the properties of CAAnimation itself, the other two protocols declare key properties that determine the animation duration and animation effects before and after:

    public protocol CAMediaTiming {
    
        public var beginTime: CFTimeInterval { get set }
    
        public var duration: CFTimeInterval { get set }
    
        public var speed: Float { get set }
    
        public var timeOffset: CFTimeInterval { get set }
    
        public var repeatCount: Float { get set }
    
        public var repeatDuration: CFTimeInterval { get set }
    
        public var autoreverses: Bool { get set }
    
        public var fillMode: String { get set }
    }
    Copy the code

    CAMediaTiming is a control animation time protocol, provides the animation process of time related attributes, for these attributes in the control of animation time a very clear explanation, the author here is no longer introduced. In addition, there is another protocol, the CAAction protocol:

    public protocol CAAction { /* Called to trigger the event named 'path' on the receiver. The object * (e.g. the layer) on  which the event happened is 'anObject'. The * arguments dictionary may be nil, If non-nil it carries parameters * associated with the event. */ @available(iOS 2.0, *) public func runActionForKey(event: String, object anObject: AnyObject, arguments dict: [NSObject : AnyObject]?) }Copy the code

    This method is called after the animation has taken place and tells the layer what to do with it, such as render.

    CAAction

    Explicit animation

    The author mentioned at the beginning that implicit animation is an automatic transition animation generated when the animatable properties of a single layer change, so there must be corresponding explicit animation. The process of making an explicit animation is to create an animation object and then add it to the layer that implements the animation. This code creates a base animation object that changes layer.position.y and adds it to the layer with an end value of 160. The default animation length is 0.25 seconds:

    let animation = CABasicAnimation(keyPath: "position.y")
    animation.toValue = NSNumber(float: 160)
    layer.position.y = 160
    layer.addAnimation(animation, forKey: nil)
    Copy the code

    A more detailed explanation of animation is not planned for this article. In the following core animation, I will introduce various CAAnimation subclasses in more detail for different animation scenes. Here’s the core code for the sticky popover above:

    func startAnimation() {
        let displayLink = CADisplayLink(target: self, selector: #selector(fluctAnimation(_:)))
        displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
        let move = CABasicAnimation(keyPath: "position.y")
        move.toValue = NSNumber(float: 160)
        leftLayer.position.y = 160
        rightLayer.position.y = 160
        leftLayer.addAnimation(move, forKey: nil)
        rightLayer.addAnimation(move, forKey: nil)
    
        let spring = CASpringAnimation(keyPath: "position.y")
        spring.damping = 15
        spring.initialVelocity = 40
        spring.toValue = NSNumber(float: 160)
        centerLayer.position.y = 160
        centerLayer.addAnimation(spring, forKey: "spring")
    }
    Copy the code

    After animating the three layers down, create a CADisplayLink timer to synchronize the screen refresh rate and update the pop-up effect:

    @objc private func fluctAnimation(link: CADisplayLink) { let path = UIBezierPath() path.moveToPoint(CGPointZero) guard let _ = centerLayer.animationForKey("spring") else { return } let offset = leftLayer.presentationLayer()! .position.y - centerLayer.presentationLayer()! .position.y var controlY: CGFloat = 160 if offset < 0 { controlY = centerLayer.presentationLayer()! .position.y + 30 } else if offset > 0 { controlY = centerLayer.presentationLayer()! .position.y - 30 } path.addLineToPoint(leftLayer.presentationLayer()! .position) path.addQuadCurveToPoint(rightLayer.presentationLayer()! .position, controlPoint: CGPoint(x: centerLayer.position.x, y: controlY)) path.addLineToPoint(CGPoint(x: UIScreen.mainScreen().bounds.width, y: 0)) path.closePath() fluctLayer.path = path.CGPath }Copy the code

    What happened to implicit animation

    As mentioned above, implicit animation occurs when the animatable properties of individual CALayer objects are changed. If our layer already has a UIView object bound to it, then when we directly modify the properties of this layer, the display effect will be changed from the old value to the new value in an instant, with no additional effect. This is explained in the CoreAnimation programming guide: UIView disables Layer animations by default, but re-enables them in the Animate block. This is the behavior we see, but if we dig deep into the internal implementation of this mechanism, we’ll be surprised at how well the view and layer work together, which is CAAction

    When the properties of any animatable layer change, layer queries a CAAction object by sending actionForLayer(Layer: Event 🙂 to its agent. This method returns three results:

    • Return a followingCAActionObject, in this caselayerUse this action to complete the animation
    • Returns anilSo that thelayerThey’ll look for it somewhere else
    • Returns aNSNullObject, like thislayerThe search is stopped and the animation is not executed

    Normally, when a CALayer is associated with UIView, that UIView object becomes a layer proxy. Therefore, when the layer properties are modified, the associated UIView object usually returns NSNull directly, and only returns the actual animation in the Animate block state, allowing the layer to continue to look for action options. We verify this with code:

    print("===========normal call===========") print("\(self.view.actionForLayer(self.view.layer, forKey: "Opacity") ") UIView. AnimateWithDuration (0.25) {print (" = = = = = = = = = = = the animate block call = = = = = = = = = = = ") print("\(self.view.actionForLayer(self.view.layer, forKey: "opacity"))") }Copy the code

    The console output is as follows, and indeed a CABasicAnimation object is returned in the animation block to help complete the animation

    ===========normal call===========
    Optional()
    ===========animate block call===========
    Optional(; fillMode = both; timingFunction = easeInEaseOut; duration = 0.25; fromValue = 1; keyPath = opacity>)
    Copy the code

    Normally a UIView in an animation block will return a core animation object like this, but if it returns nil, the layer continues to look for other action solutions, four times, as explained in CALayer’s header:

    When the Layer object finds the animation for the property modification action, it calls the addAnimation(_:forKey:) method to start executing the animation. Again, we inherit the CALayer object to override this method:

    class LXDActionLayer: CALayer { override func addAnimation(anim: CAAnimation, forKey key: String?) { print("***********************************************") print("Layer will add an animation: \(anim)") super.addAnimation(anim, forKey: key) } } class LXDActionView: UIView { override class func layerClass() -> AnyClass { return LXDActionLayer.classForCoder() } } override func viewDidLoad() { super.viewDidLoad() let actionView = LXDActionView() view.addSubview(actionView) print("===========normal call===========") print("\(self.view.actionForLayer(self.view.layer, forKey: "Opacity") ") actionView. Layer. The opacity = 0.5 UIView. AnimateWithDuration (0.25) {print (" = = = = = = = = = = = the animate block call===========") print("\(self.view.actionForLayer(self.view.layer, forKey: "opacity"))") actionView.layer.opacity = 0 }Copy the code

    The console output is as follows:

    ===========normal call===========
    Optional()
    ===========animate block call===========
    Optional(; fillMode = both; timingFunction = easeInEaseOut; duration = 0.25; fromValue = 1; keyPath = opacity>)
    ***********************************************
    Layer will add an animation: 
    Copy the code

    Some people may wonder why the two output addresses of CABasicAnimation are different. To ensure that the same animation object can be executed on multiple CALayer objects, the CAAnimation object is copied once when the addAnimation(_:forKey:) method is called. You can inherit the CABasicAnimation object to override the copy method to test yourself

    Stern said

    Originally, the author wanted to use animation particles to explain the framework of core animation directly, but considering that if the core animation can be comprehensively explained once, it will be of great help to explain the future article and the production of animation particles.

    In this paper, the demo

    Please indicate the author and address of this article