session-ios/Signal/src/Loki/Components/VoiceMessageView.swift

166 lines
6.0 KiB
Swift
Raw Normal View History

2020-09-30 06:39:56 +02:00
import Accelerate
2020-10-02 03:24:36 +02:00
@objc(LKVoiceMessageView)
final class VoiceMessageView : UIView {
2020-10-01 01:25:17 +02:00
private let voiceMessage: TSAttachment
2020-10-02 03:04:37 +02:00
private let isOutgoing: Bool
2020-09-30 06:39:56 +02:00
private var isAnimating = false
2020-10-01 01:25:17 +02:00
private var volumeSamples: [Float] = [] { didSet { updateShapeLayers() } }
private var progress: CGFloat = 0
2020-10-01 10:15:48 +02:00
@objc var duration: Int = 0 { didSet { updateDurationLabel() } }
2020-09-30 06:39:56 +02:00
// MARK: Components
private lazy var loader: UIView = {
let result = UIView()
result.backgroundColor = Colors.text.withAlphaComponent(0.2)
return result
}()
2020-10-01 10:15:48 +02:00
private lazy var durationLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.mediumFontSize)
return result
}()
2020-09-30 06:39:56 +02:00
private lazy var backgroundShapeLayer: CAShapeLayer = {
let result = CAShapeLayer()
result.fillColor = Colors.text.cgColor
return result
}()
private lazy var foregroundShapeLayer: CAShapeLayer = {
let result = CAShapeLayer()
2020-10-02 03:04:37 +02:00
result.fillColor = (isLightMode && isOutgoing) ? UIColor.white.cgColor : Colors.accent.cgColor
2020-09-30 06:39:56 +02:00
return result
}()
// MARK: Settings
2020-10-01 10:15:48 +02:00
private let vMargin: CGFloat = 0
2020-09-30 06:39:56 +02:00
private let sampleSpacing: CGFloat = 1
2020-10-01 10:15:48 +02:00
@objc public static let contentHeight: CGFloat = 32
2020-10-01 06:24:00 +02:00
2020-09-30 06:39:56 +02:00
// MARK: Initialization
2020-10-02 03:04:37 +02:00
@objc(initWithVoiceMessage:isOutgoing:)
init(voiceMessage: TSAttachment, isOutgoing: Bool) {
2020-10-01 01:25:17 +02:00
self.voiceMessage = voiceMessage
2020-10-02 03:04:37 +02:00
self.isOutgoing = isOutgoing
2020-09-30 06:39:56 +02:00
super.init(frame: CGRect.zero)
initialize()
}
override init(frame: CGRect) {
2020-10-01 01:25:17 +02:00
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
2020-09-30 06:39:56 +02:00
}
required init?(coder: NSCoder) {
2020-10-01 01:25:17 +02:00
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
2020-09-30 06:39:56 +02:00
}
private func initialize() {
setUpViewHierarchy()
2020-10-01 01:25:17 +02:00
if voiceMessage.isDownloaded {
loader.alpha = 0
guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
return print("[Loki] Couldn't get URL for voice message.")
}
2020-10-01 10:15:48 +02:00
let targetSampleCount = 48
if let cachedVolumeSamples = Storage.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
2020-10-01 06:24:00 +02:00
self.volumeSamples = cachedVolumeSamples
2020-10-01 01:25:17 +02:00
self.stopAnimating()
2020-10-01 06:24:00 +02:00
} else {
let voiceMessageID = voiceMessage.uniqueId!
2020-10-01 10:15:48 +02:00
AudioUtilities.getVolumeSamples(for: url, targetSampleCount: targetSampleCount).done(on: DispatchQueue.main) { [weak self] volumeSamples in
2020-10-01 06:24:00 +02:00
guard let self = self else { return }
self.volumeSamples = volumeSamples
Storage.write { transaction in
Storage.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
}
2020-10-01 10:15:48 +02:00
self.durationLabel.alpha = 1
2020-10-01 06:24:00 +02:00
self.stopAnimating()
}.catch(on: DispatchQueue.main) { error in
print("[Loki] Couldn't sample audio file due to error: \(error).")
}
2020-10-01 01:25:17 +02:00
}
} else {
2020-10-01 10:15:48 +02:00
durationLabel.alpha = 0
2020-10-01 01:25:17 +02:00
showLoader()
2020-09-30 06:39:56 +02:00
}
}
private func setUpViewHierarchy() {
set(.width, to: 200)
2020-10-02 03:24:36 +02:00
set(.height, to: VoiceMessageView.contentHeight)
2020-09-30 06:39:56 +02:00
addSubview(loader)
loader.pin(to: self)
layer.insertSublayer(backgroundShapeLayer, at: 0)
layer.insertSublayer(foregroundShapeLayer, at: 1)
2020-10-01 10:15:48 +02:00
addSubview(durationLabel)
durationLabel.center(.vertical, in: self)
durationLabel.pin(.trailing, to: .trailing, of: self)
2020-09-30 06:39:56 +02:00
}
2020-10-01 01:25:17 +02:00
// MARK: UI & Updating
2020-09-30 06:39:56 +02:00
private func showLoader() {
isAnimating = true
loader.alpha = 1
animateLoader()
}
private func animateLoader() {
2020-10-02 03:24:36 +02:00
loader.frame = CGRect(x: 0, y: 0, width: 0, height: VoiceMessageView.contentHeight)
2020-09-30 06:39:56 +02:00
UIView.animate(withDuration: 2) { [weak self] in
2020-10-02 03:24:36 +02:00
self?.loader.frame = CGRect(x: 0, y: 0, width: 200, height: VoiceMessageView.contentHeight)
2020-09-30 06:39:56 +02:00
} completion: { [weak self] _ in
guard let self = self else { return }
if self.isAnimating { self.animateLoader() }
}
}
private func stopAnimating() {
isAnimating = false
loader.alpha = 0
}
override func layoutSubviews() {
super.layoutSubviews()
2020-10-01 01:25:17 +02:00
updateShapeLayers()
}
2020-10-01 06:24:00 +02:00
@objc(updateForProgress:)
func update(for progress: CGFloat) {
2020-10-01 01:25:17 +02:00
self.progress = progress
updateShapeLayers()
2020-09-30 06:39:56 +02:00
}
2020-10-01 01:25:17 +02:00
private func updateShapeLayers() {
2020-09-30 06:39:56 +02:00
guard !volumeSamples.isEmpty else { return }
2020-10-01 10:15:48 +02:00
let sMin = CGFloat(volumeSamples.min()!)
let sMax = CGFloat(volumeSamples.max()!)
let w = width() - durationLabel.width() - Values.smallSpacing
let h = height() - 2 * vMargin
let sW = (w - sampleSpacing * CGFloat(volumeSamples.count - 1)) / CGFloat(volumeSamples.count)
2020-09-30 06:39:56 +02:00
let backgroundPath = UIBezierPath()
let foregroundPath = UIBezierPath()
for (i, value) in volumeSamples.enumerated() {
2020-10-01 10:15:48 +02:00
let x = CGFloat(i) * (sW + sampleSpacing)
let fraction = (CGFloat(value) - sMin) / (sMax - sMin)
let sH = max(8, h * fraction)
let y = vMargin + (h - sH) / 2
2020-09-30 06:39:56 +02:00
let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2)
backgroundPath.append(subPath)
2020-10-01 06:24:00 +02:00
if progress > CGFloat(i) / CGFloat(volumeSamples.count) { foregroundPath.append(subPath) }
2020-09-30 06:39:56 +02:00
}
backgroundPath.close()
foregroundPath.close()
backgroundShapeLayer.path = backgroundPath.cgPath
foregroundShapeLayer.path = foregroundPath.cgPath
}
2020-10-01 10:15:48 +02:00
private func updateDurationLabel() {
durationLabel.text = OWSFormat.formatDurationSeconds(duration)
updateShapeLayers()
}
2020-09-30 06:39:56 +02:00
}