Skip to content

Commit a21ff0c

Browse files
committed
Added Spiner animation types feature
1 parent b9ee2b8 commit a21ff0c

11 files changed

Lines changed: 593 additions & 23 deletions

Source/SpinerAnimationType.swift

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//
2+
// SpinerAnimationType.swift
3+
// TransitionButton
4+
//
5+
// Created by Rahul Mayani on 11/09/20.
6+
// Copyright © 2020 ITechnoDev. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
// swiftlint:disable:next class_delegate_protocol
13+
protocol TransitionButtonAnimationDelegate {
14+
15+
func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?)
16+
}
17+
18+
/**
19+
Enum of animation types used for spiner animation.
20+
21+
- DefaultSpinner: LineSpinFadeLoader animation.
22+
- BallRotate: BallRotate animation.
23+
- BallPulse: BallPulse animation.
24+
- AudioEqualizer: AudioEqualizer animation.
25+
- BallClipRotate: BallClipRotate animation.
26+
- BallScale: BallScale animation.
27+
*/
28+
public enum SpinerAnimationType: Int, CaseIterable {
29+
30+
/**
31+
DefaultSpinner.
32+
33+
- returns: Instance of DefaultSpinner.
34+
*/
35+
case defaultSpinner = 0
36+
/**
37+
BallRotate.
38+
39+
- returns: Instance of SpinerBallRotate.
40+
*/
41+
case ballRotate = 1
42+
/**
43+
BallPulse.
44+
45+
- returns: Instance of SpinerBallPulse.
46+
*/
47+
case ballPulse = 2
48+
/**
49+
AudioEqualizer.
50+
51+
- returns: Instance of SpinerAudioEqualizer.
52+
*/
53+
case audioEqualizer = 3
54+
/**
55+
BallClipRotate.
56+
57+
- returns: Instance of SpinerBallClipRotate.
58+
*/
59+
case ballClipRotate = 4
60+
/**
61+
BallScale.
62+
63+
- returns: Instance of SpinerBallScale.
64+
*/
65+
case ballScale = 5
66+
67+
68+
// swiftlint:disable:next cyclomatic_complexity function_body_length
69+
func animation() -> TransitionButtonAnimationDelegate {
70+
switch self {
71+
case .defaultSpinner:
72+
return DefaultSpinner()
73+
case .ballRotate:
74+
return SpinerBallRotate()
75+
case .ballPulse:
76+
return SpinerBallPulse()
77+
case .audioEqualizer:
78+
return SpinerAudioEqualizer()
79+
case .ballClipRotate:
80+
return SpinerBallClipRotate()
81+
case .ballScale:
82+
return SpinerBallScale()
83+
}
84+
}
85+
}
86+
87+
enum TransitionButtonAnimationShape {
88+
case circle
89+
case ringTwoHalfVertical
90+
case ringTwoHalfHorizontal
91+
case line
92+
93+
// swiftlint:disable:next cyclomatic_complexity function_body_length
94+
func layerWith(size: CGSize, color: UIColor) -> CALayer {
95+
let layer: CAShapeLayer = CAShapeLayer()
96+
var path: UIBezierPath = UIBezierPath()
97+
let lineWidth: CGFloat = 2
98+
99+
switch self {
100+
case .circle:
101+
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
102+
radius: size.width / 2,
103+
startAngle: 0,
104+
endAngle: CGFloat(2 * Double.pi),
105+
clockwise: false)
106+
layer.fillColor = color.cgColor
107+
case .ringTwoHalfVertical:
108+
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
109+
radius: size.width / 2,
110+
startAngle: CGFloat(-3 * Double.pi / 4),
111+
endAngle: CGFloat(-Double.pi / 4),
112+
clockwise: true)
113+
path.move(
114+
to: CGPoint(x: size.width / 2 - size.width / 2 * cos(CGFloat(Double.pi / 4)),
115+
y: size.height / 2 + size.height / 2 * sin(CGFloat(Double.pi / 4)))
116+
)
117+
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
118+
radius: size.width / 2,
119+
startAngle: CGFloat(-5 * Double.pi / 4),
120+
endAngle: CGFloat(-7 * Double.pi / 4),
121+
clockwise: false)
122+
layer.fillColor = nil
123+
layer.strokeColor = color.cgColor
124+
layer.lineWidth = lineWidth
125+
case .ringTwoHalfHorizontal:
126+
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
127+
radius: size.width / 2,
128+
startAngle: CGFloat(3 * Double.pi / 4),
129+
endAngle: CGFloat(5 * Double.pi / 4),
130+
clockwise: true)
131+
path.move(
132+
to: CGPoint(x: size.width / 2 + size.width / 2 * cos(CGFloat(Double.pi / 4)),
133+
y: size.height / 2 - size.height / 2 * sin(CGFloat(Double.pi / 4)))
134+
)
135+
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
136+
radius: size.width / 2,
137+
startAngle: CGFloat(-Double.pi / 4),
138+
endAngle: CGFloat(Double.pi / 4),
139+
clockwise: true)
140+
layer.fillColor = nil
141+
layer.strokeColor = color.cgColor
142+
layer.lineWidth = lineWidth
143+
case .line:
144+
path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height),
145+
cornerRadius: size.width / 2)
146+
layer.fillColor = color.cgColor
147+
}
148+
149+
layer.backgroundColor = nil
150+
layer.path = path.cgPath
151+
layer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
152+
153+
return layer
154+
}
155+
}

