|
@@ -14,14 +14,15 @@
|
14
|
14
|
* limitations under the License.
|
15
|
15
|
*/
|
16
|
16
|
|
17
|
|
-/// Alias defining a completion closure that returns a Bool
|
18
|
|
-public typealias CompletionAction = (Bool) -> Void
|
|
17
|
+public typealias AnimationCompletion = (Bool) -> Void
|
19
|
18
|
|
20
|
|
-/// A window that allows its root view controller to be presented
|
21
|
|
-/// in full screen or in a custom Picture in Picture mode
|
22
|
|
-open class PiPWindow: UIWindow {
|
|
19
|
+/// Coordinates the view state of a specified view to allow
|
|
20
|
+/// to be presented in full screen or in a custom Picture in Picture mode.
|
|
21
|
+/// This object will also provide the drag and tap interactions of the view
|
|
22
|
+/// when is presented in Picure in Picture mode.
|
|
23
|
+public class PiPViewCoordinator {
|
23
|
24
|
|
24
|
|
- /// Limits the boundries of root view position on screen when minimized
|
|
25
|
+ /// Limits the boundries of view position on screen when minimized
|
25
|
26
|
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
|
26
|
27
|
left: 5,
|
27
|
28
|
bottom: 5,
|
|
@@ -31,10 +32,10 @@ open class PiPWindow: UIWindow {
|
31
|
32
|
}
|
32
|
33
|
}
|
33
|
34
|
|
34
|
|
- /// The size ratio for root view controller view when in PiP mode
|
|
35
|
+ /// The size ratio of the view when in PiP mode
|
35
|
36
|
public var pipSizeRatio: CGFloat = {
|
36
|
37
|
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
|
37
|
|
- switch (deviceIdiom) {
|
|
38
|
+ switch deviceIdiom {
|
38
|
39
|
case .pad:
|
39
|
40
|
return 0.25
|
40
|
41
|
case .phone:
|
|
@@ -44,50 +45,62 @@ open class PiPWindow: UIWindow {
|
44
|
45
|
}
|
45
|
46
|
}()
|
46
|
47
|
|
47
|
|
- /// The PiP state of this contents of the window
|
48
|
|
- private(set) var isInPiP: Bool = false
|
|
48
|
+ private(set) var isInPiP: Bool = false // true if view is in PiP mode
|
49
|
49
|
|
50
|
|
- private let dragController: DragGestureController = DragGestureController()
|
|
50
|
+ private(set) var view: UIView
|
|
51
|
+ private var currentBounds: CGRect = CGRect.zero
|
51
|
52
|
|
52
|
|
- /// Used when in PiP mode to enable/disable exit PiP UI
|
53
|
53
|
private var tapGestureRecognizer: UITapGestureRecognizer?
|
54
|
54
|
private var exitPiPButton: UIButton?
|
55
|
55
|
|
56
|
|
- /// Help out to bubble up the gesture detection outside of the rootVC frame
|
57
|
|
- open override func point(inside point: CGPoint,
|
58
|
|
- with event: UIEvent?) -> Bool {
|
59
|
|
- guard let vc = rootViewController else {
|
60
|
|
- return super.point(inside: point, with: event)
|
61
|
|
- }
|
62
|
|
- return vc.view.frame.contains(point)
|
|
56
|
+ private let dragController: DragGestureController = DragGestureController()
|
|
57
|
+
|
|
58
|
+ public init(withView view: UIView) {
|
|
59
|
+ self.view = view
|
|
60
|
+ }
|
|
61
|
+
|
|
62
|
+ /// Configure the view to be always on top of all the contents
|
|
63
|
+ /// of the provided parent view.
|
|
64
|
+ /// If a parentView is not provided it will try to use the main window
|
|
65
|
+ public func configureAsStickyView(withParentView parentView: UIView? = nil) {
|
|
66
|
+ guard
|
|
67
|
+ let parentView = parentView ?? UIApplication.shared.keyWindow
|
|
68
|
+ else { return }
|
|
69
|
+
|
|
70
|
+ parentView.addSubview(view)
|
|
71
|
+ currentBounds = parentView.bounds
|
|
72
|
+ view.frame = currentBounds
|
|
73
|
+ view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
|
63
|
74
|
}
|
64
|
75
|
|
65
|
|
- /// animate in the window
|
66
|
|
- open func show(completion: CompletionAction? = nil) {
|
67
|
|
- if self.isHidden || self.alpha < 1 {
|
68
|
|
- self.isHidden = false
|
69
|
|
- self.alpha = 0
|
|
76
|
+ /// Show view with fade in animation
|
|
77
|
+ public func show(completion: AnimationCompletion? = nil) {
|
|
78
|
+ if view.isHidden || view.alpha < 1 {
|
|
79
|
+ view.isHidden = false
|
|
80
|
+ view.alpha = 0
|
70
|
81
|
|
71
|
|
- animateTransition(animations: {
|
72
|
|
- self.alpha = 1
|
|
82
|
+ animateTransition(animations: { [weak self] in
|
|
83
|
+ self?.view.alpha = 1
|
73
|
84
|
}, completion: completion)
|
74
|
85
|
}
|
75
|
86
|
}
|
76
|
87
|
|
77
|
|
- /// animate out the window
|
78
|
|
- open func hide(completion: CompletionAction? = nil) {
|
79
|
|
- if !self.isHidden || self.alpha > 0 {
|
80
|
|
- animateTransition(animations: {
|
81
|
|
- self.alpha = 1
|
|
88
|
+ /// Hide view with fade out animation
|
|
89
|
+ public func hide(completion: AnimationCompletion? = nil) {
|
|
90
|
+ if view.isHidden || view.alpha > 0 {
|
|
91
|
+ animateTransition(animations: { [weak self] in
|
|
92
|
+ self?.view.alpha = 0
|
|
93
|
+ self?.view.isHidden = true
|
82
|
94
|
}, completion: completion)
|
83
|
95
|
}
|
84
|
96
|
}
|
85
|
97
|
|
86
|
|
- /// Resize the root view to PiP mode
|
87
|
|
- open func enterPictureInPicture() {
|
88
|
|
- guard let view = rootViewController?.view else { return }
|
|
98
|
+ /// Resize view to and change state to custom PictureInPicture mode
|
|
99
|
+ /// This will resize view, add a gesture to enable user to "drag" view
|
|
100
|
+ /// around screen, and add a button of top of the view to be able to exit mode
|
|
101
|
+ public func enterPictureInPicture() {
|
89
|
102
|
isInPiP = true
|
90
|
|
- animateRootViewChange()
|
|
103
|
+ animateViewChange()
|
91
|
104
|
dragController.startDragListener(inView: view)
|
92
|
105
|
dragController.insets = dragBoundInsets
|
93
|
106
|
|
|
@@ -99,10 +112,11 @@ open class PiPWindow: UIWindow {
|
99
|
112
|
view.addGestureRecognizer(tapGestureRecognizer)
|
100
|
113
|
}
|
101
|
114
|
|
102
|
|
- /// Resize the root view to full screen
|
103
|
|
- open func exitPictureInPicture() {
|
|
115
|
+ /// Exit Picture in picture mode, this will resize view, remove
|
|
116
|
+ /// exit pip button, and disable the drag gesture
|
|
117
|
+ @objc public func exitPictureInPicture() {
|
104
|
118
|
isInPiP = false
|
105
|
|
- animateRootViewChange()
|
|
119
|
+ animateViewChange()
|
106
|
120
|
dragController.stopDragListener()
|
107
|
121
|
|
108
|
122
|
// hide PiP UI
|
|
@@ -115,6 +129,13 @@ open class PiPWindow: UIWindow {
|
115
|
129
|
tapGestureRecognizer = nil
|
116
|
130
|
}
|
117
|
131
|
|
|
132
|
+ /// Reset view to provide bounds, use this method on rotation or
|
|
133
|
+ /// screen size changes
|
|
134
|
+ public func resetBounds(bounds: CGRect) {
|
|
135
|
+ currentBounds = bounds
|
|
136
|
+ exitPictureInPicture()
|
|
137
|
+ }
|
|
138
|
+
|
118
|
139
|
/// Stop the dragging gesture of the root view
|
119
|
140
|
public func stopDragGesture() {
|
120
|
141
|
dragController.stopDragListener()
|
|
@@ -132,41 +153,14 @@ open class PiPWindow: UIWindow {
|
132
|
153
|
button.backgroundColor = .gray
|
133
|
154
|
button.layer.cornerRadius = size.width / 2
|
134
|
155
|
button.frame = CGRect(origin: CGPoint.zero, size: size)
|
135
|
|
- if let view = rootViewController?.view {
|
136
|
|
- button.center = view.convert(view.center, from:view.superview)
|
137
|
|
- }
|
|
156
|
+ button.center = view.convert(view.center, from: view.superview)
|
138
|
157
|
button.addTarget(target, action: action, for: .touchUpInside)
|
139
|
158
|
return button
|
140
|
159
|
}
|
141
|
160
|
|
142
|
|
- // MARK: - Manage presentation switching
|
143
|
|
-
|
144
|
|
- private func animateRootViewChange() {
|
145
|
|
- UIView.animate(withDuration: 0.25) {
|
146
|
|
- self.rootViewController?.view.frame = self.changeRootViewRect()
|
147
|
|
- self.rootViewController?.view.setNeedsLayout()
|
148
|
|
- }
|
149
|
|
- }
|
150
|
|
-
|
151
|
|
- private func changeRootViewRect() -> CGRect {
|
152
|
|
- guard isInPiP else {
|
153
|
|
- return self.bounds
|
154
|
|
- }
|
155
|
|
-
|
156
|
|
- // resize to suggested ratio and position to the bottom right
|
157
|
|
- let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
|
158
|
|
- let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
159
|
|
- height: bounds.size.height * pipSizeRatio)
|
160
|
|
- let x: CGFloat = adjustedBounds.maxX - size.width
|
161
|
|
- let y: CGFloat = adjustedBounds.maxY - size.height
|
162
|
|
- return CGRect(x: x, y: y, width: size.width, height: size.height)
|
163
|
|
- }
|
164
|
|
-
|
165
|
|
- // MARK: - Exit PiP
|
|
161
|
+ // MARK: - Interactions
|
166
|
162
|
|
167
|
163
|
@objc private func toggleExitPiP() {
|
168
|
|
- guard let view = rootViewController?.view else { return }
|
169
|
|
-
|
170
|
164
|
if exitPiPButton == nil {
|
171
|
165
|
// show button
|
172
|
166
|
let exitSelector = #selector(exitPictureInPicture)
|
|
@@ -182,18 +176,40 @@ open class PiPWindow: UIWindow {
|
182
|
176
|
}
|
183
|
177
|
}
|
184
|
178
|
|
185
|
|
- @objc private func exitPiP() {
|
186
|
|
- exitPictureInPicture()
|
|
179
|
+ // MARK: - Size calculation
|
|
180
|
+
|
|
181
|
+ private func animateViewChange() {
|
|
182
|
+ UIView.animate(withDuration: 0.25) {
|
|
183
|
+ self.view.frame = self.changeViewRect()
|
|
184
|
+ self.view.setNeedsLayout()
|
|
185
|
+ }
|
|
186
|
+ }
|
|
187
|
+
|
|
188
|
+ private func changeViewRect() -> CGRect {
|
|
189
|
+ let bounds = currentBounds
|
|
190
|
+
|
|
191
|
+ guard isInPiP else {
|
|
192
|
+ return bounds
|
|
193
|
+ }
|
|
194
|
+
|
|
195
|
+ // resize to suggested ratio and position to the bottom right
|
|
196
|
+ let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets)
|
|
197
|
+ let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
|
198
|
+ height: bounds.size.height * pipSizeRatio)
|
|
199
|
+ let x: CGFloat = adjustedBounds.maxX - size.width
|
|
200
|
+ let y: CGFloat = adjustedBounds.maxY - size.height
|
|
201
|
+ return CGRect(x: x, y: y, width: size.width, height: size.height)
|
187
|
202
|
}
|
188
|
203
|
|
189
|
|
- // MARK: - Animation transition
|
|
204
|
+ // MARK: - Animation helpers
|
190
|
205
|
|
191
|
206
|
private func animateTransition(animations: @escaping () -> Void,
|
192
|
|
- completion: CompletionAction?) {
|
|
207
|
+ completion: AnimationCompletion?) {
|
193
|
208
|
UIView.animate(withDuration: 0.1,
|
194
|
209
|
delay: 0,
|
195
|
210
|
options: .beginFromCurrentState,
|
196
|
211
|
animations: animations,
|
197
|
212
|
completion: completion)
|
198
|
213
|
}
|
|
214
|
+
|
199
|
215
|
}
|