#4096 - Added locked mode transition and animations, locked recording mode and real time waveform.

This commit is contained in:
Stefan Ceriu 2021-06-18 15:49:58 +03:00
parent 99eb2d16ac
commit 3130fd5257
21 changed files with 504 additions and 156 deletions

View file

@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "voice_message_lock_chevron.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "voice_message_lock_chevron@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "voice_message_lock_chevron@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "voice_message_lock_icon_locked.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "voice_message_lock_icon_locked@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "voice_message_lock_icon_locked@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "voice_message_lock_icon_unlocked.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "voice_message_lock_icon_unlocked@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "voice_message_lock_icon_unlocked@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -131,6 +131,9 @@ internal enum Asset {
internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon")
internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon")
internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient")
internal static let voiceMessageLockChevron = ImageAsset(name: "voice_message_lock_chevron")
internal static let voiceMessageLockIconLocked = ImageAsset(name: "voice_message_lock_icon_locked")
internal static let voiceMessageLockIconUnlocked = ImageAsset(name: "voice_message_lock_icon_unlocked")
internal static let voiceMessagePauseButtonDark = ImageAsset(name: "voice_message_pause_button_dark")
internal static let voiceMessagePauseButtonLight = ImageAsset(name: "voice_message_pause_button_light")
internal static let voiceMessagePlayButtonDark = ImageAsset(name: "voice_message_play_button_dark")

View file

@ -17,6 +17,8 @@
import Foundation
import AVFoundation
private let silenceThreshold: Float = -50.0
protocol VoiceMessageAudioRecorderDelegate: AnyObject {
func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder)
func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder)
@ -104,11 +106,7 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate {
}
private func normalizedPowerLevelFromDecibels(_ decibels: Float) -> Float {
if decibels < -60.0 || decibels == 0.0 {
return 0.0
}
return powf((powf(10.0, 0.05 * decibels) - powf(10.0, 0.05 * -60.0)) * (1.0 / (1.0 - powf(10.0, 0.05 * -60.0))), 1.0 / 2.0)
return decibels / silenceThreshold
}
}

View file

@ -31,6 +31,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
private var audioRecorder: VoiceMessageAudioRecorder?
private var audioSamples: [Float] = []
private var isInLockedMode: Bool = false
@objc public weak var delegate: VoiceMessageControllerDelegate?
@objc public var voiceMessageToolbarView: UIView {
@ -54,6 +57,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
self._voiceMessageToolbarView.update(theme: self.themeService.theme)
NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil)
updateUI()
}
// MARK: - VoiceMessageToolbarViewDelegate
@ -87,27 +92,32 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
}
func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) {
isInLockedMode = false
audioRecorder?.stopRecording()
deleteRecordingAtURL(audioRecorder?.url)
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) {
isInLockedMode = true
updateUI()
}
// MARK: - AudioRecorderDelegate
func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) {
_voiceMessageToolbarView.state = .recording
self.displayLink.isPaused = false
updateUI()
}
func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) {
_voiceMessageToolbarView.state = .idle
displayLink.isPaused = true
updateUI()
}
func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) {
isInLockedMode = false
updateUI()
MXLog.error("Failed recording voice message.")
_voiceMessageToolbarView.state = .idle
displayLink.isPaused = true
}
// MARK: - Private
@ -129,10 +139,27 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
}
@objc private func handleDisplayLinkTick() {
guard let audioRecorder = audioRecorder else {
return
updateUI()
}
private func updateUI() {
displayLink.isPaused = !(audioRecorder?.isRecording ?? false)
let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples()
if audioSamples.count != requiredNumberOfSamples {
audioSamples = [Float](repeating: 0.0, count: requiredNumberOfSamples)
}
_voiceMessageToolbarView.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioRecorder.currentTime))
if let sample = audioRecorder?.averagePowerForChannelNumber(0) {
audioSamples.append(sample)
audioSamples.remove(at: 0)
}
var details = VoiceMessageToolbarViewDetails()
details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle))
details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioRecorder?.currentTime ?? 0.0))
details.audioSamples = audioSamples
_voiceMessageToolbarView.configureWithDetails(details)
}
}

View file

