• This series is about the use of animation in iOS development, so you should be familiar with iOS development.
  • The code is based on SWIFT, so you have to know SWIFT.
  • This article builds on the previous one, so know the basic uses of Layer Animations!

Basic uses of Layer Animations were introduced in the iOS Animation Guide – 2. Basic Uses of Layer Animations. In this article we will take a closer look at Layer Animations with a few small examples Animations. This article will be a bit longer and more consistent than the previous one, so get ready to go!

The article Outlines

  1. CAKeyframeAnimation that can be changed between multiple values.
  2. CAShapeLayer can be drawn in various shapes.




    DOG VS FOX

  3. CAGradientLayer that adds effects to text.




    Sliding unlock effect

  4. Pull down refresh with track.




    Simulate a pull-down refresh

  5. CAReplicatorLayer that can be copied indefinitely.




    CAReplicatorLayer

1. CAKeyframeAnimation

The fromValue and toValue attributes from one value to another are not efficient enough to meet development needs. For example, we need to pass a view through three points at a time. Do you do it twice? It’s too much trouble. Yeah, we can do that with CAKeyframeAnimation,CAKeyframeAnimation has a property values is an array and it’s a nice alternative to fromValue,toValue, we can put three points in values array and solve the problem.

let flight = CAKeyframeAnimation(keyPath: Flight.duration = 2.0 // infinite repetitionFlight.repeatCount = MAXFLOAT // Note: CGPoint cannot be assigned directly to values that need to be converted, elements in array can be structed as // .map {NSValue(CGPoint: $0)} flight.values = [CGPoint(x: 50.0, y: 100.0), CGPoint(x: 100.0), View.frame.width-50, y: 160), CGPoint(x: 50.0, y: view.center.y), CGPoint(x: 50.0, y: 100.0)].map {NSValue(CGPoint: $0)} // flight.values = [// NSValue(CGPoint: CGPoint(x: 50.0, y: 100.0)), // NSValue(CGPoint: CGPoint(x: 50.0, y: 100.0))), // NSValue(CGPoint: CGPoint(x: 100.0)) View.frame.width-50, y: 160)), // NSValue(CGPoint: CGPoint(x: 50.0, y: view.center.y)), // NSValue(CGPoint: CGPoint(x: 50.0, y: view.center.y)), // NSValue(CGPoint: CGPoint(x: 50.0 y: 100.0)), / /] flight. KeyTimes = [0.0, 0.33, 0.66, 1.0] dogImageView. Layer. AddAnimation (flight, forKey: nil)Copy the code



Or we can do the view left and right slosh, not add to the above displacement, separate implementation:

let wobble = CAKeyframeAnimation(keyPath: Wobble. Duration = 2.5 wobble. RepeatCount = MAXFLOAT // Wobble. Values = [0.0, -m_pi_4 /4, 0.0, M_PI_4/4, 0.0] // set the wobble between values in percentage [0,1]. 1.0] dogImageView. Layer. AddAnimation (wobble, forKey: nil)Copy the code



2. CAShapeLayer

Use the Case hapelayer to draw a variety of graphics. For example, to draw circles:

let circleLayer = CAShapeLayer() let maskLayer = CAShapeLayer() circleLayer.path = UIBezierPath(ovalInRect: dogImageView.bounds).CGPath circleLayer.fillColor = UIColor.clearColor().CGColor maskLayer.path = circleLayer.path // Some of the cut out beyond maskLayer dogImageView. Layer. Mask = maskLayer dogImageView. Layer. AddSublayer (circleLayer)Copy the code



Let’s take a look:




DOG VS FOX

Since Git images are looping, it’s hard to tell where the animation starts and ends. The animation actually starts like this:




This is the AvatarView hierarchy:




  • PhotoLayer: Is used to place images.
  • CircleLayer: Is used to draw circles.
  • MaskLayer: Is used to crop the image.
  • Label: Sets the name.

Let’s analyze the steps:

  1. Set rounded corners for both images
  2. Move the two images towards the middle and turn the image into a square corner when finished
  3. When the two images are in the middle, create an elliptical collision effect between the two images
  4. Back up, return the image to the starting position, and perform Step 1

1. In the didMoveToWindow method of AvatarView, add the new properties to it

 override func didMoveToWindow() {
        layer.addSublayer(photoLayer)
        photoLayer.mask = maskLayer
        layer.addSublayer(circleLayer)
        addSubview(label)

    }Copy the code

2. Rewrite the layoutSubviews method to set the rounded corners of the image

override func layoutSubviews() { photoLayer.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height) circleLayer.path = UIBezierPath(ovalInRect: bounds).CGPath circleLayer.strokeColor = UIColor.whiteColor().CGColor circleLayer.lineWidth = lineWidth circleLayer.fillColor = UIColor.clearColor().CGColor maskLayer.path = circleLayer.path maskLayer.position = CGPoint(x: 0.0, y: 0.0) label.frame = CGRect(x: 0.0, y: bound.size. Height + 10.0, width: bound.size. Width, height: 24.0)}Copy the code

Func boundsOffset: boundsOffset: morphSize Specifies the size to be set when the image collides.

 func boundsOffset(boundsOffset:CGFloat, morphSize: CGSize) {
}Copy the code

4. Set the image to the middle in the boundsOffset method:

/ / forward UIView. AnimateWithDuration (usingSpringWithDamping: animationDuration, delay: 0.0, 0.8, initialSpringVelocity: 0.0, options: [], animations: {self.frame.origin. X = boundsOffset}, completion: {_ in // Change rounded image to square image self.animateToSquare()})Copy the code

5. There is a collision effect when the image is in the middle:

Let morphedFrame = (originalCenter.x > boundsOffset)? CGRect(x: 0.0, y: bounds. Height-morphsize. Height, width: morphSize. bounds.width - morphSize.width, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height) let morphAnimation = CABasicAnimation(keyPath: "path") morphAnimation.duration = animationDuration morphAnimation.toValue = UIBezierPath(ovalInRect: morphedFrame).CGPath morphAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) circleLayer.addAnimation(morphAnimation, forKey:nil) maskLayer.addAnimation(morphAnimation, forKey: nil)Copy the code

6. Return to initial position:

/ / back UIView. AnimateWithDuration (animationDuration, delay: animationDuration, usingSpringWithDamping: InitialSpringVelocity: 1.0, Options: [], animations: {self.center = originalCenter}, completion: {_ in delay(seconds: 0.1) {if! self.isSquare { self.boundsOffset(boundsOffset, morphSize: morphSize) } } })Copy the code

7. Turn rounded corners into square ones. This is not technically the last step, the fourth step has a self.animateToSquare()

func animateToSquare() { isSquare = true let squarePath = UIBezierPath(rect: bounds).CGPath let morph = CABasicAnimation(keyPath: "Path ") morph.duration = 0.25 morph.fromValue = circlelayer. path morph.toValue = squarePath circleLayer.addAnimation(morph, forKey: nil) maskLayer.addAnimation(morph, forKey: nil) circleLayer.path = squarePath maskLayer.path = squarePath }Copy the code

So that’s the general procedure, and now we can use it in ViewController.swift

  1. Create two AvatarViews and set the image, size and position.
  2. Call AvatarView’s boundsOffset method to set the offset position and the size required for the image to collide.
AvatarSize = avatar1.frame. Size let morphSize = CGSize(width: avatarSize. Width * 0.85, height: AvatarSize. Height * 1.05) let bounceXOffset: CGFloat = view. Frame. The size, width / 2.0 - avatar1. Our lineWidth * 2 - avatar1. Frame. The width avatar2. BoundsOffset (bounceXOffset, morphSize:morphSize) avatar1.boundsOffset(avatar1.frame.origin.x - bounceXOffset, morphSize:morphSize)Copy the code

3. CAGradientLayer

What about the text effect we see on iphones sliding to unlock almost every day? Step by step >.

lazy var gradientLayer: GradientLayer = {let gradientLayer = CAGradientLayer() gradientLayer.startPoint = CGPoint(x: 0.0, y: Gradientlayer. endPoint = gradientPoint (x: 1.0, y: 1.0) Let colors = [uicolor.blackcolor ().cgcolor, uicolor.whitecolor ().cgcolor, UIColor. BlackColor (). CGColor] gradientLayer. Colors = colors/color/position let locations = [0.25, 0.5, 0.75] gradientLayer. Locations = locations Return gradientLayer}()Copy the code

Create a new view, set the location dimensions arbitrarily, and add the gradientLayer to the view

        let gradientView = UIView(frame: CGRect(x: 0, y: self.view.frame.height/2, width: self.view.frame.width, height: 80))
        gradientView.backgroundColor = UIColor.lightGrayColor()
        gradientLayer.frame = gradientView.bounds
        gradientView.layer.addSublayer(gradientLayer)
        view.addSubview(gradientView)Copy the code

Then we can see something like this:




How is it? It’s a little bit like that! CABasicAnimation has a locations property that animates gradientLayer by changing color positions.

let gradientAnimation = CABasicAnimation(keyPath: "Locations") gradientAnimation. FromValue = [0.0, 0.0, 0.25] gradientAnimation. ToValue = [0.75, 1.0, 1.0] gradientAnimation. Duration = 3.0 gradientAnimation. RepeatCount = Float. Infinity gradientLayer.addAnimation(gradientAnimation, forKey: nil)Copy the code



Git drops frames so badly! 3. Modify the size of the white area. For the sake of reusability, we wrapped the code with a custom view:GradientLabel, which has a text property that draws a string onto the view from a graphical context.

Lazy var textAttributes: [String: AnyObject] = { let style = NSMutableParagraphStyle() style.alignment = .Center return [ NSFontAttributeName:UIFont(name: "HelveticaNeue - Thin", size: 28.0)! NSParagraphStyleAttributeName: style]} ()Copy the code
    @IBInspectable var text: String! {
        didSet {

            setNeedsDisplay()
            UIGraphicsBeginImageContextWithOptions(frame.size, false, 0)
            text.drawInRect(bounds, withAttributes: textAttributes)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()

            let maskLayer = CALayer()
            maskLayer.backgroundColor = UIColor.clearColor().CGColor
            maskLayer.frame = CGRectOffset(bounds, bounds.size.width, 0)
            maskLayer.contents = image.CGImage

            gradientLayer.mask = maskLayer
        }
    }Copy the code

And then you can use it outside

override func viewDidLoad() { super.viewDidLoad() let label = GradientLabel() label.center = view.center label.bounds = CGRect(x: 0, y: 0, width: 239, height: AddSubview (label) view.backgroundColor = uicolor.darkgrayColor ()}Copy the code



Sliding unlock effect

4. Pull down refresh with track.

Pull-down refresh is used in almost every APP. There are third-party frameworks available, so there are not many cases where you have to implement it yourself. But sometimes you need to customize it, so understand the principle! The principle is very simple: it’s basically a proxy method that listens to the tableView scroll, and when it’s refreshed, it goes back to its original state. This time we’re gonna do one with special effects.

let ovalShapeLayer: CAShapeLayer = CAShapeLayer() let airplaneLayer: CALayer = CALayer () / / white circle ovalShapeLayer strokeColor = UIColor. WhiteColor (). The CGColor ovalShapeLayer. FillColor = UIColor. ClearColor (.) CGColor ovalShapeLayer. Our lineWidth = 4.0 ovalShapeLayer. LineDashPattern = [2, Height /2 * 0.8 ovalShapelayer. path = UIBezierPath(ovalInRect: CGRect(x: UIBezierPath)) frame.size.width/2 - refreshRadius, y:frame.size.height/2 - refreshRadius , width: 2*refreshRadius, height: 2*refreshRadius)).CGPath layer.addSublayer(ovalShapeLayer)Copy the code

Then add the airplane image at the start position

Let airplaneImage = UIImage(named: "airplane") airplanelayer.contents = airplaneImage? .CGImage airplaneLayer. Bounds = CGRect(x: 0.0, y: 0.0, width: (airplaneImage? .size.width)! , height: airplaneImage! .size. Height) airplanelayer. position = CGPoint(x: frame.size. Width /2 + frame.size. Height /2 * 0.8, y: frame.size.height/2) layer.addSublayer(airplaneLayer)Copy the code



2. Set to start the refresh and end the refresh animation

/ / refresh func beginRefreshing () {isRefreshing = true UIView. AnimateWithDuration (0.3, animations: { var newInsets = self.scrollView! .contentInset newInsets.top += self.frame.size.height self.scrollView! .contentInset = newInsets }) let strokeStartAnimation = CABasicAnimation(keyPath: "StrokeStart") strokeStartAnimation. FromValue = 0.5 strokeStartAnimation. ToValue = 1.0 let strokeEndAnimation = CABasicAnimation(keyPath: "StrokeEnd") strokeEndAnimation. FromValue = 0.0 strokeEndAnimation. ToValue = 1.0 let strokeAnimationGroup = CAAnimationGroup () strokeAnimationGroup. Duration = 1.5 strokeAnimationGroup. RepeatDuration = 5.0 strokeAnimationGroup.animations = [strokeStartAnimation, strokeEndAnimation] ovalShapeLayer.addAnimation(strokeAnimationGroup, forKey: nil) let flightAnimation = CAKeyframeAnimation(keyPath: "position") flightAnimation.path = ovalShapeLayer.path flightAnimation.calculationMode = kCAAnimationPaced let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation") airplaneOrientationAnimation.fromValue = 0 airplaneOrientationAnimation.toValue = 2 * M_PI let FlightAnimationGroup = CAAnimationGroup () flightAnimationGroup. Duration = 1.5 flightAnimationGroup. RepeatDuration = 5.0 flightAnimationGroup.animations = [flightAnimation, airplaneOrientationAnimation] airplaneLayer.addAnimation(flightAnimationGroup, forKey: Nil)} / / end refresh func endRefreshing () {isRefreshing = false UIView. AnimateWithDuration (0.3, delay: 0.0, the options: .CurveEaseOut ,animations: { var newInsets = self.scrollView! .contentInset newInsets.top -= self.frame.size.height self.scrollView! .contentInset = newInsets }, completion: {_ in }) }Copy the code

3. Set the start and end of the animation based on the offset in the scrollView method of the tabelView

func scrollViewDidScroll(scrollView: UIScrollView) { let offsetY = CGFloat( max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0)) self. Progress = min(Max (offsetY/frame.size. Height, 0.0), 1.0) if! refreshing { redrawFromProgress(progress) } } func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { if ! Refreshing && self. Progress >= 1.0 {delegate? .refreshViewDidRefresh(self) beginRefreshing() } }Copy the code



Simulate a pull-down refresh

5. CAReplicatorLayer that can be copied indefinitely

CAReplicatorLayer, a subclass of CALayer, uses it to copy objects it creates to create complex effects. Create a CAReplicatorLayer and copy it.

Let replicator = CAReplicatorLayer() let dot = CALayer() let dotLength: CGFloat = 6.0 let dotOffset: CGFloat = 8.0 replicator.frame = view.bounds view.layer.addSublayer(replicator) dot.frame = CGRect(x: replicator.frame.size.width - dotLength, y: replicator.position.y, width: dotLength, height: BackgroundColor = uicolor.lightgraycolor ().cgcolor dot. BorderColor = UIColor(White: 1.0, alpha: BorderWidth = 0.5 dot. CornerRadius = 1.5 replicator.addSublayer(dot) // Make replicator.instancecount = Int (view. Frame. The size, width/dotOffset) replicator. InstanceTransform = CATransform3DMakeTranslation (- dotOffset, 0.0, 0.0)Copy the code



2. Make it work and make each dot do a little delay.

let move = CABasicAnimation(keyPath: Y move. ToValue = dot.position.y move = 10 dot.addanimation (move, forKey: nil) // Delay 0.02 second replicator.instancedelay = 0.02Copy the code



3. Comment out the code in 2 to make this effect

Replicator. InstanceDelay = 0.02 let scale = CABasicAnimation(keyPath: "transform") scale.fromValue = NSValue(CATransform3D: CATransform3DIdentity) scale.toValue = NSValue(CATransform3D: CATransform3DMakeScale (1.4, 15, Duration = 0.33 scale.repeatcount = Float. Infinity scale. Autoreverses = true scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) dot.addAnimation(scale, forKey: "dotScale")Copy the code



4. Add a gradient

let fade = CABasicAnimation(keyPath: Duration = 0.33 fade. BeginTime = CACurrentMediaTime() + 0.33 fade.repeatCount = Float.infinity fade.autoreverses = true fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) dot.addAnimation(fade, forKey: "dotOpacity")Copy the code



5. Add gradient colors

let tint = CABasicAnimation(keyPath: "backgroundColor") tint.fromValue = UIColor.magentaColor().CGColor tint.toValue = UIColor.cyanColor().CGColor Duration = 0.66tint.beginTime = CACurrentMediaTime() + 0.28tint.fillmode = kcafillmodeenrepeatcount = Float.infinity tint.autoreverses = true tint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) dot.addAnimation(tint, forKey: "dotColor")Copy the code



let initialRotation = CABasicAnimation(keyPath: "InstanceTransform. Rotation") initialRotation. FromValue. = 0.0 initialRotation toValue = 0.01 initialRotation. Duration = 0.33 initialRotation. RemovedOnCompletion = false initialRotation. FillMode = kCAFillModeForwards initialRotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) replicator.addAnimation(initialRotation, forKey: "initialRotation") let rotation = CABasicAnimation(keyPath: "InstanceTransform. Rotation") rotation. FromValue = 0.01 rotation. The rotation toValue = 0.01. Duration = 0.99 Start time = CACurrentMediaTime() + 0.33 repeatCount = Float. Infinity rotation. Autoreverses = true rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) replicator.addAnimation(rotation, forKey: "replicatorRotation")Copy the code



This paper sort the: iOS. Animations. By. Tutorials. V2.0 source: github.com/DarielChen/… If you have any questions, please leave a message at -d