Skip to content

Instantly share code, notes, and snippets.

@WorldDownTown
Last active February 24, 2020 13:17
Show Gist options
  • Save WorldDownTown/5b305bf546fdb5d510be003a5a14e305 to your computer and use it in GitHub Desktop.
Save WorldDownTown/5b305bf546fdb5d510be003a5a14e305 to your computer and use it in GitHub Desktop.
Checkmark with ripple animation
import PlaygroundSupport
import UIKit
final class RippleCheckmark: UIView {
private let circleLayer: CAShapeLayer = .init()
private let rippleLayer: CAShapeLayer = .init()
private let checkmarkLayer: CAShapeLayer = .init()
private var completion: (() -> Void)?
var fillColor: UIColor = .blue {
didSet { update() }
}
var strokeColor: UIColor = .white {
didSet { update() }
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
let position: CGPoint = .init(x: bounds.midX, y: bounds.midY)
rippleLayer.bounds = layer.bounds
rippleLayer.position = position
rippleLayer.path = UIBezierPath(ovalIn: rippleLayer.bounds).cgPath
circleLayer.bounds = layer.bounds
circleLayer.position = position
circleLayer.path = UIBezierPath(ovalIn: circleLayer.bounds).cgPath
checkmarkLayer.bounds = circleLayer.bounds
checkmarkLayer.position = position
checkmarkLayer.path = checkmarkPath().cgPath
}
func startAnimation(completion: @escaping () -> Void) {
self.completion = completion
checkmarkLayer.add(makeStrokeAnimation(), forKey: nil)
circleLayer.add(makeScaleAnimation(), forKey: nil)
let rippleAnimation: CAAnimationGroup = makeRippleAnimation()
rippleAnimation.delegate = self
rippleLayer.add(rippleAnimation, forKey: nil)
}
private func setup() {
backgroundColor = .clear
layer.addSublayer(rippleLayer)
checkmarkLayer.fillColor = UIColor.clear.cgColor
checkmarkLayer.lineWidth = 2
checkmarkLayer.lineCap = .round
circleLayer.addSublayer(checkmarkLayer)
layer.addSublayer(circleLayer)
update()
}
private func checkmarkPath() -> UIBezierPath {
let width: CGFloat = bounds.width
let height: CGFloat = bounds.height
let path: UIBezierPath = .init()
path.move(to: CGPoint(x: width * 0.25, y: height * 0.525))
path.addLine(to: CGPoint(x: width * 0.4, y: height * 0.675))
path.addLine(to: CGPoint(x: width * 0.725, y: height * 0.35))
return path
}
private func update() {
rippleLayer.fillColor = fillColor.withAlphaComponent(0.2).cgColor
circleLayer.fillColor = fillColor.cgColor
checkmarkLayer.strokeColor = strokeColor.cgColor
checkmarkLayer.fillColor = fillColor.cgColor
}
private func makeStrokeAnimation() -> CABasicAnimation {
let animation: CABasicAnimation = .init(keyPath: #keyPath(CAShapeLayer.strokeEnd))
animation.duration = 0.4
animation.fromValue = 0
animation.toValue = 1
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
return animation
}
private func makeScaleAnimation() -> CASpringAnimation {
let animation: CASpringAnimation = .init(keyPath: #keyPath(CALayer.transform))
animation.fromValue = CATransform3DMakeScale(0.5, 0.5, 1)
animation.toValue = CATransform3DIdentity
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.duration = animation.settlingDuration
animation.damping = 11.96
animation.initialVelocity = -3.64
animation.mass = 0.48
animation.stiffness = 300
return animation
}
private func makeRippleAnimation() -> CAAnimationGroup {
let animation1: CABasicAnimation = .init(keyPath: #keyPath(CALayer.transform))
animation1.duration = 1
animation1.fromValue = CATransform3DIdentity
animation1.toValue = CATransform3DMakeScale(2, 2, 1)
animation1.isRemovedOnCompletion = false
animation1.fillMode = .forwards
let animation2: CABasicAnimation = .init(keyPath: #keyPath(CALayer.opacity))
animation2.duration = 1
animation2.fromValue = 1
animation2.toValue = 0
animation2.isRemovedOnCompletion = false
animation2.fillMode = .forwards
let animationGroup: CAAnimationGroup = .init()
animationGroup.animations = [animation1, animation2]
animationGroup.duration = 1
animationGroup.isRemovedOnCompletion = false
animationGroup.fillMode = .forwards
animationGroup.timingFunction = CAMediaTimingFunction(name: .easeOut)
return animationGroup
}
}
// MARK: - CAAnimationDelegate
extension RippleCheckmark: CAAnimationDelegate {
func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
completion?()
completion = nil
rippleLayer.removeAllAnimations()
}
}
final class MyViewController : UIViewController {
private let checkmark: RippleCheckmark = .init(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
checkmark.fillColor = .black
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation()
}
}
private func startAnimation() {
checkmark.removeFromSuperview()
view.addSubview(checkmark)
checkmark.center = CGPoint(x: view.frame.width * 0.5, y: view.frame.height * 0.25)
checkmark.startAnimation {
}
}
}
PlaygroundPage.current.liveView = MyViewController()
@WorldDownTown
Copy link
Author

RippleCheckmark

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment