mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			208 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Swift
		
	
		
		
			
		
	
	
			208 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Swift
		
	
| 
											7 years ago
										 | // | ||
| 
											7 years ago
										 | //  Copyright (c) 2019 Open Whisper Systems. All rights reserved. | ||
| 
											7 years ago
										 | // | ||
|  | 
 | ||
|  | @objc class TypingIndicatorView: UIStackView { | ||
| 
											7 years ago
										 |     // This represents the spacing between the dots | ||
|  |     // _at their max size_. | ||
|  |     private let kDotMaxHSpacing: CGFloat = 3 | ||
| 
											7 years ago
										 | 
 | ||
|  |     @objc | ||
|  |     public static let kMinRadiusPt: CGFloat = 6 | ||
|  |     @objc | ||
|  |     public static let kMaxRadiusPt: CGFloat = 8 | ||
|  | 
 | ||
|  |     private let dot1 = DotView(dotType: .dotType1) | ||
|  |     private let dot2 = DotView(dotType: .dotType2) | ||
|  |     private let dot3 = DotView(dotType: .dotType3) | ||
|  | 
 | ||
|  |     @available(*, unavailable, message:"use other constructor instead.") | ||
|  |     required init(coder aDecoder: NSCoder) { | ||
|  |         notImplemented() | ||
|  |     } | ||
|  | 
 | ||
|  |     @available(*, unavailable, message:"use other constructor instead.") | ||
|  |     override init(frame: CGRect) { | ||
|  |         notImplemented() | ||
|  |     } | ||
|  | 
 | ||
|  |     @objc | ||
|  |     public init() { | ||
|  |         super.init(frame: .zero) | ||
|  | 
 | ||
|  |         // init(arrangedSubviews:...) is not a designated initializer. | ||
| 
											7 years ago
										 |         for dot in dots() { | ||
|  |             addArrangedSubview(dot) | ||
|  |         } | ||
| 
											7 years ago
										 | 
 | ||
|  |         self.axis = .horizontal | ||
|  |         self.spacing = kDotMaxHSpacing | ||
|  |         self.alignment = .center | ||
| 
											7 years ago
										 | 
 | ||
|  |         NotificationCenter.default.addObserver(self, | ||
|  |                                                selector: #selector(didBecomeActive), | ||
|  |                                                name: NSNotification.Name.OWSApplicationDidBecomeActive, | ||
|  |                                                object: nil) | ||
|  |     } | ||
|  | 
 | ||
|  |     deinit { | ||
|  |         NotificationCenter.default.removeObserver(self) | ||
|  |     } | ||
|  | 
 | ||
|  |     // MARK: - Notifications | ||
|  | 
 | ||
|  |     @objc func didBecomeActive() { | ||
|  |         AssertIsOnMainThread() | ||
|  | 
 | ||
|  |         // CoreAnimation animations are stopped in the background, so ensure | ||
|  |         // animations are restored if necessary. | ||
|  |         if isAnimating { | ||
|  |             startAnimation() | ||
|  |         } | ||
| 
											7 years ago
										 |     } | ||
|  | 
 | ||
| 
											7 years ago
										 |     // MARK: - | ||
|  | 
 | ||
| 
											7 years ago
										 |     @objc | ||
|  |     public override func sizeThatFits(_ size: CGSize) -> CGSize { | ||
|  |         return CGSize(width: TypingIndicatorView.kMaxRadiusPt * 3 + kDotMaxHSpacing * 2, height: TypingIndicatorView.kMaxRadiusPt) | ||
|  |     } | ||
|  | 
 | ||
| 
											7 years ago
										 |     private func dots() -> [DotView] { | ||
|  |         return [dot1, dot2, dot3] | ||
|  |     } | ||
|  | 
 | ||
| 
											7 years ago
										 |     private var isAnimating = false | ||
|  | 
 | ||
| 
											7 years ago
										 |     @objc | ||
|  |     public func startAnimation() { | ||
| 
											7 years ago
										 |         isAnimating = true | ||
|  | 
 | ||
| 
											7 years ago
										 |         for dot in dots() { | ||
|  |             dot.startAnimation() | ||
|  |         } | ||
| 
											7 years ago
										 |     } | ||
|  | 
 | ||
|  |     @objc | ||
|  |     public func stopAnimation() { | ||
| 
											7 years ago
										 |         isAnimating = false | ||
|  | 
 | ||
| 
											7 years ago
										 |         for dot in dots() { | ||
|  |             dot.stopAnimation() | ||
|  |         } | ||
| 
											7 years ago
										 |     } | ||
|  | 
 | ||
|  |     private enum DotType { | ||
|  |         case dotType1 | ||
|  |         case dotType2 | ||
|  |         case dotType3 | ||
|  |     } | ||
|  | 
 | ||
|  |     private class DotView: UIView { | ||
|  |         private let dotType: DotType | ||
|  | 
 | ||
|  |         private let shapeLayer = CAShapeLayer() | ||
|  | 
 | ||
|  |         @available(*, unavailable, message:"use other constructor instead.") | ||
|  |         required init?(coder aDecoder: NSCoder) { | ||
|  |             notImplemented() | ||
|  |         } | ||
|  | 
 | ||
|  |         @available(*, unavailable, message:"use other constructor instead.") | ||
|  |         override init(frame: CGRect) { | ||
|  |             notImplemented() | ||
|  |         } | ||
|  | 
 | ||
|  |         init(dotType: DotType) { | ||
|  |             self.dotType = dotType | ||
|  | 
 | ||
|  |             super.init(frame: .zero) | ||
|  | 
 | ||
|  |             autoSetDimension(.width, toSize: kMaxRadiusPt) | ||
|  |             autoSetDimension(.height, toSize: kMaxRadiusPt) | ||
|  | 
 | ||
| 
											7 years ago
										 |             layer.addSublayer(shapeLayer) | ||
| 
											7 years ago
										 |         } | ||
|  | 
 | ||
| 
											7 years ago
										 |         fileprivate func startAnimation() { | ||
|  |             stopAnimation() | ||
|  | 
 | ||
| 
											5 years ago
										 |             let baseColor = Colors.text | ||
| 
											7 years ago
										 |             let timeIncrement: CFTimeInterval = 0.15 | ||
|  |             var colorValues = [CGColor]() | ||
|  |             var pathValues = [CGPath]() | ||
|  |             var keyTimes = [CFTimeInterval]() | ||
|  |             var animationDuration: CFTimeInterval = 0 | ||
|  | 
 | ||
|  |             let addDotKeyFrame = { (keyFrameTime: CFTimeInterval, progress: CGFloat) in | ||
| 
											7 years ago
										 |                 let dotColor = baseColor.withAlphaComponent(CGFloatLerp(0.4, 1.0, CGFloatClamp01(progress))) | ||
| 
											7 years ago
										 |                 colorValues.append(dotColor.cgColor) | ||
| 
											7 years ago
										 |                 let radius = CGFloatLerp(TypingIndicatorView.kMinRadiusPt, TypingIndicatorView.kMaxRadiusPt, CGFloatClamp01(progress)) | ||
| 
											7 years ago
										 |                 let margin = (TypingIndicatorView.kMaxRadiusPt - radius) * 0.5 | ||
|  |                 let bezierPath = UIBezierPath(ovalIn: CGRect(x: margin, y: margin, width: radius, height: radius)) | ||
|  |                 pathValues.append(bezierPath.cgPath) | ||
|  | 
 | ||
|  |                 keyTimes.append(keyFrameTime) | ||
|  |                 animationDuration = max(animationDuration, keyFrameTime) | ||
|  |             } | ||
|  | 
 | ||
|  |             // All animations in the group apparently need to have the same number | ||
|  |             // of keyframes, and use the same timing. | ||
|  |             switch dotType { | ||
|  |             case .dotType1: | ||
|  |                 addDotKeyFrame(0 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(1 * timeIncrement, 0.5) | ||
|  |                 addDotKeyFrame(2 * timeIncrement, 1.0) | ||
|  |                 addDotKeyFrame(3 * timeIncrement, 0.5) | ||
|  |                 addDotKeyFrame(4 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(5 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(6 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(10 * timeIncrement, 0.0) | ||
|  |                 break | ||
|  |             case .dotType2: | ||
|  |                 addDotKeyFrame(0 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(1 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(2 * timeIncrement, 0.5) | ||
|  |                 addDotKeyFrame(3 * timeIncrement, 1.0) | ||
|  |                 addDotKeyFrame(4 * timeIncrement, 0.5) | ||
|  |                 addDotKeyFrame(5 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(6 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(10 * timeIncrement, 0.0) | ||
|  |                 break | ||
|  |             case .dotType3: | ||
|  |                 addDotKeyFrame(0 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(1 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(2 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(3 * timeIncrement, 0.5) | ||
|  |                 addDotKeyFrame(4 * timeIncrement, 1.0) | ||
|  |                 addDotKeyFrame(5 * timeIncrement, 0.5) | ||
|  |                 addDotKeyFrame(6 * timeIncrement, 0.0) | ||
|  |                 addDotKeyFrame(10 * timeIncrement, 0.0) | ||
|  |                 break | ||
|  |             } | ||
|  | 
 | ||
|  |             let makeAnimation: (String, [Any]) -> CAKeyframeAnimation = { (keyPath, values) in | ||
|  |                 let animation = CAKeyframeAnimation() | ||
|  |                 animation.keyPath = keyPath | ||
|  |                 animation.values = values | ||
|  |                 animation.duration = animationDuration | ||
|  |                 return animation | ||
|  |             } | ||
|  | 
 | ||
|  |             let groupAnimation = CAAnimationGroup() | ||
|  |             groupAnimation.animations = [ | ||
|  |                 makeAnimation("fillColor", colorValues), | ||
|  |                 makeAnimation("path", pathValues) | ||
|  |             ] | ||
|  |             groupAnimation.duration = animationDuration | ||
|  |             groupAnimation.repeatCount = MAXFLOAT | ||
|  | 
 | ||
|  |             shapeLayer.add(groupAnimation, forKey: UUID().uuidString) | ||
|  |         } | ||
| 
											7 years ago
										 | 
 | ||
| 
											7 years ago
										 |         fileprivate func stopAnimation() { | ||
|  |             shapeLayer.removeAllAnimations() | ||
| 
											7 years ago
										 |         } | ||
|  |     } | ||
|  | } |