Clean up PiP mode for iOS

This commit is contained in:
Daniel Ornelas 2018-04-03 20:25:12 -05:00 committed by Paweł Domas
parent 219b93a3c9
commit fd44721bac
6 changed files with 153 additions and 304 deletions

View File

@ -51,6 +51,8 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@ -21,19 +21,73 @@ class ViewController: UIViewController {
@IBOutlet weak var videoButton: UIButton?
private var jitsiMeetCoordinator: JitsiMeetPresentationCoordinator?
fileprivate var pipViewCoordinator: PiPViewCoordinator?
fileprivate var jitsiMeetView: JitsiMeetView?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillTransition(to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let rect = CGRect(origin: CGPoint.zero, size: size)
pipViewCoordinator?.resetBounds(bounds: rect)
}
// MARK: - Actions
@IBAction func openJitsiMeet(sender: Any?) {
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
self.jitsiMeetCoordinator = jitsiMeetCoordinator
jitsiMeetCoordinator.jitsiMeetView.welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView.load(nil)
jitsiMeetCoordinator.show()
cleanUp()
// create and configure jitsimeet view
let jitsiMeetView = JitsiMeetView()
jitsiMeetView.welcomePageEnabled = true
jitsiMeetView.pictureInPictureEnabled = true
jitsiMeetView.load(nil)
jitsiMeetView.delegate = self
self.jitsiMeetView = jitsiMeetView
// Enable jitsimeet view to be a view that can be displayed
// on top of all the things, and let the coordinator to manage
// the view state and interactions
pipViewCoordinator = PiPViewCoordinator(withView: jitsiMeetView)
pipViewCoordinator?.configureAsStickyView(withParentView: view)
// animate in
jitsiMeetView.alpha = 0
pipViewCoordinator?.show()
}
fileprivate func cleanUp() {
jitsiMeetView?.removeFromSuperview()
jitsiMeetView = nil
pipViewCoordinator = nil
}
}
extension ViewController: JitsiMeetViewDelegate {
func conferenceFailed(_ data: [AnyHashable : Any]!) {
hideJitsiMeetViewAndCleanUp()
}
func conferenceLeft(_ data: [AnyHashable : Any]!) {
hideJitsiMeetViewAndCleanUp()
}
func enterPicture(inPicture data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.pipViewCoordinator?.enterPictureInPicture()
}
}
private func hideJitsiMeetViewAndCleanUp() {
DispatchQueue.main.async {
self.pipViewCoordinator?.hide() { _ in
self.cleanUp()
}
}
}
}

View File

@ -31,10 +31,8 @@
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* PiPWindow.swift */; };
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
/* End PBXBuildFile section */
@ -68,10 +66,8 @@
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
C6A3425C204EF76800E062DD /* PiPWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiPWindow.swift; sourceTree = "<group>"; };
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetPresentationCoordinator.swift; sourceTree = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -171,9 +167,7 @@
isa = PBXGroup;
children = (
C6A3425E204EF76800E062DD /* DragGestureController.swift */,
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */,
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */,
C6A3425C204EF76800E062DD /* PiPWindow.swift */,
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */,
);
path = "picture-in-picture";
sourceTree = "<group>";
@ -345,14 +339,12 @@
buildActionMask = 2147483647;
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */,
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */,
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,

View File

@ -1,107 +0,0 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Coordinates the presentation of JitsiMeetViewController inside of
/// an external window that can be resized and dragged with custom PiP mode
open class JitsiMeetPresentationCoordinator: NSObject {
public let meetViewController: JitsiMeetViewController
public let meetWindow: PiPWindow
public var isInPiP: Bool {
get {
return meetWindow.isInPiP
}
}
public var jitsiMeetView: JitsiMeetView {
get {
return meetViewController.jitsiMeetView
}
}
public init(meetViewController: JitsiMeetViewController? = nil,
meetWindow: PiPWindow? = nil) {
self.meetViewController = meetViewController ?? JitsiMeetViewController()
self.meetWindow = meetWindow ?? PiPWindow(frame: UIScreen.main.bounds)
super.init()
configureMeetWindow()
configureMeetViewController()
}
/// Show window with jitsi meet and perform a completion closure
open func show(completion: CompletionAction? = nil) {
meetWindow.show(completion: completion)
}
/// Hide window with jitsi meet and perform a completion closure
open func hide(completion: CompletionAction? = nil) {
meetWindow.hide(completion: completion)
}
open func cleanUp() {
// TODO: more clean up work on this
meetWindow.isHidden = true
meetWindow.stopDragGesture()
}
deinit {
cleanUp()
}
// MARK: - helpers
private func configureMeetViewController() {
meetViewController.jitsiMeetView.pictureInPictureEnabled = true
meetViewController.delegate = self
}
private func configureMeetWindow() {
meetWindow.backgroundColor = .clear
meetWindow.windowLevel = UIWindowLevelStatusBar + 100
meetWindow.rootViewController = self.meetViewController
}
}
extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
open func performPresentationUpdate(to: JitsiMeetPresentationUpdate) {
switch to {
case .enterPictureInPicture:
meetWindow.enterPictureInPicture()
case .sizeChange:
// resize to full screen if rotation happens
if meetWindow.isInPiP {
meetWindow.exitPictureInPicture()
}
}
}
open func conferenceStarted() {
if meetWindow.isHidden {
meetWindow.show()
}
}
open func conferenceEnded(didFail: Bool) {
cleanUp()
}
}

