You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PiPWindow.swift 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /*
  2. * Copyright @ 2017-present Atlassian Pty Ltd
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. /// Alias defining a completion closure that returns a Bool
  17. public typealias CompletionAction = (Bool) -> Void
  18. /// A window that allows its root view controller to be presented
  19. /// in full screen or in a custom Picture in Picture mode
  20. open class PiPWindow: UIWindow {
  21. /// Limits the boundries of root view position on screen when minimized
  22. public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
  23. left: 5,
  24. bottom: 5,
  25. right: 5) {
  26. didSet {
  27. dragController.insets = dragBoundInsets
  28. }
  29. }
  30. /// The size ratio for root view controller view when in PiP mode
  31. public var pipSizeRatio: CGFloat = 0.333
  32. /// The PiP state of this contents of the window
  33. private(set) var isInPiP: Bool = false
  34. private let dragController: DragGestureController = DragGestureController()
  35. /// Used when in PiP mode to enable/disable exit PiP UI
  36. private var tapGestureRecognizer: UITapGestureRecognizer?
  37. private var exitPiPButton: UIButton?
  38. /// Help out to bubble up the gesture detection outside of the rootVC frame
  39. open override func point(inside point: CGPoint,
  40. with event: UIEvent?) -> Bool {
  41. guard let vc = rootViewController else {
  42. return super.point(inside: point, with: event)
  43. }
  44. return vc.view.frame.contains(point)
  45. }
  46. /// animate in the window
  47. open func show(completion: CompletionAction? = nil) {
  48. if self.isHidden || self.alpha < 1 {
  49. self.isHidden = false
  50. self.alpha = 0
  51. animateTransition(animations: {
  52. self.alpha = 1
  53. }, completion: completion)
  54. }
  55. }
  56. /// animate out the window
  57. open func hide(completion: CompletionAction? = nil) {
  58. if !self.isHidden || self.alpha > 0 {
  59. animateTransition(animations: {
  60. self.alpha = 1
  61. }, completion: completion)
  62. }
  63. }
  64. /// Resize the root view to PiP mode
  65. open func enterPictureInPicture() {
  66. guard let view = rootViewController?.view else { return }
  67. isInPiP = true
  68. animateRootViewChange()
  69. dragController.startDragListener(inView: view)
  70. dragController.insets = dragBoundInsets
  71. // add single tap gesture recognition for displaying exit PiP UI
  72. let exitSelector = #selector(toggleExitPiP)
  73. let tapGestureRecognizer = UITapGestureRecognizer(target: self,
  74. action: exitSelector)
  75. self.tapGestureRecognizer = tapGestureRecognizer
  76. view.addGestureRecognizer(tapGestureRecognizer)
  77. }
  78. /// Resize the root view to full screen
  79. open func exitPictureInPicture() {
  80. isInPiP = false
  81. animateRootViewChange()
  82. dragController.stopDragListener()
  83. // hide PiP UI
  84. exitPiPButton?.removeFromSuperview()
  85. exitPiPButton = nil
  86. // remove gesture
  87. let exitSelector = #selector(toggleExitPiP)
  88. tapGestureRecognizer?.removeTarget(self, action: exitSelector)
  89. tapGestureRecognizer = nil
  90. }
  91. /// Stop the dragging gesture of the root view
  92. public func stopDragGesture() {
  93. dragController.stopDragListener()
  94. }
  95. /// Customize the presentation of exit pip button
  96. open func configureExitPiPButton(target: Any,
  97. action: Selector) -> UIButton {
  98. let buttonImage = UIImage.init(named: "image-resize",
  99. in: Bundle(for: type(of: self)),
  100. compatibleWith: nil)
  101. let button = UIButton(type: .custom)
  102. let size: CGSize = CGSize(width: 44, height: 44)
  103. button.setImage(buttonImage, for: .normal)
  104. button.backgroundColor = .gray
  105. button.layer.cornerRadius = size.width / 2
  106. button.frame = CGRect(origin: CGPoint.zero, size: size)
  107. if let view = rootViewController?.view {
  108. button.center = view.convert(view.center, from:view.superview)
  109. }
  110. button.addTarget(target, action: action, for: .touchUpInside)
  111. return button
  112. }
  113. // MARK: - Manage presentation switching
  114. private func animateRootViewChange() {
  115. UIView.animate(withDuration: 0.25) {
  116. self.rootViewController?.view.frame = self.changeRootViewRect()
  117. self.rootViewController?.view.setNeedsLayout()
  118. }
  119. }
  120. private func changeRootViewRect() -> CGRect {
  121. guard isInPiP else {
  122. return self.bounds
  123. }
  124. // resize to suggested ratio and position to the bottom right
  125. let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
  126. let size = CGSize(width: bounds.size.width * pipSizeRatio,
  127. height: bounds.size.height * pipSizeRatio)
  128. let x: CGFloat = adjustedBounds.maxX - size.width
  129. let y: CGFloat = adjustedBounds.maxY - size.height
  130. return CGRect(x: x, y: y, width: size.width, height: size.height)
  131. }
  132. // MARK: - Exit PiP
  133. @objc private func toggleExitPiP() {
  134. guard let view = rootViewController?.view else { return }
  135. if exitPiPButton == nil {
  136. // show button
  137. let exitSelector = #selector(exitPictureInPicture)
  138. let button = configureExitPiPButton(target: self,
  139. action: exitSelector)
  140. view.addSubview(button)
  141. exitPiPButton = button
  142. } else {
  143. // hide button
  144. exitPiPButton?.removeFromSuperview()
  145. exitPiPButton = nil
  146. }
  147. }
  148. @objc private func exitPiP() {
  149. exitPictureInPicture()
  150. }
  151. // MARK: - Animation transition
  152. private func animateTransition(animations: @escaping () -> Void,
  153. completion: CompletionAction?) {
  154. UIView.animate(withDuration: 0.1,
  155. delay: 0,
  156. options: .beginFromCurrentState,
  157. animations: animations,
  158. completion: completion)
  159. }
  160. }