Last active
February 24, 2020 13:19
-
-
Save WorldDownTown/b74fbdd4120c7e88a72d3e1e9bef6fdb to your computer and use it in GitHub Desktop.
Loading indicator like Google's Application
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import PlaygroundSupport | |
import UIKit | |
final class GoogleLoadingIndicator: UIView { | |
var colors: [UIColor] = [] | |
private var index: Int = 0 | |
private let totalDuration: TimeInterval = 2 | |
private let movingAngle: CGFloat = 5 / 6 * .pi2 | |
private let rotationCount: Int = 6 | |
private let minimumAngle: CGFloat = .pi2 / 24 | |
private let strokeAnimationKey: String = "strokeAnimationKey" | |
private let rotatingLayer: CALayer = .init() | |
private let circleLayer: CAShapeLayer = { | |
let layer: CAShapeLayer = .init() | |
layer.fillColor = UIColor.clear.cgColor | |
layer.lineWidth = 5 | |
return layer | |
}() | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
rotatingLayer.frame = layer.bounds | |
circleLayer.frame = rotatingLayer.bounds | |
circleLayer.path = makeCirclePath() | |
} | |
func startAnimation() { | |
circleLayer.strokeColor = colors.first?.cgColor ?? UIColor.blue.cgColor | |
rotatingLayer.addSublayer(circleLayer) | |
layer.addSublayer(rotatingLayer) | |
rotatingLayer.add(makeRotationAnimation(), forKey: nil) | |
addStrokeAnimation() | |
} | |
func stopAnimation() { | |
rotatingLayer.removeFromSuperlayer() | |
rotatingLayer.removeAllAnimations() | |
circleLayer.removeAllAnimations() | |
} | |
private func makeCirclePath() -> CGPath { | |
let path: UIBezierPath = .init() | |
path.addArc(withCenter: CGPoint(x: frame.width / 2, y: frame.height / 2), | |
radius: frame.width / 2, | |
startAngle: -minimumAngle, | |
endAngle: .pi2 * 2, // 2回転 | |
clockwise: true) | |
return path.cgPath | |
} | |
private func makeRotationAnimation() -> CABasicAnimation { | |
let animation: CABasicAnimation = .init(keyPath: "transform.rotation") | |
animation.duration = totalDuration | |
animation.fromValue = 0 | |
animation.toValue = CGFloat.pi2 | |
animation.isRemovedOnCompletion = false | |
animation.fillMode = .forwards | |
animation.repeatCount = .infinity | |
return animation | |
} | |
private func makeStrokeAnimationGroup(startingAngle: CGFloat) -> CAAnimationGroup { | |
let totalAngle: CGFloat = .pi2 * 2 + minimumAngle | |
let animation0: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeStart)) | |
animation0.duration = 0 | |
animation0.beginTime = 0 | |
animation0.fromValue = startingAngle / totalAngle | |
animation0.toValue = animation0.fromValue | |
animation0.isRemovedOnCompletion = false | |
animation0.fillMode = .forwards | |
let animation1: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeEnd)) | |
animation1.duration = totalDuration * 0.4 | |
animation1.beginTime = 0 | |
animation1.fromValue = (startingAngle + minimumAngle) / totalAngle | |
animation1.toValue = (startingAngle + minimumAngle + movingAngle) / totalAngle | |
animation1.isRemovedOnCompletion = false | |
animation1.fillMode = .forwards | |
let animation2: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeStart)) | |
animation2.duration = totalDuration * 0.4 | |
animation2.beginTime = totalDuration * 0.3 | |
animation2.fromValue = startingAngle / totalAngle | |
animation2.toValue = (startingAngle + movingAngle) / totalAngle | |
animation2.isRemovedOnCompletion = false | |
animation2.fillMode = .forwards | |
let animationGroup: CAAnimationGroup = .init() | |
animationGroup.animations = [animation0, animation1, animation2] | |
animationGroup.duration = totalDuration | |
animationGroup.isRemovedOnCompletion = false | |
animationGroup.fillMode = .forwards | |
return animationGroup | |
} | |
private func addStrokeAnimation(startingAngle: CGFloat = 0) { | |
let strokeAnimation: CAAnimationGroup = makeStrokeAnimationGroup(startingAngle: startingAngle) | |
strokeAnimation.delegate = self | |
circleLayer.add(strokeAnimation, forKey: strokeAnimationKey) | |
} | |
} | |
// MARK: - CAAnimationDelegate | |
extension GoogleLoadingIndicator: CAAnimationDelegate { | |
func animationDidStop(_ animation: CAAnimation, finished flag: Bool) { | |
guard flag else { return } | |
if let color = colors.popLast() { | |
circleLayer.strokeColor = color.cgColor | |
colors.insert(color, at: 0) | |
} | |
index = (index + 1) % rotationCount | |
let startingAngle: CGFloat = (movingAngle * CGFloat(index)).truncatingRemainder(dividingBy: .pi2) | |
addStrokeAnimation(startingAngle: startingAngle) | |
} | |
} | |
private extension CGFloat { | |
static var pi2: Self { .pi * 2 } | |
} | |
final class MyViewController : UIViewController { | |
private let indicator: GoogleLoadingIndicator = .init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
view.backgroundColor = .white | |
indicator.center = view.center | |
indicator.colors = [.blue, .green, .yellow, .red] | |
view.addSubview(indicator) | |
indicator.startAnimation() | |
} | |
} | |
PlaygroundPage.current.liveView = MyViewController() |
Author
WorldDownTown
commented
Feb 24, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment