Files
2022-02-04 13:42:29 +00:00

511 lines
24 KiB
Swift

//
// ConnectionLogo.swift
// PrivadoVPN
//
// Created by Lizaveta Malinouskaya on 18.11.21.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import UIKit
protocol ConnectionLogoInput {
func update(state: ConnectionViewColorState)
func didTapPastClickZone()
}
class ConnectionLogo: UIView, ConnectionLogoInput {
enum Constants {
enum Localized {
static let tap = NSLocalizedString("connection.lable.tap", comment: "Tap here")
}
enum Font {
static let label = UIFont(name: "Roboto-Medium", size: 14 * Constants.Geometry.multiplier)
}
enum AnimationKeys {
static let opacity = "opacity"
static let tint = "contentsMultiplyColor"
static let opacity1 = "OpacityAnimation1"
static let opacity2 = "OpacityAnimation2"
static let opacity3 = "OpacityAnimation3"
static let rotation = "rotationAnimation"
static let color = "ColorAnimation"
}
enum AnimationDuration {
static let animation: CFTimeInterval = 3 * fadeInOut
static let fadeInOut: CFTimeInterval = 3
static let connected: CFTimeInterval = 0.5
static let color: CFTimeInterval = 8
static let rotation: CFTimeInterval = 10
static let disconnect: CFTimeInterval = 0.5
}
enum Geometry {
static let lock: CGFloat = 85 * multiplier
static let background: CGFloat = 233 * multiplier
static let innerCircle: CGFloat = 117 * multiplier
static let middleCircle: CGFloat = 145 * multiplier
static let outerCircle: CGFloat = 173 * multiplier
static let rectHeight: CGFloat = 26 * multiplier
static let rectWidth: CGFloat = 85 * multiplier
static let labelY: CGFloat = 32 * multiplier
static let multiplier = DeviceInfoProvider.isIPad ? 0.85 : 0.75
}
enum Image {
static let lockDisconnected = UIImage(named: "ipad.connection.lock.disconnected.new")
static let background = UIImage(named: "ipad.connection.background.ellipse")
static let innerCircle = UIImage(named: "ipad.connection.circle.inner")
static let middleCircle = UIImage(named: "ipad.connection.circle.middle")
static let outerCircle = UIImage(named: "ipad.connection.circle.out")
static let innerLarge = UIImage(named: "ipad.connection.circle.inner.large")
static let middleLarge = UIImage(named: "ipad.connection.circle.middle.large")
static let outerLarge = UIImage(named: "ipad.connection.circle.out.large")
static let innerMedium = UIImage(named: "ipad.connection.circle.inner.medium")
static let middleMedium = UIImage(named: "ipad.connection.circle.middle.medium")
static let outerMedium = UIImage(named: "ipad.connection.circle.out.medium")
static let innerSmall = UIImage(named: "ipad.connection.circle.inner.small")
static let outerSmall = UIImage(named: "ipad.connection.circle.out.small")
}
enum Color {
static let clickBackground = Colors.AnimationLock.clickBackground.color
static let clickLock = Colors.AnimationLock.clickLock.color
static let clickRings = Colors.AnimationLock.clickRings.color
static let clickText = Colors.AnimationLock.clickText.color
static let clickWhite = Colors.basicText.color
static let backgroundBlue = Colors.AnimationLock.blue.color
static let connected = Colors.AnimationLock.green.color
static let disconnected = Colors.AnimationLock.red.color
static let progress = Colors.AnimationLock.yellow.color
static let white = Colors.AnimationLock.white.color
}
}
private var state: ConnectionViewColorState? {
willSet {
if newValue == self.state {
self.didEnterTwice = true
} else {
self.didEnterTwice = false
}
}
}
private var connectionProgressColor = Constants.Color.progress
private var didEnterTwice = false
private var tapLabel: UIView?
private var lockView: UIImageView?
private var innerRingView: UIImageView?
private var middleRingView: UIImageView?
private var outerRingView: UIImageView?
private var backgroundView: UIImageView?
private var innerRingViews: [UIImageView] = []
private var middleRingViews: [UIImageView] = []
private var outerRingViews: [UIImageView] = []
private var foregroundImageView: UIImageView?
private var didReturnFromBackground = false
private var clickZoneAnimationFinished = true
// MARK: - Init
init() {
super.init(frame: .zero)
NotificationCenter.default.addObserver(self, selector: #selector(self.applicationDidBecomeActive), name: Notification.Name(PrivadoConstants.Application.didBecomeActive), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.applicationDidBecomeActive), name: Notification.Name(PrivadoConstants.Notification.serversViewDidAppear), object: nil)
self.createUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
let a = "adfdf"
}
// MARK: - selectors
// Set animations after application have been closed
@objc func applicationDidBecomeActive() {
if self.didReturnFromBackground {
guard let state = self.state else { return }
self.update(state: state)
self.setRotationAnimations()
}
self.didReturnFromBackground = true
}
@objc func didTapPastClickZone() {
if self.state == .disconnected {
if self.clickZoneAnimationFinished {
UIView.animate(withDuration: Constants.AnimationDuration.disconnect, delay: 0, options: .autoreverse, animations: {
self.changeRingsColor(to: Constants.Color.clickRings)
self.tapLabel?.layer.opacity = 0
self.tapLabel?.layer.opacity = 1
self.lockView?.tintColor = Constants.Color.clickLock
self.backgroundView?.tintColor = Constants.Color.clickBackground
self.clickZoneAnimationFinished = false
}, completion: { _ in
self.tapLabel?.layer.opacity = 0
self.changeRingsColor(to: Constants.Color.white)
self.lockView?.tintColor = Constants.Color.disconnected
self.backgroundView?.tintColor = Constants.Color.backgroundBlue
self.clickZoneAnimationFinished = true
})
}
}
}
// MARK: - ConnectionLogoInput
func update(state: ConnectionViewColorState) {
self.state = state
if !didEnterTwice {
switch state {
case .connected:
self.stopProgressAnimation()
self.setRotationSpeed(isConnecting: false)
self.addConnectedAnimation()
case .connecting:
self.setRotationSpeed(isConnecting: true)
self.addProgressAnimation()
self.changeRingsColor(to: Constants.Color.white)
case .disconnecting:
self.stopProgressAnimation()
self.stopConnectedAnimation()
UIView.animate(withDuration: Constants.AnimationDuration.disconnect, animations: {
self.changeRingsColor(to: Constants.Color.disconnected)
self.lockView?.tintColor = Constants.Color.disconnected
})
case .disconnected:
self.stopProgressAnimation()
UIView.animate(withDuration: Constants.AnimationDuration.disconnect, animations: {
self.changeRingsColor(to: Constants.Color.white)
})
self.lockView?.tintColor = Constants.Color.disconnected
case .error:
self.setRotationSpeed(isConnecting: false)
self.stopProgressAnimation()
}
}
}
// MARK: - UI
private func createUI() {
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundView = self.initializeImage(from: self, image: Constants.Image.background, width: Constants.Geometry.background)
self.backgroundView?.tintColor = Constants.Color.backgroundBlue
self.lockView = self.initializeImage(from: self, image: Constants.Image.lockDisconnected, width: Constants.Geometry.lock)
self.innerRingView = self.initializeImage(from: self, image: Constants.Image.innerCircle, width: Constants.Geometry.innerCircle)
self.innerRingViews = [self.initializeImage(from: self, image: Constants.Image.innerLarge, width: Constants.Geometry.innerCircle),
self.initializeImage(from: self, image: Constants.Image.innerMedium, width: Constants.Geometry.innerCircle),
self.initializeImage(from: self, image: Constants.Image.innerSmall, width: Constants.Geometry.innerCircle)]
self.middleRingView = self.initializeImage(from: self, image: Constants.Image.middleCircle, width: Constants.Geometry.middleCircle)
self.middleRingViews = [self.initializeImage(from: self, image: Constants.Image.middleLarge, width: Constants.Geometry.middleCircle),
self.initializeImage(from: self, image: Constants.Image.middleMedium, width: Constants.Geometry.middleCircle)]
self.outerRingView = self.initializeImage(from: self, image: Constants.Image.outerCircle, width: Constants.Geometry.outerCircle)
self.outerRingViews = [self.initializeImage(from: self, image: Constants.Image.outerLarge, width: Constants.Geometry.outerCircle),
self.initializeImage(from: self, image: Constants.Image.outerMedium, width: Constants.Geometry.outerCircle),
self.initializeImage(from: self, image: Constants.Image.outerSmall, width: Constants.Geometry.outerCircle)]
self.tapLabel = self.initializeTapLabel(from: self)
self.setRotationAnimations()
self.setColorViewsOpacity()
self.changeRingsColor(to: Constants.Color.white)
}
private func initializeImage(from container: UIView, image: UIImage?, width: CGFloat) -> UIImageView {
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(imageView)
imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
imageView.widthAnchor.constraint(equalToConstant: width),
imageView.heightAnchor.constraint(equalToConstant: width)
])
return imageView
}
private func initializeTapLabel(from container: UIView) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = Constants.Color.clickWhite
view.layer.cornerRadius = 10
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = Constants.Font.label
label.text = Constants.Localized.tap
label.textColor = Constants.Color.backgroundBlue
view.addSubview(label)
view.layer.opacity = 0
self.addSubview(view)
NSLayoutConstraint.activate([
view.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: Constants.Geometry.labelY),
view.centerXAnchor.constraint(equalTo: self.centerXAnchor),
view.heightAnchor.constraint(equalToConstant: Constants.Geometry.rectHeight),
view.widthAnchor.constraint(equalToConstant: Constants.Geometry.rectWidth),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
return view
}
// MARK: - animations
private func setRotationSpeed(isConnecting: Bool) {
let speed: Float = isConnecting ? 2 : 1.1
self.layer.timeOffset = self.layer.convertTime(CACurrentMediaTime(), from: nil)
self.layer.beginTime = CACurrentMediaTime()
self.layer.speed = speed
}
private func setRotationAnimations() {
self.innerRingView?.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation)
self.outerRingView?.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation)
self.middleRingView?.rotate(clockwise: true, duration: Constants.AnimationDuration.rotation)
for view in self.innerRingViews {
view.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation)
}
for view in self.outerRingViews {
view.rotate(clockwise: false, duration: Constants.AnimationDuration.rotation)
}
for view in self.middleRingViews {
view.rotate(clockwise: true, duration: Constants.AnimationDuration.rotation)
}
}
private func addProgressAnimation() {
self.setColorShiftAnimation(fromColor: Constants.Color.disconnected, toColor: Constants.Color.progress, duration: Constants.AnimationDuration.color)
self.addInnerCircleOpacityAnimation(isConnecting: true)
self.addMidleCircleOpacityAnimation(isConnecting: true)
self.addOuterCircleOpacityAnimation(isConnecting: true)
}
private func addConnectedAnimation() {
self.setColorShiftAnimation(fromColor: self.connectionProgressColor, toColor: Constants.Color.connected, duration: Constants.AnimationDuration.color / 2)
self.addInnerCircleOpacityAnimation(isConnecting: false)
self.addMidleCircleOpacityAnimation(isConnecting: false)
self.addOuterCircleOpacityAnimation(isConnecting: false)
}
private func setColorShiftAnimation(fromColor: UIColor, toColor: UIColor, duration: CFTimeInterval) {
self.addColorShiftAnimation(to: self.lockView, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration)
self.lockView?.tintColor = toColor
for view in self.innerRingViews {
self.addColorShiftAnimation(to: view, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration)
view.tintColor = toColor
}
for view in self.outerRingViews {
self.addColorShiftAnimation(to: view, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration)
view.tintColor = toColor
}
for view in self.middleRingViews {
self.addColorShiftAnimation(to: view, fromColor: fromColor.cgColor, toColor: toColor.cgColor, duration: duration)
view.tintColor = toColor
}
}
private func changeRingsColor(to color: UIColor) {
self.innerRingView?.tintColor = color
self.middleRingView?.tintColor = color
self.outerRingView?.tintColor = color
}
private func addOuterCircleOpacityAnimation(isConnecting: Bool) {
if isConnecting {
// large - large - small - all else {
var animations: [CABasicAnimation] = []
animations.append(contentsOf: self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut))
animations.append(contentsOf: self.getOpacityAnimations(beginTime: Constants.AnimationDuration.fadeInOut, duration: Constants.AnimationDuration.fadeInOut))
let group = CAAnimationGroup()
group.animations = animations
group.duration = Constants.AnimationDuration.animation
group.beginTime = CACurrentMediaTime()
group.repeatCount = Float.infinity
self.outerRingViews[safe: 0]?.layer.add(group, forKey: Constants.AnimationKeys.opacity1)
// small part
group.beginTime = CACurrentMediaTime() + 2 * Constants.AnimationDuration.fadeInOut
group.animations = getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut)
self.outerRingViews[safe: 2]?.layer.add(group, forKey: Constants.AnimationKeys.opacity2)
} else {
UIView.animate(withDuration: Constants.AnimationDuration.connected,
delay: 2 * Constants.AnimationDuration.connected,
options: .autoreverse,
animations: {
for view in self.outerRingViews {
view.layer.opacity = 0
view.layer.opacity = 1
}
},
completion: {_ in
for view in self.outerRingViews {
view.layer.opacity = 0
}
})
}
}
private func addMidleCircleOpacityAnimation(isConnecting: Bool) {
if isConnecting {
// large - medium - medium - all else {
let group = CAAnimationGroup()
group.repeatCount = Float.infinity
group.duration = Constants.AnimationDuration.animation
group.beginTime = CACurrentMediaTime()
group.animations = self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut)
self.middleRingViews[safe: 0]?.layer.add(group, forKey: Constants.AnimationKeys.opacity1)
var animations: [CABasicAnimation] = []
animations.append(contentsOf: self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut))
animations.append(contentsOf: self.getOpacityAnimations(beginTime: Constants.AnimationDuration.fadeInOut, duration: Constants.AnimationDuration.fadeInOut))
group.beginTime = CACurrentMediaTime() + Constants.AnimationDuration.fadeInOut
group.animations = animations
self.middleRingViews[safe: 1]?.layer.add(group, forKey: Constants.AnimationKeys.opacity2)
} else {
UIView.animate(withDuration: Constants.AnimationDuration.connected,
delay: Constants.AnimationDuration.connected,
options: .autoreverse,
animations: {
for view in self.middleRingViews {
view.layer.opacity = 0
view.layer.opacity = 1
}
},
completion: {_ in
for view in self.middleRingViews {
view.layer.opacity = 0
}
})
}
}
private func addInnerCircleOpacityAnimation(isConnecting: Bool) {
if isConnecting {
// medium - medium - small
// medium part 2 times
var animations: [CABasicAnimation] = []
animations.append(contentsOf: self.getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut))
animations.append(contentsOf: self.getOpacityAnimations(beginTime: Constants.AnimationDuration.fadeInOut, duration: Constants.AnimationDuration.fadeInOut))
let group = CAAnimationGroup()
group.animations = animations
group.duration = Constants.AnimationDuration.animation
group.beginTime = CACurrentMediaTime()
group.repeatCount = Float.infinity
self.innerRingViews[safe: 0]?.layer.add(group, forKey: Constants.AnimationKeys.opacity1)
// small part
group.beginTime = CACurrentMediaTime() + 2 * Constants.AnimationDuration.fadeInOut
group.animations = getOpacityAnimations(beginTime: 0, duration: Constants.AnimationDuration.fadeInOut)
self.innerRingViews[safe: 2]?.layer.add(group, forKey: Constants.AnimationKeys.opacity2)
} else {
UIView.animate(withDuration: Constants.AnimationDuration.connected,
delay: 0,
options: .autoreverse,
animations: {
for view in self.innerRingViews {
view.layer.opacity = 0
view.layer.opacity = 1
}
},
completion: {_ in
for view in self.innerRingViews {
view.layer.opacity = 0
}
})
}
}
private func getOpacityAnimations(beginTime: CFTimeInterval, duration: CFTimeInterval) -> [CABasicAnimation] {
let fromZeroAnimation = CABasicAnimation(keyPath: Constants.AnimationKeys.opacity)
fromZeroAnimation.fromValue = 0
fromZeroAnimation.toValue = 1
fromZeroAnimation.duration = duration / 2
fromZeroAnimation.beginTime = beginTime
let toZeroAnimation = CABasicAnimation(keyPath: Constants.AnimationKeys.opacity)
toZeroAnimation.fromValue = 1
toZeroAnimation.toValue = 0
toZeroAnimation.beginTime = beginTime + duration / 2
toZeroAnimation.duration = duration / 2
return [fromZeroAnimation, toZeroAnimation]
}
private func addColorShiftAnimation(to view: UIView?, fromColor: CGColor, toColor: CGColor, duration: CFTimeInterval) {
let disconnectedToProgressAnimation = CABasicAnimation(keyPath: Constants.AnimationKeys.tint)
disconnectedToProgressAnimation.fromValue = fromColor
disconnectedToProgressAnimation.toValue = toColor
disconnectedToProgressAnimation.duration = duration
view?.layer.add(disconnectedToProgressAnimation, forKey: Constants.AnimationKeys.color)
}
private func stopConnectedAnimation() {
self.lockView?.layer.removeAnimation(forKey: Constants.AnimationKeys.color)
self.setColorViewsOpacity()
}
private func stopProgressAnimation() {
self.lockView?.layer.removeAnimation(forKey: Constants.AnimationKeys.color)
let removeAnimations = { (view: UIView) in
view.layer.removeAnimation(forKey: Constants.AnimationKeys.opacity1)
view.layer.removeAnimation(forKey: Constants.AnimationKeys.opacity2)
view.layer.removeAnimation(forKey: Constants.AnimationKeys.opacity3)
}
for view in self.innerRingViews {
removeAnimations(view)
}
for view in self.middleRingViews {
removeAnimations(view)
}
for view in self.outerRingViews {
removeAnimations(view)
}
}
private func setColorViewsOpacity() {
for view in self.middleRingViews {
view.layer.opacity = 0
}
for view in self.outerRingViews {
view.layer.opacity = 0
}
for view in self.innerRingViews {
view.layer.opacity = 0
}
}
}
extension UIView {
func rotate(clockwise: Bool, duration: CFTimeInterval) {
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
var value: Double = 0
if clockwise {
value = Double.pi * 2
} else {
value = -Double.pi * 2
}
rotation.toValue = NSNumber(value: value)
rotation.duration = duration
rotation.isCumulative = true
rotation.repeatCount = Float.greatestFiniteMagnitude
self.layer.add(rotation, forKey: "RotationAnimation")
}
}