| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- /*
- * 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.
- */
-
- /// Alias defining a completion closure that returns a Bool
- public typealias CompletionAction = (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 {
-
- /// Limits the boundries of root view position on screen when minimized
- public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
- left: 5,
- bottom: 5,
- right: 5) {
- didSet {
- dragController.insets = dragBoundInsets
- }
- }
-
- /// The size ratio for root view controller view when in PiP mode
- public var pipSizeRatio: CGFloat = 0.333
-
- /// The PiP state of this contents of the window
- private(set) var isInPiP: Bool = false
-
- private let dragController: DragGestureController = DragGestureController()
-
- /// 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)
- }
-
- /// animate in the window
- open func show(completion: CompletionAction? = nil) {
- if self.isHidden || self.alpha < 1 {
- self.isHidden = false
- self.alpha = 0
-
- animateTransition(animations: {
- self.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
- }, completion: completion)
- }
- }
-
- /// Resize the root view to PiP mode
- open func enterPictureInPicture() {
- guard let view = rootViewController?.view else { return }
- isInPiP = true
- animateRootViewChange()
- dragController.startDragListener(inView: view)
- dragController.insets = dragBoundInsets
-
- // add single tap gesture recognition for displaying exit PiP UI
- let exitSelector = #selector(toggleExitPiP)
- let tapGestureRecognizer = UITapGestureRecognizer(target: self,
- action: exitSelector)
- self.tapGestureRecognizer = tapGestureRecognizer
- view.addGestureRecognizer(tapGestureRecognizer)
- }
-
- /// Resize the root view to full screen
- open func exitPictureInPicture() {
- isInPiP = false
- animateRootViewChange()
- dragController.stopDragListener()
-
- // hide PiP UI
- exitPiPButton?.removeFromSuperview()
- exitPiPButton = nil
-
- // remove gesture
- let exitSelector = #selector(toggleExitPiP)
- tapGestureRecognizer?.removeTarget(self, action: exitSelector)
- tapGestureRecognizer = nil
- }
-
- /// Stop the dragging gesture of the root view
- public func stopDragGesture() {
- dragController.stopDragListener()
- }
-
- /// Customize the presentation of exit pip button
- open func configureExitPiPButton(target: Any,
- action: Selector) -> UIButton {
- let buttonImage = UIImage.init(named: "image-resize",
- in: Bundle(for: type(of: self)),
- compatibleWith: nil)
- let button = UIButton(type: .custom)
- let size: CGSize = CGSize(width: 44, height: 44)
- button.setImage(buttonImage, for: .normal)
- 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.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
-
- @objc private func toggleExitPiP() {
- guard let view = rootViewController?.view else { return }
-
- if exitPiPButton == nil {
- // show button
- let exitSelector = #selector(exitPictureInPicture)
- let button = configureExitPiPButton(target: self,
- action: exitSelector)
- view.addSubview(button)
- exitPiPButton = button
-
- } else {
- // hide button
- exitPiPButton?.removeFromSuperview()
- exitPiPButton = nil
- }
- }
-
- @objc private func exitPiP() {
- exitPictureInPicture()
- }
-
- // MARK: - Animation transition
-
- private func animateTransition(animations: @escaping () -> Void,
- completion: CompletionAction?) {
- UIView.animate(withDuration: 0.1,
- delay: 0,
- options: .beginFromCurrentState,
- animations: animations,
- completion: completion)
- }
- }
|