session-ios/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift

216 lines
7.1 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SignalCoreKit
2018-11-01 15:43:13 +01:00
@objc class TypingIndicatorView: UIStackView {
// This represents the spacing between the dots
// _at their max size_.
private let kDotMaxHSpacing: CGFloat = 3
2018-11-01 15:43:13 +01:00
@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.
2018-11-01 18:53:58 +01:00
for dot in dots() {
addArrangedSubview(dot)
}
2018-11-01 15:43:13 +01:00
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()
}
2018-11-01 15:43:13 +01:00
}
// MARK: -
2018-11-01 17:47:08 +01:00
@objc
public override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: TypingIndicatorView.kMaxRadiusPt * 3 + kDotMaxHSpacing * 2, height: TypingIndicatorView.kMaxRadiusPt)
}
2018-11-01 18:53:58 +01:00
private func dots() -> [DotView] {
return [dot1, dot2, dot3]
}
private var isAnimating = false
2018-11-01 15:43:13 +01:00
@objc
public func startAnimation() {
isAnimating = true
2018-11-01 18:53:58 +01:00
for dot in dots() {
dot.startAnimation()
}
2018-11-01 15:43:13 +01:00
}
@objc
public func stopAnimation() {
isAnimating = false
2018-11-01 18:53:58 +01:00
for dot in dots() {
dot.stopAnimation()
}
2018-11-01 15:43:13 +01:00
}
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)
2018-11-01 18:53:58 +01:00
layer.addSublayer(shapeLayer)
ThemeManager.onThemeChange(observer: self) { [weak self] _, _ in
guard self?.shapeLayer.animationKeys()?.isEmpty == false else { return }
self?.startAnimation()
}
2018-11-01 15:43:13 +01:00
}
2018-11-01 18:53:58 +01:00
fileprivate func startAnimation() {
stopAnimation()
Merge remote-tracking branch 'upstream/dev' into feature/theming # Conflicts: # Session.xcodeproj/project.pbxproj # Session/Closed Groups/NewClosedGroupVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/Message Cells/CallMessageCell.swift # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Home/HomeVC.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Home/NewConversationButtonSet.swift # Session/Meta/Translations/de.lproj/Localizable.strings # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/es.lproj/Localizable.strings # Session/Meta/Translations/fa.lproj/Localizable.strings # Session/Meta/Translations/fi.lproj/Localizable.strings # Session/Meta/Translations/fr.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/hr.lproj/Localizable.strings # Session/Meta/Translations/id-ID.lproj/Localizable.strings # Session/Meta/Translations/it.lproj/Localizable.strings # Session/Meta/Translations/ja.lproj/Localizable.strings # Session/Meta/Translations/nl.lproj/Localizable.strings # Session/Meta/Translations/pl.lproj/Localizable.strings # Session/Meta/Translations/pt_BR.lproj/Localizable.strings # Session/Meta/Translations/ru.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/sk.lproj/Localizable.strings # Session/Meta/Translations/sv.lproj/Localizable.strings # Session/Meta/Translations/th.lproj/Localizable.strings # Session/Meta/Translations/vi-VN.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Meta/Translations/zh_CN.lproj/Localizable.strings # Session/Open Groups/JoinOpenGroupVC.swift # Session/Open Groups/OpenGroupSuggestionGrid.swift # Session/Settings/SettingsVC.swift # Session/Shared/BaseVC.swift # Session/Shared/OWSQRCodeScanningViewController.m # Session/Shared/ScanQRCodeWrapperVC.swift # Session/Shared/UserCell.swift # SessionMessagingKit/Configuration.swift # SessionShareExtension/SAEScreenLockViewController.swift # SessionUIKit/Style Guide/Gradients.swift # SignalUtilitiesKit/Media Viewing & Editing/OWSViewController+ImageEditor.swift # SignalUtilitiesKit/Screen Lock/ScreenLockViewController.m
2022-09-26 03:16:47 +02:00
let baseColor: UIColor = (ThemeManager.currentTheme.color(for: .messageBubble_incomingText) ?? .white)
2018-11-01 18:53:58 +01:00
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
2019-02-06 22:00:22 +01:00
let dotColor = baseColor.withAlphaComponent(CGFloatLerp(0.4, 1.0, CGFloatClamp01(progress)))
2018-11-01 18:53:58 +01:00
colorValues.append(dotColor.cgColor)
2019-02-06 22:00:22 +01:00
let radius = CGFloatLerp(TypingIndicatorView.kMinRadiusPt, TypingIndicatorView.kMaxRadiusPt, CGFloatClamp01(progress))
2018-11-01 18:53:58 +01:00
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)
}
2018-11-01 15:43:13 +01:00
2018-11-01 18:53:58 +01:00
fileprivate func stopAnimation() {
shapeLayer.removeAllAnimations()
2018-11-01 15:43:13 +01:00
}
}
}