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:
- Arrow drawing and moving effects
- Circle drawing and rotation of small dots
- 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