Skip to content

Commit

Permalink
Added Spiner animation types feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Rahul-Mayani committed Sep 5, 2022
1 parent ffd08cb commit 379c947
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 33 deletions.
155 changes: 155 additions & 0 deletions Sources/TransitionButton/SpinerAnimationType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// SpinerAnimationType.swift
// TransitionButton
//
// Created by Rahul Mayani on 11/09/20.
// Copyright © 2020 ITechnoDev. All rights reserved.
//

import Foundation
import UIKit

// swiftlint:disable:next class_delegate_protocol
protocol TransitionButtonAnimationDelegate {

func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?)
}

/**
Enum of animation types used for spiner animation.
- DefaultSpinner: LineSpinFadeLoader animation.
- BallRotate: BallRotate animation.
- BallPulse: BallPulse animation.
- AudioEqualizer: AudioEqualizer animation.
- BallClipRotate: BallClipRotate animation.
- BallScale: BallScale animation.
*/
public enum SpinerAnimationType: Int, CaseIterable {

/**
DefaultSpinner.
- returns: Instance of DefaultSpinner.
*/
case defaultSpinner = 0
/**
BallRotate.
- returns: Instance of SpinerBallRotate.
*/
case ballRotate = 1
/**
BallPulse.
- returns: Instance of SpinerBallPulse.
*/
case ballPulse = 2
/**
AudioEqualizer.
- returns: Instance of SpinerAudioEqualizer.
*/
case audioEqualizer = 3
/**
BallClipRotate.
- returns: Instance of SpinerBallClipRotate.
*/
case ballClipRotate = 4
/**
BallScale.
- returns: Instance of SpinerBallScale.
*/
case ballScale = 5


// swiftlint:disable:next cyclomatic_complexity function_body_length
func animation() -> TransitionButtonAnimationDelegate {
switch self {
case .defaultSpinner:
return DefaultSpinner()
case .ballRotate:
return SpinerBallRotate()
case .ballPulse:
return SpinerBallPulse()
case .audioEqualizer:
return SpinerAudioEqualizer()
case .ballClipRotate:
return SpinerBallClipRotate()
case .ballScale:
return SpinerBallScale()
}
}
}

enum TransitionButtonAnimationShape {
case circle
case ringTwoHalfVertical
case ringTwoHalfHorizontal
case line

// swiftlint:disable:next cyclomatic_complexity function_body_length
func layerWith(size: CGSize, color: UIColor) -> CALayer {
let layer: CAShapeLayer = CAShapeLayer()
var path: UIBezierPath = UIBezierPath()
let lineWidth: CGFloat = 2

switch self {
case .circle:
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
radius: size.width / 2,
startAngle: 0,
endAngle: CGFloat(2 * Double.pi),
clockwise: false)
layer.fillColor = color.cgColor
case .ringTwoHalfVertical:
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
radius: size.width / 2,
startAngle: CGFloat(-3 * Double.pi / 4),
endAngle: CGFloat(-Double.pi / 4),
clockwise: true)
path.move(
to: CGPoint(x: size.width / 2 - size.width / 2 * cos(CGFloat(Double.pi / 4)),
y: size.height / 2 + size.height / 2 * sin(CGFloat(Double.pi / 4)))
)
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
radius: size.width / 2,
startAngle: CGFloat(-5 * Double.pi / 4),
endAngle: CGFloat(-7 * Double.pi / 4),
clockwise: false)
layer.fillColor = nil
layer.strokeColor = color.cgColor
layer.lineWidth = lineWidth
case .ringTwoHalfHorizontal:
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
radius: size.width / 2,
startAngle: CGFloat(3 * Double.pi / 4),
endAngle: CGFloat(5 * Double.pi / 4),
clockwise: true)
path.move(
to: CGPoint(x: size.width / 2 + size.width / 2 * cos(CGFloat(Double.pi / 4)),
y: size.height / 2 - size.height / 2 * sin(CGFloat(Double.pi / 4)))
)
path.addArc(withCenter: CGPoint(x: size.width / 2, y: size.height / 2),
radius: size.width / 2,
startAngle: CGFloat(-Double.pi / 4),
endAngle: CGFloat(Double.pi / 4),
clockwise: true)
layer.fillColor = nil
layer.strokeColor = color.cgColor
layer.lineWidth = lineWidth
case .line:
path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height),
cornerRadius: size.width / 2)
layer.fillColor = color.cgColor
}