View File

@ -1,108 +0,0 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public enum JitsiMeetPresentationUpdate {
/// The conference wants to enter Picture-in-Picture
case enterPictureInPicture
/// A screen size change (usually screen rotation)
case sizeChange
}
public protocol JitsiMeetViewControllerDelegate: class {
/// Notifies a change of the conference presentation style.
///
/// - Parameter to: The presentation state that will be changed to
func performPresentationUpdate(to: JitsiMeetPresentationUpdate)
/// The conference started
func conferenceStarted()
/// The conference ended
///
/// - Parameter didFail: The reason of ending the conference
func conferenceEnded(didFail: Bool)
}
/// Wrapper ViewController of a JitsiMeetView
///
/// Is suggested to override this class and implement some customization
/// on how to handle the JitsiMeetView delegate events
open class JitsiMeetViewController: UIViewController {
open weak var delegate: JitsiMeetViewControllerDelegate?
private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView()
override open func loadView() {
super.loadView()
self.view = jitsiMeetView
}
open override func viewDidLoad() {
super.viewDidLoad()
jitsiMeetView.delegate = self
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
delegate?.performPresentationUpdate(to: .sizeChange)
}
}
extension JitsiMeetViewController: JitsiMeetViewDelegate {
open func conferenceWillJoin(_ data: [AnyHashable : Any]!) {
// do something
}
open func conferenceJoined(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceStarted()
}
}
open func conferenceWillLeave(_ data: [AnyHashable : Any]!) {
// do something
}
open func conferenceLeft(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: false)
}
}
open func conferenceFailed(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: true)
}
}
open func loadConfigError(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: true)
}
}
open func enterPicture(inPicture data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.performPresentationUpdate(to: .enterPictureInPicture)
}
}
}

View File

