Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Spiner animation types feature #55

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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