Merge branch 'nt/volume-button-shutter-controls'

This commit is contained in:
Nora Trapp 2019-05-23 09:39:50 -07:00
commit 700336ca22
4 changed files with 389 additions and 37 deletions

View File

@ -536,6 +536,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 */; };
@ -1317,6 +1318,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 = "<group>"; };
76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
8811CF832295D8DA00FF6549 /* VolumeButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtons.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@ -2511,6 +2513,7 @@
34E5DC8020D8050D00C08145 /* RegistrationUtils.h */,
34E5DC8120D8050D00C08145 /* RegistrationUtils.m */,
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */,
8811CF832295D8DA00FF6549 /* VolumeButtons.swift */,
FCFA64B11A24F29E0007FB87 /* UI Categories */,
);
path = util;
@ -3701,6 +3704,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 */,

View File

@ -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

View File

@ -70,7 +70,6 @@ class PhotoCaptureViewController: OWSViewController {
view.addGestureRecognizer(doubleTapToSwitchCameraGesture)
tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture)
doubleTapToSwitchCameraGesture.require(toFail: captureButton.tapGesture)
}
override func viewWillAppear(_ animated: Bool) {
@ -82,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 {
@ -333,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) {
@ -368,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()
}
@ -419,6 +433,14 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate {
var captureOrientation: AVCaptureVideoOrientation {
return lastKnownCaptureOrientation
}
func beginCaptureButtonAnimation(_ duration: TimeInterval) {
captureButton.beginRecordingAnimation(duration: duration)
}
func endCaptureButtonAnimation(_ duration: TimeInterval) {
captureButton.endRecordingAnimation(duration: duration)
}
}
// MARK: - Views
@ -440,8 +462,6 @@ class CaptureButton: UIView {
let innerButton = CircleView()
var tapGesture: UITapGestureRecognizer!
var longPressGesture: UILongPressGestureRecognizer!
let longPressDuration = 0.5
@ -457,11 +477,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 +504,39 @@ class CaptureButton: UIView {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Gestures
@objc
func didTap(_ gesture: UITapGestureRecognizer) {
delegate?.didTapCaptureButton(self)
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, 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
var initialTouchLocation: CGPoint?
var touchTimer: Timer?
var isLongPressing = false
@objc
func didLongPress(_ gesture: UILongPressGestureRecognizer) {
@ -507,13 +551,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(duration: 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 +600,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(duration: 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(duration: 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 +730,7 @@ class RecordingTimerView: UIView {
UIView.animate(withDuration: 0.4) {
self.icon.alpha = 0
}
label.text = nil
}
// MARK: -

View File

@ -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 were 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<VolumeButtonObserver>] = []
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 didPressButton(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 didReleaseButton(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(didPressVolumeUp),
name: upDownNotificationName,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didReleaseVolumeUp),
name: upUpNotificationName,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didPressVolumeDown),
name: downDownNotificationName,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(didReleaseVolumeDown),
name: downUpNotificationName,
object: nil
)
}
private func unregisterForNotifications() {
NotificationCenter.default.removeObserver(self)
}
@objc func didPressVolumeUp(_ notification: Notification) {
didPressButton(with: .up)
}
@objc func didReleaseVolumeUp(_ notification: Notification) {
didReleaseButton(with: .up)
}
@objc func didPressVolumeDown(_ notification: Notification) {
didPressButton(with: .down)
}
@objc func didReleaseVolumeDown(_ notification: Notification) {
didReleaseButton(with: .down)
}
}