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.

PiPViewCoordinator.swift 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /*
  2. * Copyright @ 2017-present 8x8, Inc.
  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. import UIKit
  17. public typealias AnimationCompletion = (Bool) -> Void
  18. public protocol PiPViewCoordinatorDelegate: class {
  19. func exitPictureInPicture()
  20. }
  21. /// Coordinates the view state of a specified view to allow
  22. /// to be presented in full screen or in a custom Picture in Picture mode.
  23. /// This object will also provide the drag and tap interactions of the view
  24. /// when is presented in Picure in Picture mode.
  25. public class PiPViewCoordinator {
  26. /// Limits the boundries of view position on screen when minimized
  27. public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
  28. left: 5,
  29. bottom: 5,
  30. right: 5) {
  31. didSet {
  32. dragController.insets = dragBoundInsets
  33. }
  34. }
  35. public enum Position {
  36. case lowerRightCorner
  37. case upperRightCorner
  38. case lowerLeftCorner
  39. case upperLeftCorner
  40. }
  41. public var initialPositionInSuperview = Position.lowerRightCorner
  42. // Unused. Remove on the next major release.
  43. @available(*, deprecated, message: "The PiP window size is now fixed to 150px.")
  44. public var c: CGFloat = 0.0
  45. public weak var delegate: PiPViewCoordinatorDelegate?
  46. private(set) var isInPiP: Bool = false // true if view is in PiP mode
  47. private(set) var view: UIView
  48. private var currentBounds: CGRect = CGRect.zero
  49. private var tapGestureRecognizer: UITapGestureRecognizer?
  50. private var exitPiPButton: UIButton?
  51. private let dragController: DragGestureController = DragGestureController()
  52. public init(withView view: UIView) {
  53. self.view = view
  54. }
  55. /// Configure the view to be always on top of all the contents
  56. /// of the provided parent view.
  57. /// If a parentView is not provided it will try to use the main window
  58. public func configureAsStickyView(withParentView parentView: UIView? = nil) {
  59. guard
  60. let parentView = parentView ?? UIApplication.shared.keyWindow
  61. else { return }
  62. parentView.addSubview(view)
  63. currentBounds = parentView.bounds
  64. view.frame = currentBounds
  65. view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
  66. }
  67. /// Show view with fade in animation
  68. public func show(completion: AnimationCompletion? = nil) {
  69. if view.isHidden || view.alpha < 1 {
  70. view.isHidden = false
  71. view.alpha = 0
  72. animateTransition(animations: { [weak self] in
  73. self?.view.alpha = 1
  74. }, completion: completion)
  75. }
  76. }
  77. /// Hide view with fade out animation
  78. public func hide(completion: AnimationCompletion? = nil) {
  79. if view.isHidden || view.alpha > 0 {
  80. animateTransition(animations: { [weak self] in
  81. self?.view.alpha = 0
  82. self?.view.isHidden = true
  83. }, completion: completion)
  84. }
  85. }
  86. /// Resize view to and change state to custom PictureInPicture mode
  87. /// This will resize view, add a gesture to enable user to "drag" view
  88. /// around screen, and add a button of top of the view to be able to exit mode
  89. public func enterPictureInPicture() {
  90. isInPiP = true
  91. animateViewChange()
  92. dragController.startDragListener(inView: view)
  93. dragController.insets = dragBoundInsets
  94. // add single tap gesture recognition for displaying exit PiP UI
  95. let exitSelector = #selector(toggleExitPiP)
  96. let tapGestureRecognizer = UITapGestureRecognizer(target: self,
  97. action: exitSelector)
  98. self.tapGestureRecognizer = tapGestureRecognizer
  99. view.addGestureRecognizer(tapGestureRecognizer)
  100. }
  101. /// Exit Picture in picture mode, this will resize view, remove
  102. /// exit pip button, and disable the drag gesture
  103. @objc public func exitPictureInPicture() {
  104. isInPiP = false
  105. animateViewChange()
  106. dragController.stopDragListener()
  107. // hide PiP UI
  108. exitPiPButton?.removeFromSuperview()
  109. exitPiPButton = nil
  110. // remove gesture
  111. let exitSelector = #selector(toggleExitPiP)
  112. tapGestureRecognizer?.removeTarget(self, action: exitSelector)
  113. tapGestureRecognizer = nil
  114. delegate?.exitPictureInPicture()
  115. }
  116. /// Reset view to provide bounds, use this method on rotation or
  117. /// screen size changes
  118. public func resetBounds(bounds: CGRect) {
  119. currentBounds = bounds
  120. exitPictureInPicture()
  121. }
  122. /// Stop the dragging gesture of the root view
  123. public func stopDragGesture() {
  124. dragController.stopDragListener()
  125. }
  126. /// Customize the presentation of exit pip button
  127. open func configureExitPiPButton(target: Any,
  128. action: Selector) -> UIButton {
  129. let buttonImage = UIImage.init(named: "image-resize",
  130. in: Bundle(for: type(of: self)),
  131. compatibleWith: nil)
  132. let button = UIButton(type: .custom)
  133. let size: CGSize = CGSize(width: 44, height: 44)
  134. button.setImage(buttonImage, for: .normal)
  135. button.backgroundColor = .gray
  136. button.layer.cornerRadius = size.width / 2
  137. button.frame = CGRect(origin: CGPoint.zero, size: size)
  138. button.center = view.convert(view.center, from: view.superview)
  139. button.addTarget(target, action: action, for: .touchUpInside)
  140. return button
  141. }
  142. // MARK: - Interactions
  143. @objc private func toggleExitPiP() {
  144. if exitPiPButton == nil {
  145. // show button
  146. let exitSelector = #selector(exitPictureInPicture)
  147. let button = configureExitPiPButton(target: self,
  148. action: exitSelector)
  149. view.addSubview(button)
  150. exitPiPButton = button
  151. } else {
  152. // hide button
  153. exitPiPButton?.removeFromSuperview()
  154. exitPiPButton = nil
  155. }
  156. }
  157. // MARK: - Size calculation
  158. private func animateViewChange() {
  159. UIView.animate(withDuration: 0.25) {
  160. self.view.frame = self.changeViewRect()
  161. self.view.setNeedsLayout()
  162. }
  163. }
  164. private func changeViewRect() -> CGRect {
  165. let bounds = currentBounds
  166. guard isInPiP else {
  167. return bounds
  168. }
  169. // resize to suggested ratio and position to the bottom right
  170. let adjustedBounds = bounds.inset(by: dragBoundInsets)
  171. let size = CGSize(width: 150, height: 150)
  172. let origin = initialPositionFor(pipSize: size, bounds: adjustedBounds)
  173. return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height)
  174. }
  175. private func initialPositionFor(pipSize size: CGSize, bounds: CGRect) -> CGPoint {
  176. switch initialPositionInSuperview {
  177. case .lowerLeftCorner:
  178. return CGPoint(x: bounds.minX, y: bounds.maxY - size.height)
  179. case .lowerRightCorner:
  180. return CGPoint(x: bounds.maxX - size.width, y: bounds.maxY - size.height)
  181. case .upperLeftCorner:
  182. return CGPoint(x: bounds.minX, y: bounds.minY)
  183. case .upperRightCorner:
  184. return CGPoint(x: bounds.maxX - size.width, y: bounds.minY)
  185. }
  186. }
  187. // MARK: - Animation helpers
  188. private func animateTransition(animations: @escaping () -> Void,
  189. completion: AnimationCompletion?) {
  190. UIView.animate(withDuration: 0.1,
  191. delay: 0,
  192. options: .beginFromCurrentState,
  193. animations: animations,
  194. completion: completion)
  195. }
  196. }