layer.backgroundColor = nil
layer.path = path.cgPath
layer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)

return layer
}
}
34 changes: 16 additions & 18 deletions Sources/TransitionButton/SpinerLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import UIKit

class SpinerLayer: CAShapeLayer {

/// Animation type.
public var type: SpinerAnimationType = .defaultSpinner

var spinnerColor = UIColor.white {
didSet {
strokeColor = spinnerColor.cgColor
Expand Down Expand Up @@ -38,34 +41,29 @@ class SpinerLayer: CAShapeLayer {
super.init(layer: layer)

}

func animation() {
self.isHidden = false
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0
rotate.toValue = Double.pi * 2
rotate.duration = 0.4
rotate.timingFunction = CAMediaTimingFunction(name: .linear)

rotate.repeatCount = HUGE
rotate.fillMode = .forwards
rotate.isRemovedOnCompletion = false
self.add(rotate, forKey: rotate.keyPath)

let animation: TransitionButtonAnimationDelegate = type.animation()
animation.setupSpinnerAnimation(in: self, frame: frame, color: spinnerColor, spinnerSize: nil)
}

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

func stopAnimation() {
self.isHidden = true
self.removeAllAnimations()
removeAnimationLayer()
}

private func removeAnimationLayer() {
if self.sublayers != nil {
for item in self.sublayers! {
item.removeAllAnimations()
item.removeFromSuperlayer()
}
}
}
}
46 changes: 46 additions & 0 deletions Sources/TransitionButton/Spiners/DefaultSpinner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// DefaultSpinner.swift
// TransitionButton
//
// Created by Rahul Mayani on 11/09/20.
// Copyright © 2020 ITechnoDev. All rights reserved.
//

import Foundation
import UIKit

class DefaultSpinner: TransitionButtonAnimationDelegate {

/// setup spinner layer
///
/// - Parameters:
/// - layer: layer Parent layer (Button layer)
/// - frame: frame of parant layer
/// - color: color of spinner
/// - spinnerSize: size of spinner layer
func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?) {

self.setToFrame(frame, layer: layer)

let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0
rotate.toValue = Double.pi * 2
rotate.duration = 0.4
rotate.timingFunction = CAMediaTimingFunction(name: .linear)

rotate.repeatCount = HUGE
rotate.fillMode = .forwards
rotate.isRemovedOnCompletion = false
layer.add(rotate, forKey: rotate.keyPath)
}

func setToFrame(_ frame: CGRect, layer: CAShapeLayer) {
let radius:CGFloat = (frame.height / 2) * 0.5
layer.frame = CGRect(x: 0, y: 0, width: frame.height, height: frame.height)
let center = CGPoint(x: frame.height / 2, y: layer.bounds.center.y)
let startAngle = 0 - Double.pi/2
let endAngle = Double.pi * 2 - Double.pi/2
let clockwise: Bool = true
layer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: clockwise).cgPath
}
}
52 changes: 52 additions & 0 deletions Sources/TransitionButton/Spiners/SpinerAudioEqualizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// SpinerAudioEqualizer.swift
// TransitionButton
//
// Created by Rahul Mayani on 11/09/20.
// Copyright © 2020 ITechnoDev. All rights reserved.
//

import Foundation
import UIKit

class SpinerAudioEqualizer: TransitionButtonAnimationDelegate {

func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?) {
let lineSize = frame.width / 12
let x = (layer.bounds.width - lineSize * 7) / 2
let y = (layer.bounds.height - frame.height) / 2
let duration: [CFTimeInterval] = [4.3, 2.5, 1.7, 3.1]
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]

