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
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| @objc class TypingIndicatorView: UIStackView {
 | |
|     // This represents the spacing between the dots
 | |
|     // _at their max size_.
 | |
|     private let kDotMaxHSpacing: CGFloat = 3
 | |
| 
 | |
|     @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.
 | |
|         for dot in dots() {
 | |
|             addArrangedSubview(dot)
 | |
|         }
 | |
| 
 | |
|         self.axis = .horizontal
 | |
|         self.spacing = kDotMaxHSpacing
 | |
|         self.alignment = .center
 | |
| 
 | |
|         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()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: -
 | |
| 
 | |
|     @objc
 | |
|     public override func sizeThatFits(_ size: CGSize) -> CGSize {
 | |
|         return CGSize(width: TypingIndicatorView.kMaxRadiusPt * 3 + kDotMaxHSpacing * 2, height: TypingIndicatorView.kMaxRadiusPt)
 | |
|     }
 | |
| 
 | |
|     private func dots() -> [DotView] {
 | |
|         return [dot1, dot2, dot3]
 | |
|     }
 | |
| 
 | |
|     private var isAnimating = false
 | |
| 
 | |
|     @objc
 | |
|     public func startAnimation() {
 | |
|         isAnimating = true
 | |
| 
 | |
|         for dot in dots() {
 | |
|             dot.startAnimation()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     public func stopAnimation() {
 | |
|         isAnimating = false
 | |
| 
 | |
|         for dot in dots() {
 | |
|             dot.stopAnimation()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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)
 | |
| 
 | |
|             layer.addSublayer(shapeLayer)
 | |
|         }
 | |
| 
 | |
|         fileprivate func startAnimation() {
 | |
|             stopAnimation()
 | |
| 
 | |
|             let baseColor = Colors.text
 | |
|             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
 | |
|                 let dotColor = baseColor.withAlphaComponent(CGFloatLerp(0.4, 1.0, CGFloatClamp01(progress)))
 | |
|                 colorValues.append(dotColor.cgColor)
 | |
|                 let radius = CGFloatLerp(TypingIndicatorView.kMinRadiusPt, TypingIndicatorView.kMaxRadiusPt, CGFloatClamp01(progress))
 | |
|                 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)
 | |
|         }
 | |
| 
 | |
|         fileprivate func stopAnimation() {
 | |
|             shapeLayer.removeAllAnimations()
 | |
|         }
 | |
|     }
 | |
| }
 |