Recently, a small wheel pull refresh, as long as follow a protocol can customize their own dynamic effect pull refresh and load, I also wrote several dynamic effect into, the following is a better dynamic effect implementation process

First on the effect map and github address, welcome to welcome star, complete code demo and enter the view, there are other good effects we can learn to exchange ~

Analysis of dynamic effect

The first step in writing a motion effect should be to carefully analyze it, expand each frame to find the most appropriate way to achieve it, we can decompose the above animation into the following three steps:

  1. Arrow drawing and moving effects
  2. Circle drawing and rotation of small dots
  3. Draw and animate the hooks

Here are the main classes that will be used:

  • CAShapeLayer
  • UIBezierPath
  • CABasicAnimation
  • CAKeyframeAnimation
  • DispatchSourceTimer

Arrow drawing and moving effects

We use CAShapeLayer and UIBezierPath to realize the drawing of the cutting head. The arrow is divided into two parts, one is the vertical line and the part of the arrow head, which is convenient to realize the animation effect later. The following is the main drawing code and effect diagram:

// Draw vertical lines
private func initLineLayer(a) {
    let width  = frame.size.width
    let height = frame.size.height
    let path = UIBezierPath()
    path.move(to: .init(x: width/2, y: 0))
    path.addLine(to: .init(x: width/2, y: height/2 + height/3))
    lineLayer = CAShapeLayer() lineLayer? .lineWidth = lineWidth*2lineLayer? .strokeColor = color.cgColor lineLayer? .fillColor =UIColor.clear.cgColor lineLayer? .lineCap = kCALineCapRound lineLayer? .path = path.cgPath lineLayer? .strokeStart =0.5
    addSublayer(lineLayer!)
}

// Draw the head of the arrow
private func initArrowLayer(a) {
    let width  = frame.size.width
    let height = frame.size.height
    let path = UIBezierPath()
    path.move(to: .init(x: width/2 - height/6, y: height/2 + height/6))
    path.addLine(to: .init(x: width/2, y: height/2 + height/3))
    path.addLine(to: .init(x: width/2 + height/6, y: height/2 + height/6))
    arrowLayer = CAShapeLayer() arrowLayer? .lineWidth = lineWidth*2arrowLayer? .strokeColor = color.cgColor arrowLayer? .lineCap = kCALineCapRound arrowLayer? .lineJoin = kCALineJoinRound arrowLayer? .fillColor =UIColor.clear.cgColor arrowLayer? .path = path.cgPath addSublayer(arrowLayer!) }Copy the code

Then the arrow animation is realized. We animate the line and arrow heads respectively, and control their strokeStart and strokeEnd by CABasicAnimation. The following are the renderings and main codes:

// Animation of arrows
public func startAnimation(a) -> Self {
    let start = CABasicAnimation(keyPath: "strokeStart")
    start.duration  = animationDuration
    start.fromValue = 0
    start.toValue   = 0.5
    start.isRemovedOnCompletion = false
    start.fillMode  = kCAFillModeForwards
    start.delegate    = self
    start.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

    let end = CABasicAnimation(keyPath: "strokeEnd")
    end.duration  = animationDuration
    end.fromValue = 1
    end.toValue   = 0.5
    end.isRemovedOnCompletion = false
    end.fillMode  = kCAFillModeForwards
    end.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) arrowLayer? .add(start, forKey:"strokeStart") arrowLayer? .add(end, forKey:"strokeEnd")

    return self
}

// Line animation
private func addLineAnimation(a) {
    let start = CABasicAnimation(keyPath: "strokeStart")
    start.fromValue = 0.5
    start.toValue = 0
    start.isRemovedOnCompletion = false
    start.fillMode  = kCAFillModeForwards
    start.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    start.duration  = animationDuration/2lineLayer? .add(start, forKey:"strokeStart")

    let end = CABasicAnimation(keyPath: "strokeEnd")
    end.beginTime = CACurrentMediaTime() + animationDuration/3
    end.duration  = animationDuration/2
    end.fromValue = 1
    end.toValue   = 0.03
    end.isRemovedOnCompletion = false
    end.fillMode  = kCAFillModeForwards
    end.delegate  = self
    end.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) lineLayer? .add(end, forKey:"strokeEnd")}// Control the sequence through a delegate
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if flag {
        if let anim = anim as? CABasicAnimation {
            if anim.keyPath == "strokeStart"{ arrowLayer? .isHidden =true
                addLineAnimation()
            }else{ lineLayer? .isHidden =trueanimationEnd? ()}}}}Copy the code

Circle drawing and rotation of small dots

The same circles and dots can also be drawn with a CAShapeLayer and UIBezierPath

, below is the effect picture and the main code:

// Draw the outer ring
private func drawCircle(a) {
    let width  = frame.size.width
    let height = frame.size.height
    let path = UIBezierPath()
    path.addArc(withCenter: .init(x: width/2, y: height/2), radius: height/2, startAngle: 0, endAngle: CGFloat(Double.pi * 2.0), clockwise: false)
    circle.lineWidth   = lineWidth
    circle.strokeColor = color.cgColor
    circle.fillColor   = UIColor.clear.cgColor
    circle.path        = path.cgPath
    addSublayer(circle)
    circle.isHidden = true
}

