"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-6312/ios/sdk/src/picture-in-picture/PiPViewCoordinator.swift" (4 Jul 2022, 9308 Bytes) of package /linux/misc/jitsi-meet-6312.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Swift source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    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 
   17 import UIKit
   18 
   19 public typealias AnimationCompletion = (Bool) -> Void
   20 
   21 public protocol PiPViewCoordinatorDelegate: class {
   22 
   23     func exitPictureInPicture()
   24 }
   25 
   26 /// Coordinates the view state of a specified view to allow
   27 /// to be presented in full screen or in a custom Picture in Picture mode.
   28 /// This object will also provide the drag and tap interactions of the view
   29 /// when is presented in Picture in Picture mode.
   30 public class PiPViewCoordinator {
   31     
   32     public enum Position {
   33         case lowerRightCorner
   34         case upperRightCorner
   35         case lowerLeftCorner
   36         case upperLeftCorner
   37     }
   38     
   39     /// Limits the boundaries of view position on screen when minimized
   40     public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
   41                                                             left: 5,
   42                                                             bottom: 5,
   43                                                             right: 5) {
   44         didSet {
   45             dragController.insets = dragBoundInsets
   46         }
   47     }
   48 
   49     public let initialPositionInSuperView: Position = .lowerRightCorner
   50 
   51     // Unused. Remove on the next major release.
   52     @available(*, deprecated, message: "The PiP window size is now fixed to 150px.")
   53     public var c: CGFloat = 0.0
   54 
   55     public weak var delegate: PiPViewCoordinatorDelegate?
   56 
   57     private(set) var isInPiP: Bool = false // true if view is in PiP mode
   58     private(set) var view: UIView
   59     private var currentBounds: CGRect = CGRect.zero
   60 
   61     private var tapGestureRecognizer: UITapGestureRecognizer?
   62     private var exitPiPButton: UIButton?
   63 
   64     private let dragController: DragGestureController = DragGestureController()
   65 
   66     public init(withView view: UIView) {
   67         self.view = view
   68         // Required because otherwise the view will not rotate correctly.
   69         view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
   70 
   71         // Otherwise the enter/exit pip animation looks odd
   72         // when pip window is bottom left, top left or top right,
   73         // because the jitsi view content does not animate, but jumps to the new size immediately.
   74         view.clipsToBounds = true
   75     }
   76 
   77     /// Configure the view to be always on top of all the contents
   78     /// of the provided parent view.
   79     /// If a parentView is not provided it will try to use the main window
   80     public func configureAsStickyView(withParentView parentView: UIView? = nil) {
   81         guard
   82             let parentView = parentView ?? UIApplication.shared.keyWindow
   83         else {
   84             return
   85         }
   86 
   87         parentView.addSubview(view)
   88         currentBounds = parentView.bounds
   89         view.frame = currentBounds
   90         view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
   91     }
   92 
   93     /// Show view with fade in animation
   94     public func show(completion: AnimationCompletion? = nil) {
   95         if view.isHidden || view.alpha < 1 {
   96             view.isHidden = false
   97             view.alpha = 0
   98 
   99             animateTransition(animations: { [weak self] in
  100                 self?.view.alpha = 1
  101             }, completion: completion)
  102         }
  103     }
  104 
  105     /// Hide view with fade out animation
  106     public func hide(completion: AnimationCompletion? = nil) {
  107         if view.isHidden || view.alpha > 0 {
  108             animateTransition(animations: { [weak self] in
  109                 self?.view.alpha = 0
  110                 self?.view.isHidden = true
  111             }, completion: completion)
  112         }
  113     }
  114 
  115     /// Resize view to and change state to custom PictureInPicture mode
  116     /// This will resize view, add a  gesture to enable user to "drag" view
  117     /// around screen, and add a button of top of the view to be able to exit mode
  118     public func enterPictureInPicture() {
  119         isInPiP = true
  120         // Resizing is done by hand when in pip.
  121         view.autoresizingMask = []
  122 
  123         animateViewChange()
  124         dragController.startDragListener(inView: view)
  125         dragController.insets = dragBoundInsets
  126 
  127         // add single tap gesture recognition for displaying exit PiP UI
  128         let exitSelector = #selector(toggleExitPiP)
  129         let tapGestureRecognizer = UITapGestureRecognizer(target: self,
  130                                                           action: exitSelector)
  131         self.tapGestureRecognizer = tapGestureRecognizer
  132         view.addGestureRecognizer(tapGestureRecognizer)
  133     }
  134 
  135     /// Exit Picture in picture mode, this will resize view, remove
  136     /// exit pip button, and disable the drag gesture
  137     @objc public func exitPictureInPicture() {
  138         isInPiP = false
  139         // Enable autoresizing again, which got disabled for pip.
  140         view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  141 
  142         animateViewChange()
  143         dragController.stopDragListener()
  144 
  145         // hide PiP UI
  146         exitPiPButton?.removeFromSuperview()
  147         exitPiPButton = nil
  148 
  149         // remove gesture
  150         let exitSelector = #selector(toggleExitPiP)
  151         tapGestureRecognizer?.removeTarget(self, action: exitSelector)
  152         tapGestureRecognizer = nil
  153 
  154         delegate?.exitPictureInPicture()
  155     }
  156 
  157     /// Reset view to provide bounds, use this method on rotation or
  158     /// screen size changes
  159     public func resetBounds(bounds: CGRect) {
  160         currentBounds = bounds
  161 
  162         // Is required because otherwise the pip window is buggy when rotating the device.
  163         // When not in pip then autoresize will do the job.
  164         if (isInPiP) {
  165             view.frame = changeViewRect()
  166         }
  167     }
  168 
  169     /// Stop the dragging gesture of the root view
  170     public func stopDragGesture() {
  171         dragController.stopDragListener()
  172     }
  173 
  174     /// Customize the presentation of exit pip button
  175     open func configureExitPiPButton(target: Any,
  176                                      action: Selector) -> UIButton {
  177         let buttonImage = UIImage.init(named: "image-resize",
  178                                        in: Bundle(for: type(of: self)),
  179                                        compatibleWith: nil)
  180         let button = UIButton(type: .custom)
  181         let size: CGSize = CGSize(width: 44, height: 44)
  182         button.setImage(buttonImage, for: .normal)
  183         button.backgroundColor = .gray
  184         button.layer.cornerRadius = size.width / 2
  185         button.frame = CGRect(origin: CGPoint.zero, size: size)
  186         button.center = view.convert(view.center, from: view.superview)
  187         button.addTarget(target, action: action, for: .touchUpInside)
  188         return button
  189     }
  190 
  191     // MARK: - Interactions
  192     @objc private func toggleExitPiP() {
  193         if exitPiPButton == nil {
  194             // show button
  195             let exitSelector = #selector(exitPictureInPicture)
  196             let button = configureExitPiPButton(target: self,
  197                                                 action: exitSelector)
  198             view.addSubview(button)
  199             exitPiPButton = button
  200 
  201         } else {
  202             // hide button
  203             exitPiPButton?.removeFromSuperview()
  204             exitPiPButton = nil
  205         }
  206     }
  207 
  208     func animateViewChange() {
  209         UIView.animate(withDuration: 0.25) {
  210             self.view.frame = self.changeViewRect()
  211         }
  212     }
  213 
  214     private func changeViewRect() -> CGRect {
  215         let bounds = currentBounds
  216 
  217         if !isInPiP {
  218             return bounds
  219         }
  220 
  221         // resize to suggested ratio and position to the bottom right
  222         let adjustedBounds = bounds.inset(by: dragBoundInsets)
  223         let size = CGSize(width: 150, height: 150)
  224         let origin = (dragController.currentPosition ?? initialPositionInSuperView).getOriginIn(bounds: adjustedBounds, size: size)
  225 
  226         return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height)
  227     }
  228 
  229     // MARK: - Animation helpers
  230     private func animateTransition(animations: @escaping () -> Void,
  231                                    completion: AnimationCompletion?) {
  232         UIView.animate(withDuration: 0.1,
  233                        delay: 0,
  234                        options: .beginFromCurrentState,
  235                        animations: animations,
  236                        completion: completion)
  237     }
  238 
  239 }
  240 
  241 // MARK: -
  242 extension PiPViewCoordinator.Position {
  243     func getOriginIn(bounds: CGRect, size: CGSize) -> CGPoint {
  244         switch self {
  245         case .lowerLeftCorner:
  246             return CGPoint(x: bounds.minX, y: bounds.maxY - size.height)
  247         case .lowerRightCorner:
  248             return CGPoint(x: bounds.maxX - size.width, y: bounds.maxY - size.height)
  249         case .upperLeftCorner:
  250             return CGPoint(x: bounds.minX, y: bounds.minY)
  251         case .upperRightCorner:
  252             return CGPoint(x: bounds.maxX - size.width, y: bounds.minY)
  253         }
  254     }
  255 }