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.
217 lines
7.1 KiB
Swift
217 lines
7.1 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import SessionUIKit
|
|
import SignalCoreKit
|
|
import SessionUtilitiesKit
|
|
|
|
@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: .sessionDidBecomeActive,
|
|
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)
|
|
|
|
ThemeManager.onThemeChange(observer: self) { [weak self] _, _ in
|
|
guard self?.shapeLayer.animationKeys()?.isEmpty == false else { return }
|
|
|
|
self?.startAnimation()
|
|
}
|
|
}
|
|
|
|
fileprivate func startAnimation() {
|
|
stopAnimation()
|
|
|
|
let baseColor: UIColor = (ThemeManager.currentTheme.color(for: .messageBubble_incomingText) ?? .white)
|
|
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), // stringlint:disable
|
|
makeAnimation("path", pathValues) // stringlint:disable
|
|
]
|
|
groupAnimation.duration = animationDuration
|
|
groupAnimation.repeatCount = MAXFLOAT
|
|
|
|
shapeLayer.add(groupAnimation, forKey: UUID().uuidString)
|
|
}
|
|
|
|
fileprivate func stopAnimation() {
|
|
shapeLayer.removeAllAnimations()
|
|
}
|
|
}
|
|
}
|