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:
CALayer
isNSObject
Is a subclass ofUIResponder
So the layer itself does not respond to user action events but doesEvent response chainSimilar judgment methods, soCALayer
It needs to be packaged into oneUIView
Container to accomplish this function.- each
UIView
There is one in itselfCALayer
To display the content. In the latter attribute we can see that there are multiple sumsUIView
Interface properties corresponding to the variable, so we are modifyingUIView
Changed this when the interface properties of theUIView
The correspondinglayer
Properties. CALayer
Owned andUIView
Same tree hierarchy, same thingUIView
Add a subviewaddSublayer
These are similar methods.CALayer
Can be independent ofUIView
The 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. through
layer.modelLayer
Get when we modifyCALayer
The property of the model tree is immediately modified to the corresponding value - The render tree. through
layer. presentationLayer
The 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 combines
modelLayer
withpresentationLayer
To 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:
position
andanchorPoint
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
mask
andmaskToBounds
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
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:
CAAnimationGroup
Animate group objects whose role is to combine multipleCAAnimation
Animation instances are grouped together to allow layers to perform multiple animation effects simultaneously. I won’t go into more detail in this articleCAPropertyAnimation
Property animation, which is the parent of many core animation classes, is also abstractCAAnimation
Subclasses (both are abstract) it provides the important function of animating layer critical path properties, and many subclasses derived from it are important tools for animationCATransition
Over animation class, I have to say this class is very awkward in the current version. inCATransform3D
And 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 itCATransition
animation
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
defaultValueForKey
andshouldArchiveValueForKey
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 following
CAAction
Object, in this caselayer
Use this action to complete the animation - Returns a
nil
So that thelayer
They’ll look for it somewhere else - Returns a
NSNull
Object, like thislayer
The 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