// Draw lines
for i in 0 ..< 4 {
let animation = CAKeyframeAnimation()

animation.keyPath = "path"
animation.isAdditive = true
animation.values = []

for j in 0 ..< values.count {
let heightFactor = values[j]
let height = frame.height * CGFloat(heightFactor)
let point = CGPoint(x: 0, y: frame.height - height)
let path = UIBezierPath(rect: CGRect(origin: point, size: CGSize(width: lineSize, height: height - 20)))

animation.values?.append(path.cgPath)
}
animation.duration = duration[i]
animation.repeatCount = HUGE
animation.isRemovedOnCompletion = false

let line = TransitionButtonAnimationShape.line.layerWith(size: CGSize(width: lineSize, height: frame.height), color: color)
let frame = CGRect(x: x + lineSize * 2 * CGFloat(i),
y: y,
width: lineSize,
height: frame.height)

line.frame = frame
line.add(animation, forKey: "animation")
layer.addSublayer(line)
}
}
}
83 changes: 83 additions & 0 deletions Sources/TransitionButton/Spiners/SpinerBallClipRotate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// SpinerBallClipRotate.swift
// TransitionButton
//
// Created by Rahul Mayani on 11/09/20.
// Copyright © 2020 ITechnoDev. All rights reserved.
//

import Foundation
import UIKit

class SpinerBallClipRotate: TransitionButtonAnimationDelegate {

func setupSpinnerAnimation(in layer: CAShapeLayer, frame: CGRect, color: UIColor, spinnerSize: UInt?) {
let bigCircleSize: CGFloat = frame.width / 1.4
let smallCircleSize: CGFloat = frame.width / 2.5
let longDuration: CFTimeInterval = 1
#if swift(>=4.2)
let timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
#else
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
#endif

circleOf(shape: .ringTwoHalfHorizontal,
duration: longDuration,
timingFunction: timingFunction,
layer: layer,
size: bigCircleSize,
color: color, reverse: false)
circleOf(shape: .ringTwoHalfVertical,
duration: longDuration,
timingFunction: timingFunction,
layer: layer,
size: smallCircleSize,
color: color, reverse: true)
}

func createAnimationIn(duration: CFTimeInterval, timingFunction: CAMediaTimingFunction, reverse: Bool) -> CAAnimation {
// Scale animation
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")

scaleAnimation.keyTimes = [0, 0.5, 1]
scaleAnimation.timingFunctions = [timingFunction, timingFunction]
scaleAnimation.values = [1, 0.6, 1]
scaleAnimation.duration = duration

// Rotate animation
let rotateAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z")

rotateAnimation.keyTimes = scaleAnimation.keyTimes
rotateAnimation.timingFunctions = [timingFunction, timingFunction]
if !reverse {
rotateAnimation.values = [0, Double.pi, 2 * Double.pi]
} else {
rotateAnimation.values = [0, -Double.pi, -2 * Double.pi]
}
rotateAnimation.duration = duration

// Animation
let animation = CAAnimationGroup()

animation.animations = [scaleAnimation, rotateAnimation]
animation.duration = duration
animation.repeatCount = HUGE
animation.isRemovedOnCompletion = false

return animation
}

// swiftlint:disable:next function_parameter_count
func circleOf(shape: TransitionButtonAnimationShape, duration: CFTimeInterval, timingFunction: CAMediaTimingFunction, layer: CALayer, size: CGFloat, color: UIColor, reverse: Bool) {
let circle = shape.layerWith(size: CGSize(width: size, height: size), color: color)
let frame = CGRect(x: (layer.bounds.size.width - size) / 2,
y: (layer.bounds.size.height - size) / 2,
width: size,
height: size)
let animation = createAnimationIn(duration: duration, timingFunction: timingFunction, reverse: reverse)

circle.frame = frame
circle.add(animation, forKey: "animation")
layer.addSublayer(circle)
}
}
Loading

0 comments on commit 379c947

Please sign in to comment.