Source/SpinerLayer.swift

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import UIKit
1111

1212
class SpinerLayer: CAShapeLayer {
1313

14+
/// Animation type.
15+
public var type: SpinerAnimationType = .defaultSpinner
16+
1417
var spinnerColor = UIColor.white {
1518
didSet {
1619
strokeColor = spinnerColor.cgColor
@@ -38,34 +41,29 @@ class SpinerLayer: CAShapeLayer {
3841
super.init(layer: layer)
3942

4043
}
41-
44+
4245
func animation() {
4346
self.isHidden = false
44-
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
45-
rotate.fromValue = 0
46-
rotate.toValue = Double.pi * 2
47-
rotate.duration = 0.4
48-
rotate.timingFunction = CAMediaTimingFunction(name: .linear)
49-
50-
rotate.repeatCount = HUGE
51-
rotate.fillMode = .forwards
52-
rotate.isRemovedOnCompletion = false
53-
self.add(rotate, forKey: rotate.keyPath)
54-
47+
let animation: TransitionButtonAnimationDelegate = type.animation()
48+
animation.setupSpinnerAnimation(in: self, frame: frame, color: spinnerColor, spinnerSize: nil)
5549
}
5650

5751
func setToFrame(_ frame: CGRect) {
58-
let radius:CGFloat = (frame.height / 2) * 0.5
5952
self.frame = CGRect(x: 0, y: 0, width: frame.height, height: frame.height)
60-
let center = CGPoint(x: frame.height / 2, y: bounds.center.y)
61-
let startAngle = 0 - Double.pi/2
62-
let endAngle = Double.pi * 2 - Double.pi/2
63-
let clockwise: Bool = true
64-
self.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: clockwise).cgPath
6553
}
6654

6755
func stopAnimation() {
6856
self.isHidden = true
6957
self.removeAllAnimations()
58+
removeAnimationLayer()
59+
}
60+
61+
private func removeAnimationLayer() {
62+
if self.sublayers != nil {
63+
for item in self.sublayers! {
64+
item.removeAllAnimations()
65+
item.removeFromSuperlayer()
66+
}
67+
}
7068
}
7169
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// DefaultSpinner.swift
3+
// TransitionButton
4+
//
5+
// Created by Rahul Mayani on 11/09/20.
6+
// Copyright © 2020 ITechnoDev. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
class DefaultSpinner: TransitionButtonAnimationDelegate {
13+
14+
/// setup spinner layer
15+
///
16+
/// - Parameters:
17+
/// - layer: layer Parent layer (Button layer)
18+
/// - frame: frame of parant layer
19+
/// - color: color of spinner
20+
/// - spinnerSize: size of spinner layer
21+
func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?) {
22+
23+
self.setToFrame(frame, layer: layer)
24+
25+
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
26+
rotate.fromValue = 0
27+
rotate.toValue = Double.pi * 2
28+
rotate.duration = 0.4
29+
rotate.timingFunction = CAMediaTimingFunction(name: .linear)
30+
31+
rotate.repeatCount = HUGE
32+
rotate.fillMode = .forwards
33+
rotate.isRemovedOnCompletion = false
34+
layer.add(rotate, forKey: rotate.keyPath)
35+
}
36+
37+
func setToFrame(_ frame: CGRect, layer: CAShapeLayer) {
38+
let radius:CGFloat = (frame.height / 2) * 0.5
39+
layer.frame = CGRect(x: 0, y: 0, width: frame.height, height: frame.height)
40+
let center = CGPoint(x: frame.height / 2, y: layer.bounds.center.y)
41+
let startAngle = 0 - Double.pi/2
42+
let endAngle = Double.pi * 2 - Double.pi/2
43+
let clockwise: Bool = true
44+
layer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: clockwise).cgPath
45+
}
46+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// SpinerAudioEqualizer.swift
3+
// TransitionButton
4+
//
5+
// Created by Rahul Mayani on 11/09/20.
6+
// Copyright © 2020 ITechnoDev. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
class SpinerAudioEqualizer: TransitionButtonAnimationDelegate {
13+
14+
func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?) {
15+
let lineSize = frame.width / 12
16+
let x = (layer.bounds.width - lineSize * 7) / 2
17+
let y = (layer.bounds.height - frame.height) / 2
18+
let duration: [CFTimeInterval] = [4.3, 2.5, 1.7, 3.1]
19+
let values = [0, 0.7, 0.4, 0.05, 0.95, 0.3, 0.9, 0.4, 0.15, 0.18, 0.75, 0.01]
20+
21+
// Draw lines
22+
for i in 0 ..< 4 {
23+
let animation = CAKeyframeAnimation()
24+
25+
animation.keyPath = "path"
26+
animation.isAdditive = true
27+
animation.values = []
28+
29+
for j in 0 ..< values.count {
30+
let heightFactor = values[j]
31+
let height = frame.height * CGFloat(heightFactor)
32+
let point = CGPoint(x: 0, y: frame.height - height)
33+
let path = UIBezierPath(rect: CGRect(origin: point, size: CGSize(width: lineSize, height: height - 20)))
34+
35+
animation.values?.append(path.cgPath)
36+
}
37+
animation.duration = duration[i]
38+
animation.repeatCount = HUGE
39+
animation.isRemovedOnCompletion = false
40+
41+
let line = TransitionButtonAnimationShape.line.layerWith(size: CGSize(width: lineSize, height: frame.height), color: color)
42+
let frame = CGRect(x: x + lineSize * 2 * CGFloat(i),
43+
y: y,
44+
width: lineSize,
45+
height: frame.height)
46+
47+
line.frame = frame
48+
line.add(animation, forKey: "animation")
49+
layer.addSublayer(line)
50+
}
51+
}
52+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// SpinerBallClipRotate.swift
3+
// TransitionButton
4+
//
5+
// Created by Rahul Mayani on 11/09/20.
6+
// Copyright © 2020 ITechnoDev. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
class SpinerBallClipRotate: TransitionButtonAnimationDelegate {
13+
14+
func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?) {
15+
let bigCircleSize: CGFloat = frame.width / 1.4
16+
let smallCircleSize: CGFloat = frame.width / 2.5
17+
let longDuration: CFTimeInterval = 1
18+
#if swift(>=4.2)
19+
let timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
20+
#else
21+
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
22+
#endif
23+
24+
circleOf(shape: .ringTwoHalfHorizontal,
25+
duration: longDuration,
26+
timingFunction: timingFunction,
27+
layer: layer,
28+
size: bigCircleSize,
29+
color: color, reverse: false)
30+
circleOf(shape: .ringTwoHalfVertical,
31+
duration: longDuration,
32+
timingFunction: timingFunction,
33+
layer: layer,
34+
size: smallCircleSize,
35+
color: color, reverse: true)
36+
}
37+
38+
func createAnimationIn(duration: CFTimeInterval, timingFunction: CAMediaTimingFunction, reverse: Bool) -> CAAnimation {
39+
// Scale animation
40+
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
41+
42+
scaleAnimation.keyTimes = [0, 0.5, 1]
43+
scaleAnimation.timingFunctions = [timingFunction, timingFunction]
44+
scaleAnimation.values = [1, 0.6, 1]
45+
scaleAnimation.duration = duration
46+
47+
// Rotate animation
48+
let rotateAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
49+
50+
rotateAnimation.keyTimes = scaleAnimation.keyTimes
51+
rotateAnimation.timingFunctions = [timingFunction, timingFunction]
52+
if !reverse {
53+
rotateAnimation.values = [0, Double.pi, 2 * Double.pi]
54+
} else {
55+
rotateAnimation.values = [0, -Double.pi, -2 * Double.pi]
56+
}
57+
rotateAnimation.duration = duration
58+
59+
// Animation
60+
let animation = CAAnimationGroup()
61+
62+
animation.animations = [scaleAnimation, rotateAnimation]
63+
animation.duration = duration
64+
animation.repeatCount = HUGE
65+
animation.isRemovedOnCompletion = false
66+
67+
return animation
68+
}
69+
70+
// swiftlint:disable:next function_parameter_count
71+
func circleOf(shape: TransitionButtonAnimationShape, duration: CFTimeInterval, timingFunction: CAMediaTimingFunction, layer: CALayer, size: CGFloat, color: UIColor, reverse: Bool) {
72+
let circle = shape.layerWith(size: CGSize(width: size, height: size), color: color)
73+
let frame = CGRect(x: (layer.bounds.size.width - size) / 2,
74+
y: (layer.bounds.size.height - size) / 2,
75+
width: size,
76+
height: size)
77+
let animation = createAnimationIn(duration: duration, timingFunction: timingFunction, reverse: reverse)
78+
79+
circle.frame = frame
80+
circle.add(animation, forKey: "animation")
81+
layer.addSublayer(circle)
82+
}
83+
}

0 commit comments

Comments
 (0)