@ -14,14 +14,15 @@
* limitations under the License.
*/
/// Alias defining a completion closure that returns a Bool
public typealias CompletionAction = (Bool) -> Void
public typealias AnimationCompletion = (Bool) -> Void
/// A window that allows its root view controller to be presented
/// in full screen or in a custom Picture in Picture mode
open class PiPWindow: UIWindow {
/// Coordinates the view state of a specified view to allow
/// to be presented in full screen or in a custom Picture in Picture mode.
/// This object will also provide the drag and tap interactions of the view
/// when is presented in Picure in Picture mode.
public class PiPViewCoordinator {
/// Limits the boundries of root view position on screen when minimized
/// Limits the boundries of view position on screen when minimized
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
left: 5,
bottom: 5,
@ -31,10 +32,10 @@ open class PiPWindow: UIWindow {
}
}
/// The size ratio for root view controller view when in PiP mode
/// The size ratio of the view when in PiP mode
public var pipSizeRatio: CGFloat = {
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
switch (deviceIdiom) {
switch deviceIdiom {
case .pad:
return 0.25
case .phone:
@ -44,50 +45,62 @@ open class PiPWindow: UIWindow {
}
}()
/// The PiP state of this contents of the window
private(set) var isInPiP: Bool = false
private(set) var isInPiP: Bool = false // true if view is in PiP mode
private let dragController: DragGestureController = DragGestureController()
private(set) var view: UIView
private var currentBounds: CGRect = CGRect.zero
/// Used when in PiP mode to enable/disable exit PiP UI
private var tapGestureRecognizer: UITapGestureRecognizer?
private var exitPiPButton: UIButton?
/// Help out to bubble up the gesture detection outside of the rootVC frame
open override func point(inside point: CGPoint,
with event: UIEvent?) -> Bool {
guard let vc = rootViewController else {
return super.point(inside: point, with: event)
}
return vc.view.frame.contains(point)
private let dragController: DragGestureController = DragGestureController()
public init(withView view: UIView) {
self.view = view
}
/// animate in the window
open func show(completion: CompletionAction? = nil) {
if self.isHidden || self.alpha < 1 {
self.isHidden = false
self.alpha = 0
/// Configure the view to be always on top of all the contents
/// of the provided parent view.
/// If a parentView is not provided it will try to use the main window
public func configureAsStickyView(withParentView parentView: UIView? = nil) {
guard
let parentView = parentView ?? UIApplication.shared.keyWindow
else { return }
parentView.addSubview(view)
currentBounds = parentView.bounds
view.frame = currentBounds
view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
}
/// Show view with fade in animation
public func show(completion: AnimationCompletion? = nil) {
if view.isHidden || view.alpha < 1 {
view.isHidden = false
view.alpha = 0
animateTransition(animations: {
self.alpha = 1
animateTransition(animations: { [weak self] in
self?.view.alpha = 1
}, completion: completion)
}
}
/// animate out the window
open func hide(completion: CompletionAction? = nil) {
if !self.isHidden || self.alpha > 0 {
animateTransition(animations: {
self.alpha = 1
/// Hide view with fade out animation
public func hide(completion: AnimationCompletion? = nil) {
if view.isHidden || view.alpha > 0 {
animateTransition(animations: { [weak self] in
self?.view.alpha = 0
self?.view.isHidden = true
}, completion: completion)
}
}
/// Resize the root view to PiP mode
open func enterPictureInPicture() {
guard let view = rootViewController?.view else { return }
/// Resize view to and change state to custom PictureInPicture mode
/// This will resize view, add a gesture to enable user to "drag" view
/// around screen, and add a button of top of the view to be able to exit mode
public func enterPictureInPicture() {
isInPiP = true
animateRootViewChange()
animateViewChange()
dragController.startDragListener(inView: view)
dragController.insets = dragBoundInsets
@ -99,10 +112,11 @@ open class PiPWindow: UIWindow {
view.addGestureRecognizer(tapGestureRecognizer)
}
/// Resize the root view to full screen
open func exitPictureInPicture() {
/// Exit Picture in picture mode, this will resize view, remove
/// exit pip button, and disable the drag gesture
@objc public func exitPictureInPicture() {
isInPiP = false
animateRootViewChange()
animateViewChange()
dragController.stopDragListener()
// hide PiP UI
@ -115,6 +129,13 @@ open class PiPWindow: UIWindow {
tapGestureRecognizer = nil
}
/// Reset view to provide bounds, use this method on rotation or
/// screen size changes
public func resetBounds(bounds: CGRect) {
currentBounds = bounds
exitPictureInPicture()
}
/// Stop the dragging gesture of the root view
public func stopDragGesture() {
dragController.stopDragListener()
@ -132,41 +153,14 @@ open class PiPWindow: UIWindow {
button.backgroundColor = .gray
button.layer.cornerRadius = size.width / 2
button.frame = CGRect(origin: CGPoint.zero, size: size)
if let view = rootViewController?.view {
button.center = view.convert(view.center, from:view.superview)
}
button.center = view.convert(view.center, from: view.superview)
button.addTarget(target, action: action, for: .touchUpInside)
return button
}
// MARK: - Manage presentation switching
private func animateRootViewChange() {
UIView.animate(withDuration: 0.25) {
self.rootViewController?.view.frame = self.changeRootViewRect()
self.rootViewController?.view.setNeedsLayout()
}
}
private func changeRootViewRect() -> CGRect {
guard isInPiP else {
return self.bounds
}
// resize to suggested ratio and position to the bottom right
let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
let size = CGSize(width: bounds.size.width * pipSizeRatio,
height: bounds.size.height * pipSizeRatio)
let x: CGFloat = adjustedBounds.maxX - size.width
let y: CGFloat = adjustedBounds.maxY - size.height
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
// MARK: - Exit PiP
// MARK: - Interactions
@objc private func toggleExitPiP() {
guard let view = rootViewController?.view else { return }
if exitPiPButton == nil {
// show button
let exitSelector = #selector(exitPictureInPicture)
@ -182,18 +176,40 @@ open class PiPWindow: UIWindow {
}
}
@objc private func exitPiP() {
exitPictureInPicture()
// MARK: - Size calculation
private func animateViewChange() {
UIView.animate(withDuration: 0.25) {
self.view.frame = self.changeViewRect()
self.view.setNeedsLayout()
}
}
// MARK: - Animation transition
private func changeViewRect() -> CGRect {
let bounds = currentBounds
guard isInPiP else {
return bounds
}
// resize to suggested ratio and position to the bottom right
let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets)
let size = CGSize(width: bounds.size.width * pipSizeRatio,
height: bounds.size.height * pipSizeRatio)
let x: CGFloat = adjustedBounds.maxX - size.width
let y: CGFloat = adjustedBounds.maxY - size.height
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
// MARK: - Animation helpers
private func animateTransition(animations: @escaping () -> Void,
completion: CompletionAction?) {
completion: AnimationCompletion?) {
UIView.animate(withDuration: 0.1,
delay: 0,
options: .beginFromCurrentState,
animations: animations,
completion: completion)
}
}