From ac1bc1e8df39dcd585b9f76c7ba36ba1ace133bb Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 16:32:16 -0700 Subject: [PATCH 1/5] Add VolumeButtons class --- Signal.xcodeproj/project.pbxproj | 4 + Signal/src/util/VolumeButtons.swift | 247 ++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 Signal/src/util/VolumeButtons.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ede3fcd703..671f88d9f4 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -535,6 +535,7 @@ 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; 76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; }; + 8811CF842295D8DA00FF6549 /* VolumeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */; }; 954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954AEE681DF33D32002E5410 /* ContactsPickerTest.swift */; }; A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; @@ -1315,6 +1316,7 @@ 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; 76EB03C218170B33006006FC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtons.swift; sourceTree = ""; }; 8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = ""; }; 8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = ""; }; 948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = ""; }; @@ -2500,6 +2502,7 @@ 34E5DC8020D8050D00C08145 /* RegistrationUtils.h */, 34E5DC8120D8050D00C08145 /* RegistrationUtils.m */, 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */, + 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */, FCFA64B11A24F29E0007FB87 /* UI Categories */, ); path = util; @@ -3689,6 +3692,7 @@ 450D19131F85236600970622 /* RemoteVideoView.m in Sources */, 34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */, 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */, + 8811CF842295D8DA00FF6549 /* VolumeButtons.swift in Sources */, 45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */, 451166C01FD86B98000739BA /* AccountManager.swift in Sources */, 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */, diff --git a/Signal/src/util/VolumeButtons.swift b/Signal/src/util/VolumeButtons.swift new file mode 100644 index 0000000000..cf9beddcca --- /dev/null +++ b/Signal/src/util/VolumeButtons.swift @@ -0,0 +1,247 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +protocol VolumeButtonObserver: class { + func didPressVolumeButton(with identifier: VolumeButtons.Identifier) + func didReleaseVolumeButton(with identifier: VolumeButtons.Identifier) + + func didTapVolumeButton(with identifier: VolumeButtons.Identifier) + + func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) + func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) + func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) +} + +class VolumeButtons { + static let shared = VolumeButtons() + + enum Identifier { + case up, down + } + + private init?() { + // If for some reason the API we’re using goes away (for example, in + // a future iOS version) this class will never instantiate. + guard VolumeButtons.supportsListeningToEvents else { return nil } + } + + deinit { + stopObservation() + } + + // MARK: Observer Management + + private var observers: [Weak] = [] + func addObserver(observer: VolumeButtonObserver) { + AssertIsOnMainThread() + + if observers.firstIndex(where: { $0.value === observer }) == nil { + observers.append(Weak(value: observer)) + } + + guard !observers.isEmpty else { return } + startObservation() + } + + func removeObserver(_ observer: VolumeButtonObserver) { + AssertIsOnMainThread() + + observers = observers.filter { $0.value !== observer } + + guard observers.isEmpty else { return } + stopObservation() + } + + func removeAllObservers() { + AssertIsOnMainThread() + observers = [] + stopObservation() + } + + private func startObservation() { + guard !VolumeButtons.isRegisteredForEvents else { return } + VolumeButtons.isRegisteredForEvents = true + registerForNotifications() + } + + private func stopObservation() { + VolumeButtons.isRegisteredForEvents = false + unregisterForNotifications() + + defer { resetLongPress() } + guard let longPressingButton = longPressingButton else { return } + notifyObserversOfCancelLongPress(with: longPressingButton) + } + + private func notifyObserversOfTap(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didTapVolumeButton(with: identifier) + } + } + + private func notifyObserversOfBeginLongPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didBeginLongPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfCompleteLongPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didCompleteLongPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfCancelLongPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didCancelLongPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfRelease(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didReleaseVolumeButton(with: identifier) + } + } + + // MARK: Tap / long press handling + + private var longPressTimer: Timer? + private var longPressingButton: Identifier? + + // It's not possible for up and down to be pressed simulataneously + // (if you press the second button, the OS will end the press on + // the first), so it allows for simplified handling here. + private func beginButtonPress(with identifier: Identifier) { + longPressingButton = nil + + longPressTimer?.invalidate() + longPressTimer = WeakTimer.scheduledTimer( + timeInterval: longPressDuration, + target: self, + userInfo: nil, + repeats: false + ) { [weak self] _ in + self?.longPressingButton = identifier + self?.notifyObserversOfBeginLongPress(with: identifier) + self?.longPressTimer?.invalidate() + self?.longPressTimer = nil + } + + notifyObserversOfPress(with: identifier) + } + + private func endButtonPress(with identifier: Identifier) { + if longPressingButton == identifier { + notifyObserversOfCompleteLongPress(with: identifier) + } else { + notifyObserversOfTap(with: identifier) + } + + resetLongPress() + + notifyObserversOfRelease(with: identifier) + } + + private func resetLongPress() { + longPressTimer?.invalidate() + longPressTimer = nil + longPressingButton = nil + } + + // MARK: Volume Event Registration + + // let encodedSelectorString = "setWantsVolumeButtonEvents:".encodedForSelector + private static let volumeEventsSelector = Selector("BXYGaHIABgVnAX0HfnZTBwYGAQBWCHYABgVL".decodedForSelector!) + + private(set) static var isRegisteredForEvents = false { + didSet { + setEventRegistration(isRegisteredForEvents) + } + } + + private static func setEventRegistration(_ active: Bool) { + typealias Type = @convention(c) (AnyObject, Selector, Bool) -> Void + let implementation = class_getMethodImplementation(UIApplication.self, volumeEventsSelector) + let setRegistration = unsafeBitCast(implementation, to: Type.self) + setRegistration(UIApplication.shared, volumeEventsSelector, active) + } + + private static var supportsListeningToEvents: Bool { + return UIApplication.shared.responds(to: volumeEventsSelector) + } + + // MARK: Notification Handling + + // let encodedDownDownNotificationName = "_UIApplicationVolumeDownButtonDownNotification".encodedForSelector + private let downDownNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAVQEJAF8BBnp3enRyBnoBAA==".decodedForSelector!) + + // let encodedDownUpNotificationName = "_UIApplicationVolumeDownButtonUpNotification".encodedForSelector + private let downUpNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAZgJfAQZ6d3p0cgZ6AQA=".decodedForSelector!) + + // let encodedUpDownNotificationName = "_UIApplicationVolumeUpButtonDownNotification".encodedForSelector + private let upDownNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAFUBCQBfAQZ6d3p0cgZ6AQA=".decodedForSelector!) + + // let encodedUpUpNotificationName = "_UIApplicationVolumeUpButtonUpNotification".encodedForSelector + private let upUpNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAGYCXwEGend6dHIGegEA".decodedForSelector!) + + private let longPressDuration: TimeInterval = 0.5 + + private func registerForNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeUpButtonDown(_:)), + name: upDownNotificationName, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeUpButtonUp(_:)), + name: upUpNotificationName, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeDownButtonDown(_:)), + name: downDownNotificationName, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeDownButtonUp(_:)), + name: downUpNotificationName, + object: nil + ) + } + + private func unregisterForNotifications() { + NotificationCenter.default.removeObserver(self) + } + + @objc func didDetectVolumeUpButtonDown(_ notification: Notification) { + beginButtonPress(with: .up) + } + + @objc func didDetectVolumeUpButtonUp(_ notification: Notification) { + endButtonPress(with: .up) + } + + @objc func didDetectVolumeDownButtonDown(_ notification: Notification) { + beginButtonPress(with: .down) + } + + @objc func didDetectVolumeDownButtonUp(_ notification: Notification) { + endButtonPress(with: .down) + } +} From 6fcd9e2e37d57e309d74e05973ee7a1cf1c578b4 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 16:33:30 -0700 Subject: [PATCH 2/5] Cleanup animations on camera record button to better indicate state to the user --- .../Photos/PhotoCaptureViewController.swift | 89 +++++++++++++------ 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 097c395adc..3df608a8ea 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -70,7 +70,6 @@ class PhotoCaptureViewController: OWSViewController { view.addGestureRecognizer(doubleTapToSwitchCameraGesture) tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture) - doubleTapToSwitchCameraGesture.require(toFail: captureButton.tapGesture) } override func viewWillAppear(_ animated: Bool) { @@ -440,8 +439,6 @@ class CaptureButton: UIView { let innerButton = CircleView() - var tapGesture: UITapGestureRecognizer! - var longPressGesture: UILongPressGestureRecognizer! let longPressDuration = 0.5 @@ -457,11 +454,10 @@ class CaptureButton: UIView { override init(frame: CGRect) { super.init(frame: frame) - tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) - innerButton.addGestureRecognizer(tapGesture) - + // The long press handles both the tap and the hold interaction, as well as the animation + // the presents as the user begins to hold (and the button begins to grow prior to recording) longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress)) - longPressGesture.minimumPressDuration = longPressDuration + longPressGesture.minimumPressDuration = 0 innerButton.addGestureRecognizer(longPressGesture) addSubview(innerButton) @@ -485,14 +481,38 @@ class CaptureButton: UIView { fatalError("init(coder:) has not been implemented") } - // MARK: - Gestures + func beginRecordingAnimation(_ duration: TimeInterval, delay: TimeInterval = 0) { + UIView.beginAnimations("recordingAnimation", context: nil) + UIView.setAnimationDelay(delay) + UIView.setAnimationBeginsFromCurrentState(true) + UIView.setAnimationDuration(duration) + UIView.setAnimationCurve(.linear) - @objc - func didTap(_ gesture: UITapGestureRecognizer) { - delegate?.didTapCaptureButton(self) + innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + superview?.layoutIfNeeded() + + UIView.commitAnimations() } + func endRecordingAnimation(_ duration: TimeInterval) { + UIView.beginAnimations("recordingAnimation", context: nil) + UIView.setAnimationBeginsFromCurrentState(true) + UIView.setAnimationDuration(duration) + UIView.setAnimationCurve(.easeIn) + + innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } + zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } + superview?.layoutIfNeeded() + + UIView.commitAnimations() + } + + // MARK: - Gestures + var initialTouchLocation: CGPoint? + var touchTimer: Timer? + var isLongPressing = false @objc func didLongPress(_ gesture: UILongPressGestureRecognizer) { @@ -507,13 +527,24 @@ class CaptureButton: UIView { case .possible: break case .began: initialTouchLocation = gesture.location(in: gesture.view) - delegate?.didBeginLongPressCaptureButton(self) - UIView.animate(withDuration: 0.2) { - self.innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - self.zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - self.superview?.layoutIfNeeded() + beginRecordingAnimation(0.4, delay: 0.1) + + isLongPressing = false + + touchTimer?.invalidate() + touchTimer = WeakTimer.scheduledTimer( + timeInterval: longPressDuration, + target: self, + userInfo: nil, + repeats: false + ) { [weak self] _ in + guard let `self` = self else { return } + self.isLongPressing = true + self.delegate?.didBeginLongPressCaptureButton(self) } case .changed: + guard isLongPressing else { break } + guard let referenceHeight = delegate?.zoomScaleReferenceHeight else { owsFailDebug("referenceHeight was unexpectedly nil") return @@ -545,22 +576,25 @@ class CaptureButton: UIView { delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha) case .ended: - UIView.animate(withDuration: 0.2) { - self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } - self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } + endRecordingAnimation(0.2) - self.superview?.layoutIfNeeded() + if isLongPressing { + delegate?.didCompleteLongPressCaptureButton(self) + } else { + delegate?.didTapCaptureButton(self) } - delegate?.didCompleteLongPressCaptureButton(self) + + touchTimer?.invalidate() + touchTimer = nil case .cancelled, .failed: + endRecordingAnimation(0.2) - UIView.animate(withDuration: 0.2) { - self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } - self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } - - self.superview?.layoutIfNeeded() + if isLongPressing { + delegate?.didCancelLongPressCaptureButton(self) } - delegate?.didCancelLongPressCaptureButton(self) + + touchTimer?.invalidate() + touchTimer = nil } } } @@ -672,6 +706,7 @@ class RecordingTimerView: UIView { UIView.animate(withDuration: 0.4) { self.icon.alpha = 0 } + label.text = nil } // MARK: - From ad92923f52bbd88b7fc14e59cacd0963cf70a619 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 17:10:41 -0700 Subject: [PATCH 3/5] Hook into volume button handling for camera controls --- .../ViewControllers/Photos/PhotoCapture.swift | 60 ++++++++++++++++--- .../Photos/PhotoCaptureViewController.swift | 16 +++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index 8ce42fe565..83eb1adadd 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -25,6 +25,9 @@ protocol PhotoCaptureDelegate: AnyObject { func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture) var zoomScaleReferenceHeight: CGFloat? { get } var captureOrientation: AVCaptureVideoOrientation { get } + + func beginCaptureButtonAnimation(_ duration: TimeInterval) + func endCaptureButtonAnimation(_ duration: TimeInterval) } class PhotoCapture: NSObject { @@ -332,15 +335,10 @@ class PhotoCapture: NSObject { private func clampZoom(_ factor: CGFloat, device: AVCaptureDevice) -> CGFloat { return min(factor.clamp(minimumZoom, maximumZoom), device.activeFormat.videoMaxZoomFactor) } -} - -extension PhotoCapture: CaptureButtonDelegate { // MARK: - Photo - - func didTapCaptureButton(_ captureButton: CaptureButton) { + private func handleTap() { Logger.verbose("") - guard let delegate = delegate else { return } guard delegate.photoCaptureCanCaptureMoreItems(self) else { delegate.photoCaptureDidTryToCaptureTooMany(self) @@ -355,7 +353,7 @@ extension PhotoCapture: CaptureButtonDelegate { // MARK: - Video - func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) { + private func handleLongPressBegin() { AssertIsOnMainThread() Logger.verbose("") @@ -375,7 +373,7 @@ extension PhotoCapture: CaptureButtonDelegate { }.retainUntilComplete() } - func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { + private func handleLongPressComplete() { Logger.verbose("") sessionQueue.async { self.captureOutput.completeVideo(delegate: self) @@ -386,7 +384,7 @@ extension PhotoCapture: CaptureButtonDelegate { delegate?.photoCaptureDidCompleteVideo(self) } - func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) { + private func handleLongPressCancel() { Logger.verbose("") AssertIsOnMainThread() sessionQueue.async { @@ -394,6 +392,50 @@ extension PhotoCapture: CaptureButtonDelegate { } delegate?.photoCaptureDidCancelVideo(self) } +} + +extension PhotoCapture: VolumeButtonObserver { + func didPressVolumeButton(with identifier: VolumeButtons.Identifier) { + delegate?.beginCaptureButtonAnimation(0.5) + } + + func didReleaseVolumeButton(with identifier: VolumeButtons.Identifier) { + delegate?.endCaptureButtonAnimation(0.2) + } + + func didTapVolumeButton(with identifier: VolumeButtons.Identifier) { + handleTap() + } + + func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { + handleLongPressBegin() + } + + func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { + handleLongPressComplete() + } + + func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { + handleLongPressCancel() + } +} + +extension PhotoCapture: CaptureButtonDelegate { + func didTapCaptureButton(_ captureButton: CaptureButton) { + handleTap() + } + + func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) { + handleLongPressBegin() + } + + func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { + handleLongPressComplete() + } + + func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) { + handleLongPressCancel() + } var zoomScaleReferenceHeight: CGFloat? { return delegate?.zoomScaleReferenceHeight diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 3df608a8ea..7691c12539 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -81,8 +81,14 @@ class PhotoCaptureViewController: OWSViewController { super.viewDidAppear(animated) if hasCaptureStarted { BenchEventComplete(eventId: "Show-Camera") + VolumeButtons.shared?.addObserver(observer: photoCapture) } } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillAppear(animated) + VolumeButtons.shared?.removeObserver(photoCapture) + } override var prefersStatusBarHidden: Bool { guard !OWSWindowManager.shared().hasCall() else { @@ -332,6 +338,8 @@ class PhotoCaptureViewController: OWSViewController { view.addSubview(captureButton) captureButton.autoHCenterInSuperview() captureButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true + + VolumeButtons.shared?.addObserver(observer: photoCapture) } private func showFailureUI(error: Error) { @@ -418,6 +426,14 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { var captureOrientation: AVCaptureVideoOrientation { return lastKnownCaptureOrientation } + + func beginCaptureButtonAnimation(_ duration: TimeInterval) { + captureButton.beginRecordingAnimation(duration) + } + + func endCaptureButtonAnimation(_ duration: TimeInterval) { + captureButton.endRecordingAnimation(duration) + } } // MARK: - Views From 1fe31d9b0c46730f9b73de5b87df976ba9935cc8 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 18:43:26 -0700 Subject: [PATCH 4/5] Fix a bug with capture feedback when using volume buttons --- .../Photos/PhotoCaptureViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 7691c12539..03f574279e 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -375,6 +375,13 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { captureFeedbackView.backgroundColor = .black view.insertSubview(captureFeedbackView, aboveSubview: previewView) captureFeedbackView.autoPinEdgesToSuperviewEdges() + + // Ensure the capture feedback is laid out before we remove it, + // depending on where we're coming from a layout pass might not + // trigger in 0.05 seconds otherwise. + view.setNeedsLayout() + view.layoutIfNeeded() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { captureFeedbackView.removeFromSuperview() } From 4bd83073cf3874ac6c391de6e8b4f54c886e9e98 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Thu, 23 May 2019 08:46:38 -0700 Subject: [PATCH 5/5] PR Feedback --- .../Photos/PhotoCaptureViewController.swift | 57 ++++++++++--------- Signal/src/util/VolumeButtons.swift | 28 ++++----- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 03f574279e..13619d93dd 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -435,11 +435,11 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { } func beginCaptureButtonAnimation(_ duration: TimeInterval) { - captureButton.beginRecordingAnimation(duration) + captureButton.beginRecordingAnimation(duration: duration) } func endCaptureButtonAnimation(_ duration: TimeInterval) { - captureButton.endRecordingAnimation(duration) + captureButton.endRecordingAnimation(duration: duration) } } @@ -504,31 +504,32 @@ class CaptureButton: UIView { fatalError("init(coder:) has not been implemented") } - func beginRecordingAnimation(_ duration: TimeInterval, delay: TimeInterval = 0) { - UIView.beginAnimations("recordingAnimation", context: nil) - UIView.setAnimationDelay(delay) - UIView.setAnimationBeginsFromCurrentState(true) - UIView.setAnimationDuration(duration) - UIView.setAnimationCurve(.linear) - - innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - superview?.layoutIfNeeded() - - UIView.commitAnimations() + func beginRecordingAnimation(duration: TimeInterval, delay: TimeInterval = 0) { + UIView.animate( + withDuration: duration, + delay: delay, + options: [.beginFromCurrentState, .curveLinear], + animations: { + self.innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + self.zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + self.superview?.layoutIfNeeded() + }, + completion: nil + ) } - func endRecordingAnimation(_ duration: TimeInterval) { - UIView.beginAnimations("recordingAnimation", context: nil) - UIView.setAnimationBeginsFromCurrentState(true) - UIView.setAnimationDuration(duration) - UIView.setAnimationCurve(.easeIn) - - innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } - zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } - superview?.layoutIfNeeded() - - UIView.commitAnimations() + func endRecordingAnimation(duration: TimeInterval, delay: TimeInterval = 0) { + UIView.animate( + withDuration: duration, + delay: delay, + options: [.beginFromCurrentState, .curveEaseIn], + animations: { + self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } + self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } + self.superview?.layoutIfNeeded() + }, + completion: nil + ) } // MARK: - Gestures @@ -550,7 +551,7 @@ class CaptureButton: UIView { case .possible: break case .began: initialTouchLocation = gesture.location(in: gesture.view) - beginRecordingAnimation(0.4, delay: 0.1) + beginRecordingAnimation(duration: 0.4, delay: 0.1) isLongPressing = false @@ -599,7 +600,7 @@ class CaptureButton: UIView { delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha) case .ended: - endRecordingAnimation(0.2) + endRecordingAnimation(duration: 0.2) if isLongPressing { delegate?.didCompleteLongPressCaptureButton(self) @@ -610,7 +611,7 @@ class CaptureButton: UIView { touchTimer?.invalidate() touchTimer = nil case .cancelled, .failed: - endRecordingAnimation(0.2) + endRecordingAnimation(duration: 0.2) if isLongPressing { delegate?.didCancelLongPressCaptureButton(self) diff --git a/Signal/src/util/VolumeButtons.swift b/Signal/src/util/VolumeButtons.swift index cf9beddcca..7d2cef5b23 100644 --- a/Signal/src/util/VolumeButtons.swift +++ b/Signal/src/util/VolumeButtons.swift @@ -120,7 +120,7 @@ class VolumeButtons { // It's not possible for up and down to be pressed simulataneously // (if you press the second button, the OS will end the press on // the first), so it allows for simplified handling here. - private func beginButtonPress(with identifier: Identifier) { + private func didPressButton(with identifier: Identifier) { longPressingButton = nil longPressTimer?.invalidate() @@ -139,7 +139,7 @@ class VolumeButtons { notifyObserversOfPress(with: identifier) } - private func endButtonPress(with identifier: Identifier) { + private func didReleaseButton(with identifier: Identifier) { if longPressingButton == identifier { notifyObserversOfCompleteLongPress(with: identifier) } else { @@ -198,28 +198,28 @@ class VolumeButtons { private func registerForNotifications() { NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeUpButtonDown(_:)), + selector: #selector(didPressVolumeUp), name: upDownNotificationName, object: nil ) NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeUpButtonUp(_:)), + selector: #selector(didReleaseVolumeUp), name: upUpNotificationName, object: nil ) NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeDownButtonDown(_:)), + selector: #selector(didPressVolumeDown), name: downDownNotificationName, object: nil ) NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeDownButtonUp(_:)), + selector: #selector(didReleaseVolumeDown), name: downUpNotificationName, object: nil ) @@ -229,19 +229,19 @@ class VolumeButtons { NotificationCenter.default.removeObserver(self) } - @objc func didDetectVolumeUpButtonDown(_ notification: Notification) { - beginButtonPress(with: .up) + @objc func didPressVolumeUp(_ notification: Notification) { + didPressButton(with: .up) } - @objc func didDetectVolumeUpButtonUp(_ notification: Notification) { - endButtonPress(with: .up) + @objc func didReleaseVolumeUp(_ notification: Notification) { + didReleaseButton(with: .up) } - @objc func didDetectVolumeDownButtonDown(_ notification: Notification) { - beginButtonPress(with: .down) + @objc func didPressVolumeDown(_ notification: Notification) { + didPressButton(with: .down) } - @objc func didDetectVolumeDownButtonUp(_ notification: Notification) { - endButtonPress(with: .down) + @objc func didReleaseVolumeDown(_ notification: Notification) { + didReleaseButton(with: .down) } }