// Draw small dots
private func drawPoint(a) {
    let width  = frame.size.width
    let path = UIBezierPath()
    path.addArc(withCenter: .init(x: width/2, y: width/2), radius: width/2, startAngle: CGFloat(Double.pi * 1.5), endAngle: CGFloat((Double.pi * 1.5) - 0.1), clockwise: false)
    point.lineCap     = kCALineCapRound
    point.lineWidth   = lineWidth*2
    point.fillColor   = UIColor.clear.cgColor
    point.strokeColor = pointColor.cgColor
    point.path        = path.cgPath
    pointBack.addSublayer(point)
    point.isHidden = true
}Copy the code

Implementation of rotation, because the speed of rotation is an acceleration effect, so we use DispatchSourceTimer to control the speed of selection, the following is the effect picture and the main code:

// Rotation control
public func startAnimation(a) {
    circle.isHidden = false
    point.isHidden  = false

    codeTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) codeTimer? .scheduleRepeating(deadline: .now(), interval: .milliseconds(42)) codeTimer? .setEventHandler(handler: { [weak self] in
        guard self! =nil else {
            return
        }
        self! .rotated =self! .rotated -self! .rotatedSpeedif self! .stop {let count = Int(self! .rotated /CGFloat(Double.pi * 2))
            if (CGFloat(Double.pi * 2 * Double(count)) - self! .rotated) >=1.1 {
                var transform = CGAffineTransform.identity
                transform = transform.rotated(by: -1.1)
                DispatchQueue.main.async {
                    self! .pointBack.setAffineTransform(transform)self! .point.isHidden =true
                    self! .check? .startAnimation() }self! .codeTimer? .cancel()return}}if self! .rotatedSpeed <0.65 {
            if self! .speedInterval <0.02 {
                self! .speedInterval =self! .speedInterval +0.001
            }
            self! .rotatedSpeed =self! .rotatedSpeed +self! .speedInterval }var transform = CGAffineTransform.identity
        transform = transform.rotated(by: self! .rotated)DispatchQueue.main.async {
            self! .pointBack.setAffineTransform(transform) } }) codeTimer? .resume() addPointAnimation() }// Point change
private func addPointAnimation(a) {
    let width  = frame.size.width
    let path = CABasicAnimation(keyPath: "path")
    path.beginTime = CACurrentMediaTime() + 1
    path.fromValue = point.path
    let toPath = UIBezierPath()
    toPath.addArc(withCenter: .init(x: width/2, y: width/2), radius: width/2, startAngle: CGFloat(Double.pi * 1.5), endAngle: CGFloat((Double.pi * 1.5) - 0.3), clockwise: false)
    path.toValue = toPath.cgPath
    path.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
    path.duration = 2
    path.isRemovedOnCompletion = false
    path.fillMode = kCAFillModeForwards
    point.add(path, forKey: "path")}Copy the code

Draw and animate the hooks

We also use CAShapeLayer and UIBezierPath to draw the check box. Here are the renderings and the main code:

// Draw the checkmark
private func drawCheck(a) {
    let width = Double(frame.size.width)
    check = CAShapeLayer() check? .lineCap = kCALineCapRound check? .lineJoin = kCALineJoinRound check? .lineWidth = lineWidth check? .fillColor =UIColor.clear.cgColor check? .strokeColor = color.cgColor check? .strokeStart =0check? .strokeEnd =0
    let path = UIBezierPath(a)let a = sin(0.4) * (width/2)
    let b = cos(0.4) * (width/2)
    path.move(to: CGPoint.init(x: width/2 - b, y: width/2 - a))
    path.addLine(to: CGPoint.init(x: width/2 - width/20 , y: width/2 + width/8))
    path.addLine(to: CGPoint.init(x: width - width/5, y: width/2- a)) check? .path = path.cgPath addSublayer(check!) }Copy the code

We use CAKeyframeAnimation to control strokeStart and strokeEnd. The following is the effect picture and main code:

// Animate the hook
func startAnimation(a) {
    let start = CAKeyframeAnimation(keyPath: "strokeStart")
    start.values = [0.0.4.0.3]
    start.isRemovedOnCompletion = false
    start.fillMode = kCAFillModeForwards
    start.duration = 0.2
    start.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

    let end = CAKeyframeAnimation(keyPath: "strokeEnd")
    end.values = [0.1.0.9]

    end.isRemovedOnCompletion = false
    end.fillMode = kCAFillModeForwards
    end.duration = 0.3
    end.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) check? .add(start, forKey:"start") check? .add(end, forKey:"end")}Copy the code

conclusion

For the rotation of the ball, I chose DispatchSourceTimer instead of CADisplayLink, because CADisplayLink is affected by UITableview, and the implementation of animation requires patience to tune the details, and there are various implementations, If you have any better suggestions or suggestions you can put forward ~

The complete code, you can go to github address to download, welcome everyone star and comment and contribute code, if there is a good dynamic effect can also provide, finally thank you for reading