@ -26,6 +26,7 @@ struct VoiceMessagePlaybackViewDetails {
var samples: [Float] = []
var playing: Bool = false
var playbackEnabled = false
var recording: Bool = false
}
class VoiceMessagePlaybackView: UIView {
@ -33,6 +34,7 @@ class VoiceMessagePlaybackView: UIView {
private var waveformView: VoiceMessageWaveformView!
@IBOutlet private var backgroundView: UIView!
@IBOutlet private var recordingIcon: UIView!
@IBOutlet private var playButton: UIButton!
@IBOutlet private var elapsedTimeLabel: UILabel!
@IBOutlet private var waveformContainerView: UIView!
@ -66,6 +68,8 @@ class VoiceMessagePlaybackView: UIView {
}
playButton.isEnabled = details.playbackEnabled
playButton.isHidden = details.recording
recordingIcon.isHidden = !details.recording
elapsedTimeLabel.text = details.currentTime
waveformView.progress = details.progress

View file

@ -21,44 +21,48 @@
<constraint firstAttribute="height" priority="999" constant="44" id="RFF-Im-d7x"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eAi-HM-Wvj">
<rect key="frame" x="44" y="0.0" width="40" height="44"/>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="ZQ2-Ij-mYr">
<rect key="frame" x="8" y="0.0" width="411" height="44"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_record_icon" translatesAutoresizingMaskIntoConstraints="NO" id="REB-gl-h0h">
<rect key="frame" x="0.0" y="17" width="10" height="10"/>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GL1-b8-dZK">
<rect key="frame" x="14" y="6" width="32" height="32"/>
<state key="normal" image="voice_message_play_button_light"/>
<connections>
<action selector="onPlayButtonTap" destination="cGR-49-HWB" eventType="touchUpInside" id="B5j-st-pUp"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eAi-HM-Wvj">
<rect key="frame" x="50" y="0.0" width="40" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="iuv-MD-XYg"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7Fl-yZ-dZB">
<rect key="frame" x="94" y="7" width="317" height="30"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="40" id="iuv-MD-XYg"/>
<constraint firstItem="7Fl-yZ-dZB" firstAttribute="height" secondItem="ZQ2-Ij-mYr" secondAttribute="height" constant="-14" id="PiL-fv-hP1"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GL1-b8-dZK">
<rect key="frame" x="8" y="6" width="32" height="32"/>
<state key="normal" image="voice_message_play_button_light"/>
<connections>
<action selector="onPlayButtonTap" destination="cGR-49-HWB" eventType="touchUpInside" id="B5j-st-pUp"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7Fl-yZ-dZB">
<rect key="frame" x="88" y="7" width="331" height="30"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="Ugy-Dx-gcs"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ugy-Dx-gcs" firstAttribute="trailing" secondItem="LPc-i8-8UC" secondAttribute="trailing" id="2AH-VU-Kcc"/>
<constraint firstItem="7Fl-yZ-dZB" firstAttribute="top" secondItem="cGR-49-HWB" secondAttribute="top" constant="7" id="4wl-W5-P44"/>
<constraint firstItem="eAi-HM-Wvj" firstAttribute="leading" secondItem="GL1-b8-dZK" secondAttribute="trailing" constant="4" id="89L-2g-RhG"/>
<constraint firstAttribute="bottom" secondItem="ZQ2-Ij-mYr" secondAttribute="bottom" id="BSe-tM-f0V"/>
<constraint firstItem="LPc-i8-8UC" firstAttribute="leading" secondItem="Ugy-Dx-gcs" secondAttribute="leading" id="FnY-Ab-FVL"/>
<constraint firstAttribute="bottom" secondItem="eAi-HM-Wvj" secondAttribute="bottom" id="K3Q-rl-3zD"/>
<constraint firstItem="7Fl-yZ-dZB" firstAttribute="leading" secondItem="eAi-HM-Wvj" secondAttribute="trailing" constant="4" id="KUM-Pg-Ume"/>
<constraint firstItem="GL1-b8-dZK" firstAttribute="leading" secondItem="cGR-49-HWB" secondAttribute="leading" constant="8" id="RIG-A2-5bp"/>
<constraint firstItem="Ugy-Dx-gcs" firstAttribute="trailing" secondItem="7Fl-yZ-dZB" secondAttribute="trailing" constant="8" id="TRa-VB-QEc"/>
<constraint firstItem="GL1-b8-dZK" firstAttribute="centerY" secondItem="cGR-49-HWB" secondAttribute="centerY" id="TW1-ng-w6C"/>
<constraint firstAttribute="bottom" secondItem="7Fl-yZ-dZB" secondAttribute="bottom" constant="7" id="Zx4-eB-H2q"/>
<constraint firstItem="ZQ2-Ij-mYr" firstAttribute="top" secondItem="cGR-49-HWB" secondAttribute="top" id="KRu-5w-kGE"/>
<constraint firstAttribute="bottom" secondItem="LPc-i8-8UC" secondAttribute="bottom" id="apf-b1-yIb"/>
<constraint firstItem="eAi-HM-Wvj" firstAttribute="top" secondItem="cGR-49-HWB" secondAttribute="top" id="jxd-Mq-ASx"/>
<constraint firstAttribute="bottom" secondItem="GL1-b8-dZK" secondAttribute="bottom" constant="6" id="nzi-5c-92a"/>
<constraint firstItem="GL1-b8-dZK" firstAttribute="top" secondItem="cGR-49-HWB" secondAttribute="top" constant="6" id="zCt-aS-zv4"/>
<constraint firstItem="ZQ2-Ij-mYr" firstAttribute="leading" secondItem="cGR-49-HWB" secondAttribute="leading" constant="8" id="fDO-rh-Jbl"/>
<constraint firstAttribute="trailing" secondItem="ZQ2-Ij-mYr" secondAttribute="trailing" constant="8" id="fM3-nY-rDV"/>
<constraint firstItem="LPc-i8-8UC" firstAttribute="top" secondItem="cGR-49-HWB" secondAttribute="top" id="zl5-Sf-qSF"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
@ -68,6 +72,7 @@
<outlet property="backgroundView" destination="LPc-i8-8UC" id="mfD-md-nTj"/>
<outlet property="elapsedTimeLabel" destination="eAi-HM-Wvj" id="z70-aJ-O90"/>
<outlet property="playButton" destination="GL1-b8-dZK" id="5u7-CG-d99"/>
<outlet property="recordingIcon" destination="REB-gl-h0h" id="uL1-nI-bhF"/>
<outlet property="waveformContainerView" destination="7Fl-yZ-dZB" id="f9u-wS-jvG"/>
</connections>
<point key="canvasLocation" x="-1742.753623188406" y="-299.33035714285711"/>
@ -75,5 +80,6 @@
</objects>
<resources>
<image name="voice_message_play_button_light" width="32" height="32"/>
<image name="voice_message_record_icon" width="10" height="10"/>
</resources>
</document>

View file

@ -20,16 +20,27 @@ protocol VoiceMessageToolbarViewDelegate: AnyObject {
func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView)
func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView)
}
enum VoiceMessageToolbarViewUIState {
case idle
case recording
case record
case lockedModeRecord
case lockedModePlayback
}
struct VoiceMessageToolbarViewDetails {
var state: VoiceMessageToolbarViewUIState = .idle
var elapsedTime: String = ""
var audioSamples: [Float] = []
}
class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate {
@IBOutlet private var backgroundView: UIView!
@IBOutlet private var recordingContainerView: UIView!
@IBOutlet private var recordButtonsContainerView: UIView!
@IBOutlet private var primaryRecordButton: UIButton!
@IBOutlet private var secondaryRecordButton: UIButton!
@ -37,43 +48,40 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
@IBOutlet private var recordingChromeContainerView: UIView!
@IBOutlet private var recordingIndicatorView: UIView!
@IBOutlet private var elapsedTimeLabel: UILabel!
@IBOutlet private var slideToCancelContainerView: UIView!
@IBOutlet private var slideToCancelLabel: UILabel!
@IBOutlet private var slideToCancelChevron: UIImageView!
@IBOutlet private var slideToCancelGradient: UIImageView!
@IBOutlet private var elapsedTimeLabel: UILabel!
@IBOutlet private var lockContainerView: UIView!
@IBOutlet private var lockContainerBackgroundView: UIView!
@IBOutlet private var primaryLockButton: UIButton!
@IBOutlet private var secondaryLockButton: UIButton!
@IBOutlet private var lockChevron: UIView!
@IBOutlet private var lockedModeContainerView: UIView!
@IBOutlet private var deleteButton: UIButton!
@IBOutlet private var playbackViewContainerView: UIView!
@IBOutlet private var sendButton: UIButton!
private var playbackView: VoiceMessagePlaybackView!
private var cancelLabelToRecordButtonDistance: CGFloat = 0.0
private var lockChevronToRecordButtonDistance: CGFloat = 0.0
private var lockChevronToLockButtonDistance: CGFloat = 0.0
private var panDirection: UISwipeGestureRecognizer.Direction?
private var details: VoiceMessageToolbarViewDetails?
private var currentTheme: Theme? {
didSet {
updateUIAnimated(true)
updateUIWithDetails(details, animated: true)
}
}
weak var delegate: VoiceMessageToolbarViewDelegate?
var state: VoiceMessageToolbarViewUIState = .idle {
didSet {
switch state {
case .recording:
let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView)
cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX
startAnimatingRecordingIndicator()
case .idle:
cancelDrag()
}
updateUIAnimated(true)
}
}
var elapsedTime: String? {
didSet {
elapsedTimeLabel.text = elapsedTime
}
}
@objc static func instanceFromNib() -> VoiceMessageToolbarView {
let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil)
@ -87,6 +95,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
super.awakeFromNib()
slideToCancelGradient.image = Asset.Images.voiceMessageCancelGradient.image.withRenderingMode(.alwaysTemplate)
lockContainerBackgroundView.layer.cornerRadius = lockContainerBackgroundView.bounds.width / 2.0
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressGesture.delegate = self
@ -97,7 +106,52 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
longPressGesture.delegate = self
recordButtonsContainerView.addGestureRecognizer(panGesture)
updateUIAnimated(false)
playbackView = VoiceMessagePlaybackView.instanceFromNib()
playbackViewContainerView.vc_addSubViewMatchingParent(playbackView)
updateUIWithDetails(VoiceMessageToolbarViewDetails(), animated: false)
}
func configureWithDetails(_ details: VoiceMessageToolbarViewDetails) {
elapsedTimeLabel.text = details.elapsedTime
UIView.animate(withDuration: 0.25) {
self.updatePlaybackViewWithDetails(details)
}
if self.details?.state != details.state {
switch details.state {
case .record:
var convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView)
cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX
convertedFrame = self.convert(lockChevron.frame, from: lockContainerView)
lockChevronToRecordButtonDistance = recordButtonsContainerView.frame.midY + convertedFrame.maxY
lockChevronToLockButtonDistance = lockChevron.frame.minY - primaryLockButton.frame.midY
startAnimatingRecordingIndicator()
default:
cancelDrag()
}
if details.state == .lockedModeRecord && self.details?.state == .record {
UIView.animate(withDuration: 0.25) {
self.secondaryLockButton.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
self.secondaryLockButton.alpha = 0.0
} completion: { _ in
self.updateUIWithDetails(details, animated: true)
}
} else {
updateUIWithDetails(details, animated: true)
}
}
self.details = details
}
func getRequiredNumberOfSamples() -> Int {
return playbackView.getRequiredNumberOfSamples()
}
// MARK: - Themable
@ -119,8 +173,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
case UIGestureRecognizer.State.began:
delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self)
case UIGestureRecognizer.State.ended:
delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self)
case UIGestureRecognizer.State.cancelled:
// delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self)
delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self)
default:
break
@ -128,17 +181,49 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
}
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard self.state == .recording && gestureRecognizer.state == .changed else {
guard details?.state == .record && gestureRecognizer.state == .changed else {
return
}
let translation = gestureRecognizer.translation(in: self)
secondaryRecordButton.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0)
slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0)
if abs(translation.x) <= 20.0 && abs(translation.y) <= 20.0 {
panDirection = nil
} else if panDirection == nil {
if abs(translation.x) >= abs(translation.y) {
panDirection = .left
} else {
panDirection = .up
}
}
if abs(translation.x - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 {
cancelDrag()
if panDirection == .left {
secondaryRecordButton.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0)
slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0)
if abs(translation.x - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 {
delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self)
}
} else if panDirection == .up {
secondaryRecordButton.transform = CGAffineTransform(translationX: 0.0, y: min(0.0, translation.y))
let yTranslation = min(max(translation.y + lockChevronToRecordButtonDistance, -lockChevronToLockButtonDistance), 0.0)
lockChevron.transform = CGAffineTransform(translationX: 0.0, y: yTranslation)
let transitionPercentage = abs(yTranslation) / lockChevronToLockButtonDistance
lockChevron.alpha = 1.0 - transitionPercentage
secondaryRecordButton.alpha = 1.0 - transitionPercentage
primaryLockButton.alpha = 1.0 - transitionPercentage
lockContainerBackgroundView.alpha = 1.0 - transitionPercentage
secondaryLockButton.alpha = transitionPercentage
if transitionPercentage >= 1.0 {
self.delegate?.voiceMessageToolbarViewDidRequestLockedModeRecording(self)
}
} else {
secondaryRecordButton.transform = CGAffineTransform(translationX: min(0.0, translation.x), y: min(0.0, translation.y))
}
}
@ -149,19 +234,42 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
}
}
private func updateUIAnimated(_ animated: Bool) {
UIView.animate(withDuration: (animated ? 0.25 : 0.0)) {
switch self.state {
case .idle:
self.backgroundView.alpha = 0.0
self.primaryRecordButton.alpha = 1.0
self.secondaryRecordButton.alpha = 0.0
self.recordingChromeContainerView.alpha = 0.0
case .recording:
private func updateUIWithDetails(_ details: VoiceMessageToolbarViewDetails?, animated: Bool) {
guard let details = details else {
return
}
UIView.animate(withDuration: (animated ? 0.25 : 0.0), delay: 0.0, options: .beginFromCurrentState) {
switch details.state {
case .record:
self.backgroundView.alpha = 1.0
self.primaryRecordButton.alpha = 0.0
self.secondaryRecordButton.alpha = 1.0
self.recordingChromeContainerView.alpha = 1.0
self.lockContainerView.alpha = 1.0
self.lockContainerBackgroundView.alpha = 1.0
self.lockedModeContainerView.alpha = 0.0
self.recordingContainerView.alpha = 1.0
case .lockedModeRecord:
self.backgroundView.alpha = 1.0
self.primaryRecordButton.alpha = 0.0
self.secondaryRecordButton.alpha = 0.0
self.recordingChromeContainerView.alpha = 0.0
self.lockContainerView.alpha = 0.0
self.lockedModeContainerView.alpha = 1.0
self.recordingContainerView.alpha = 0.0
default:
self.backgroundView.alpha = 0.0
self.primaryRecordButton.alpha = 1.0
self.secondaryRecordButton.alpha = 0.0
self.recordingChromeContainerView.alpha = 0.0
self.lockContainerView.alpha = 0.0
self.lockContainerBackgroundView.alpha = 1.0
self.primaryLockButton.alpha = 1.0
self.secondaryLockButton.alpha = 0.0
self.lockChevron.alpha = 1.0
self.lockedModeContainerView.alpha = 0.0
self.recordingContainerView.alpha = 1.0
}
guard let theme = self.currentTheme else {
@ -176,18 +284,30 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
self.slideToCancelChevron.tintColor = theme.textSecondaryColor
self.elapsedTimeLabel.textColor = theme.textSecondaryColor
} completion: { _ in
switch self.state {
switch details.state {
case .idle:
self.secondaryRecordButton.transform = .identity
self.slideToCancelContainerView.transform = .identity
self.lockChevron.transform = .identity
self.secondaryLockButton.transform = .identity
default:
break
}
}
}
private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails) {
var playbackViewDetails = VoiceMessagePlaybackViewDetails()
playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord)
playbackViewDetails.currentTime = details.elapsedTime
playbackViewDetails.samples = details.audioSamples
playbackViewDetails.playbackEnabled = true
playbackViewDetails.progress = 0.0
playbackView.configureWithDetails(playbackViewDetails)
}
private func startAnimatingRecordingIndicator() {
if self.state != .recording {
if self.details?.state != .record {
return
}
@ -202,4 +322,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
}
}
@IBAction private func onTrashButtonTap(_ sender: UIBarItem) {
delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self)
}
@IBAction private func onSendButtonTap(_ sender: UIBarItem) {
delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self)
}
}

View file

@ -11,121 +11,236 @@
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="VoiceMessageToolbarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FqE-3x-NQ9">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dyu-ha-046" customClass="PassthroughView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XRB-CY-ijK">
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="6FH-4Q-Z5e">
<rect key="frame" x="140" y="10" width="134.5" height="20.5"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8fP-9K-WTa">
<rect key="frame" x="492" y="-90" width="44" height="152"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="chevron.left" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="82A-vC-KEp">
<rect key="frame" x="0.0" y="2" width="12.5" height="17"/>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kvc-OZ-peC">
<rect key="frame" x="0.0" y="0.0" width="44" height="152"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.95686274510000002" green="0.97647058819999999" blue="0.99215686270000003" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YF2-5s-q5S">
<rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
<state key="normal" image="voice_message_lock_icon_unlocked"/>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vm7-e1-VJ8">
<rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
<state key="normal" image="voice_message_lock_icon_locked"/>
</button>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_lock_chevron" translatesAutoresizingMaskIntoConstraints="NO" id="c8y-xb-2nh">
<rect key="frame" x="0.0" y="64" width="44" height="24"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Slide to cancel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ydw-Nb-zP6">
<rect key="frame" x="20.5" y="0.0" width="114" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_cancel_gradient" translatesAutoresizingMaskIntoConstraints="NO" id="BYJ-HN-opT">
<rect key="frame" x="0.0" y="0.0" width="165.5" height="40"/>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="K6L-me-5EJ">
<rect key="frame" x="20" y="-5" width="64" height="50"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="YF2-5s-q5S" firstAttribute="top" secondItem="8fP-9K-WTa" secondAttribute="top" id="62w-uz-1IT"/>
<constraint firstItem="YF2-5s-q5S" firstAttribute="leading" secondItem="8fP-9K-WTa" secondAttribute="leading" id="6sU-56-2eT"/>
<constraint firstAttribute="trailing" secondItem="c8y-xb-2nh" secondAttribute="trailing" id="7HA-jr-fUD"/>
<constraint firstItem="c8y-xb-2nh" firstAttribute="centerY" secondItem="8fP-9K-WTa" secondAttribute="centerY" id="9x0-mO-M0V"/>
<constraint firstItem="c8y-xb-2nh" firstAttribute="leading" secondItem="8fP-9K-WTa" secondAttribute="leading" id="U4g-Vq-hJB"/>
<constraint firstAttribute="width" constant="44" id="iwn-h5-ilH"/>
<constraint firstAttribute="height" constant="152" id="li1-Bd-px2"/>
<constraint firstItem="vm7-e1-VJ8" firstAttribute="centerY" secondItem="YF2-5s-q5S" secondAttribute="centerY" id="voJ-BE-HrY"/>
<constraint firstItem="vm7-e1-VJ8" firstAttribute="centerX" secondItem="YF2-5s-q5S" secondAttribute="centerX" id="xKW-V1-MVd"/>
<constraint firstAttribute="trailing" secondItem="YF2-5s-q5S" secondAttribute="trailing" id="zLG-ji-Sqy"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dyu-ha-046" customClass="PassthroughView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_record_icon" translatesAutoresizingMaskIntoConstraints="NO" id="miF-pM-B9J">
<rect key="frame" x="0.0" y="0.0" width="10" height="50"/>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="6FH-4Q-Z5e">
<rect key="frame" x="205" y="26" width="134.5" height="20.5"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="chevron.left" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="82A-vC-KEp">
<rect key="frame" x="0.0" y="2" width="12.5" height="17"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Slide to cancel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ydw-Nb-zP6">
<rect key="frame" x="20.5" y="0.0" width="114" height="20.5"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_cancel_gradient" translatesAutoresizingMaskIntoConstraints="NO" id="BYJ-HN-opT">
<rect key="frame" x="0.0" y="0.0" width="217.5" height="72"/>
<color key="tintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QBp-TZ-h5s">
<rect key="frame" x="14" y="0.0" width="50" height="50"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="K6L-me-5EJ">
<rect key="frame" x="20" y="11" width="64" height="50"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_record_icon" translatesAutoresizingMaskIntoConstraints="NO" id="miF-pM-B9J">
<rect key="frame" x="0.0" y="0.0" width="10" height="50"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QBp-TZ-h5s">
<rect key="frame" x="14" y="0.0" width="50" height="50"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
</stackView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="K6L-me-5EJ" firstAttribute="leading" secondItem="dyu-ha-046" secondAttribute="leading" constant="20" id="0CB-EV-XDb"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="top" secondItem="dyu-ha-046" secondAttribute="top" id="3Mq-1a-iKc"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="width" secondItem="dyu-ha-046" secondAttribute="width" multiplier="0.4" id="4R3-5v-p6s"/>
<constraint firstItem="K6L-me-5EJ" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="Eyq-fW-20D"/>
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerX" secondItem="dyu-ha-046" secondAttribute="centerX" id="IZ1-Dr-yrw"/>
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="NAu-5j-4Yg"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="leading" secondItem="dyu-ha-046" secondAttribute="leading" id="lXc-5e-Ssj"/>
<constraint firstAttribute="bottom" secondItem="BYJ-HN-opT" secondAttribute="bottom" id="yNQ-wC-4iD"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7OQ-1F-5qT">
<rect key="frame" x="488" y="10" width="52" height="52"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BDj-Sw-VQ5">
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
<state key="normal" image="voice_message_record_button_default"/>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rel-Fo-ROL">
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" image="voice_message_record_button_recording"/>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="2Xv-EI-etf"/>
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="2ZQ-3v-0W7"/>
<constraint firstAttribute="height" constant="52" id="4XA-Gb-5NO"/>
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="Dki-cT-7xX"/>
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="fzv-iX-c1Y"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="mNa-EU-ZKQ"/>
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="phX-gD-B2H"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="pv8-li-wP8"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="ynJ-4x-1jv"/>
<constraint firstAttribute="width" constant="52" id="zPb-1B-JyA"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="K6L-me-5EJ" firstAttribute="leading" secondItem="dyu-ha-046" secondAttribute="leading" constant="20" id="0CB-EV-XDb"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="top" secondItem="dyu-ha-046" secondAttribute="top" id="3Mq-1a-iKc"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="width" secondItem="dyu-ha-046" secondAttribute="width" multiplier="0.4" id="4R3-5v-p6s"/>
<constraint firstItem="K6L-me-5EJ" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="Eyq-fW-20D"/>
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerX" secondItem="dyu-ha-046" secondAttribute="centerX" id="IZ1-Dr-yrw"/>
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="NAu-5j-4Yg"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="leading" secondItem="dyu-ha-046" secondAttribute="leading" id="lXc-5e-Ssj"/>
<constraint firstAttribute="bottom" secondItem="BYJ-HN-opT" secondAttribute="bottom" id="yNQ-wC-4iD"/>
<constraint firstItem="dyu-ha-046" firstAttribute="leading" secondItem="XRB-CY-ijK" secondAttribute="leading" id="BoC-Ut-chI"/>
<constraint firstAttribute="bottom" secondItem="dyu-ha-046" secondAttribute="bottom" id="U4h-FY-D3W"/>
<constraint firstItem="8fP-9K-WTa" firstAttribute="bottom" secondItem="7OQ-1F-5qT" secondAttribute="bottom" id="X4v-7T-LgP"/>
<constraint firstAttribute="trailing" secondItem="7OQ-1F-5qT" secondAttribute="trailing" constant="4" id="giC-4J-EUL"/>
<constraint firstItem="dyu-ha-046" firstAttribute="top" secondItem="XRB-CY-ijK" secondAttribute="top" id="ra2-Me-23b"/>
<constraint firstItem="8fP-9K-WTa" firstAttribute="centerX" secondItem="7OQ-1F-5qT" secondAttribute="centerX" id="xL5-g3-aHb"/>
<constraint firstAttribute="trailing" secondItem="dyu-ha-046" secondAttribute="trailing" id="xME-WZ-OMX"/>
<constraint firstItem="7OQ-1F-5qT" firstAttribute="centerY" secondItem="XRB-CY-ijK" secondAttribute="centerY" id="yLc-Ke-vBU"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7OQ-1F-5qT">
<rect key="frame" x="358" y="-6" width="52" height="52"/>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pkc-LT-lE6">
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BDj-Sw-VQ5">
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
<state key="normal" image="voice_message_record_button_default"/>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rel-Fo-ROL">
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" image="voice_message_record_button_recording"/>
</button>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="wL2-0Z-cvF">
<rect key="frame" x="4" y="0.0" width="532" height="72"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="U4V-EC-Ffy">
<rect key="frame" x="0.0" y="14" width="44" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="44" id="rXL-fN-mn1"/>
<constraint firstAttribute="height" constant="44" id="sMv-uS-G8f"/>
</constraints>
<color key="tintColor" red="0.55686274509803924" green="0.59999999999999998" blue="0.64313725490196072" alpha="1" colorSpace="calibratedRGB"/>
<state key="normal" image="room_context_menu_delete"/>
<connections>
<action selector="onTrashButtonTap:" destination="iN0-l3-epB" eventType="touchUpInside" id="G3W-VG-evO"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RWp-zw-zVq">
<rect key="frame" x="60" y="14" width="412" height="44"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="H6t-Lp-spE"/>
</constraints>
</view>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UuF-HN-cAU">
<rect key="frame" x="488" y="14" width="44" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="44" id="HKq-XS-LDC"/>
<constraint firstAttribute="height" constant="44" id="ZuT-pR-osp"/>
</constraints>
<state key="normal" image="send_icon"/>
<connections>
<action selector="onSendButtonTap:" destination="iN0-l3-epB" eventType="touchUpInside" id="8IQ-s2-AnY"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="2Xv-EI-etf"/>
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="2ZQ-3v-0W7"/>
<constraint firstAttribute="height" constant="52" id="4XA-Gb-5NO"/>
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="Dki-cT-7xX"/>
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="fzv-iX-c1Y"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="mNa-EU-ZKQ"/>
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="phX-gD-B2H"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="pv8-li-wP8"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="ynJ-4x-1jv"/>
<constraint firstAttribute="width" constant="52" id="zPb-1B-JyA"/>
<constraint firstItem="wL2-0Z-cvF" firstAttribute="top" secondItem="pkc-LT-lE6" secondAttribute="top" id="2Na-3x-Ri6"/>
<constraint firstAttribute="trailing" secondItem="wL2-0Z-cvF" secondAttribute="trailing" constant="8" id="7oK-QU-5uP"/>
<constraint firstAttribute="bottom" secondItem="wL2-0Z-cvF" secondAttribute="bottom" id="IKw-iw-tWg"/>
<constraint firstItem="wL2-0Z-cvF" firstAttribute="leading" secondItem="pkc-LT-lE6" secondAttribute="leading" constant="4" id="cG3-Fr-Auu"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dyu-ha-046" secondAttribute="trailing" id="4OH-8A-tMK"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="7OQ-1F-5qT" secondAttribute="trailing" constant="4" id="8x0-jm-gub"/>
<constraint firstAttribute="bottom" secondItem="dyu-ha-046" secondAttribute="bottom" id="L4o-hW-kta"/>
<constraint firstItem="dyu-ha-046" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="Qmv-hs-X3c"/>
<constraint firstItem="dyu-ha-046" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="Tdp-61-WP9"/>
<constraint firstItem="7OQ-1F-5qT" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="t9O-KA-rTy"/>
<constraint firstAttribute="trailing" secondItem="XRB-CY-ijK" secondAttribute="trailing" id="Utk-t1-anP"/>
<constraint firstAttribute="trailing" secondItem="pkc-LT-lE6" secondAttribute="trailing" id="VNU-5V-O6I"/>
<constraint firstAttribute="bottom" secondItem="XRB-CY-ijK" secondAttribute="bottom" id="VT1-7g-OYr"/>
<constraint firstItem="XRB-CY-ijK" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="VWe-l9-ZqO"/>
<constraint firstItem="pkc-LT-lE6" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="X9R-lc-F52"/>
<constraint firstAttribute="top" secondItem="XRB-CY-ijK" secondAttribute="top" id="XRb-zW-xdf"/>
<constraint firstItem="pkc-LT-lE6" firstAttribute="bottom" secondItem="iN0-l3-epB" secondAttribute="bottom" id="ppT-PL-6Jg"/>
<constraint firstAttribute="top" secondItem="pkc-LT-lE6" secondAttribute="top" id="tEJ-94-MLM"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="backgroundView" destination="FqE-3x-NQ9" id="RFR-SQ-s21"/>
<outlet property="deleteButton" destination="U4V-EC-Ffy" id="Op3-oN-2vG"/>
<outlet property="elapsedTimeLabel" destination="QBp-TZ-h5s" id="qC9-BQ-8RA"/>
<outlet property="lockChevron" destination="c8y-xb-2nh" id="p6S-mB-C1U"/>
<outlet property="lockContainerBackgroundView" destination="kvc-OZ-peC" id="ke4-gM-LQV"/>
<outlet property="lockContainerView" destination="8fP-9K-WTa" id="mFH-Va-74i"/>
<outlet property="lockedModeContainerView" destination="pkc-LT-lE6" id="bbY-iP-th3"/>
<outlet property="playbackViewContainerView" destination="RWp-zw-zVq" id="X0h-z8-9CA"/>
<outlet property="primaryLockButton" destination="YF2-5s-q5S" id="zsO-cM-wBY"/>
<outlet property="primaryRecordButton" destination="BDj-Sw-VQ5" id="dg3-fG-Bym"/>
<outlet property="recordButtonsContainerView" destination="7OQ-1F-5qT" id="HDQ-r9-2Tu"/>
<outlet property="recordingChromeContainerView" destination="dyu-ha-046" id="u7O-Vb-T2W"/>
<outlet property="recordingContainerView" destination="XRB-CY-ijK" id="czS-WC-dqS"/>
<outlet property="recordingIndicatorView" destination="miF-pM-B9J" id="zNy-ms-awL"/>
<outlet property="secondaryLockButton" destination="vm7-e1-VJ8" id="XDw-nX-Aef"/>
<outlet property="secondaryRecordButton" destination="rel-Fo-ROL" id="KXM-gt-9hS"/>
<outlet property="sendButton" destination="UuF-HN-cAU" id="bBT-hM-c9E"/>
<outlet property="slideToCancelChevron" destination="82A-vC-KEp" id="Chg-EH-UBv"/>
<outlet property="slideToCancelContainerView" destination="6FH-4Q-Z5e" id="qCc-rl-vQX"/>
<outlet property="slideToCancelGradient" destination="BYJ-HN-opT" id="qbb-Q9-xSo"/>
<outlet property="slideToCancelLabel" destination="Ydw-Nb-zP6" id="l4Y-Eg-Qwc"/>
</connections>
<point key="canvasLocation" x="-84" y="234"/>
<point key="canvasLocation" x="10.144927536231885" y="456.69642857142856"/>
</view>
</objects>
<resources>
<image name="chevron.left" catalog="system" width="96" height="128"/>
<image name="room_context_menu_delete" width="24" height="24"/>
<image name="send_icon" width="36" height="36"/>
<image name="voice_message_cancel_gradient" width="104" height="47"/>
<image name="voice_message_lock_chevron" width="24" height="24"/>
<image name="voice_message_lock_icon_locked" width="44" height="44"/>
<image name="voice_message_lock_icon_unlocked" width="44" height="44"/>
<image name="voice_message_record_button_default" width="22" height="26.5"/>
<image name="voice_message_record_button_recording" width="52" height="52"/>
<image name="voice_message_record_icon" width="10" height="10"/>

View file

@ -56,11 +56,6 @@ class VoiceMessageWaveformView: UIView {
updateBarViews()
}
func addSample(_ sample: Float) {
samples.append(sample)
updateBarViews()
}
// MARK: - Private
private func setupBarViews() {