"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-5086/ios/sdk/src/picture-in-picture/PiPViewCoordinator.swift" (22 Jun 2021, 8523 Bytes) of package /linux/misc/jitsi-meet-5086.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 Picure in Picture mode.
   30 public class PiPViewCoordinator {
   31 
   32     /// Limits the boundaries of view position on screen when minimized
   33     public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
   34                                                             left: 5,
   35                                                             bottom: 5,
   36                                                             right: 5) {
   37         didSet {
   38             dragController.insets = dragBoundInsets
   39         }
   40     }
   41 
   42     public enum Position {
   43         case lowerRightCorner
   44         case upperRightCorner
   45         case lowerLeftCorner
   46         case upperLeftCorner
   47     }
   48     
   49     public var 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 
   59     private(set) var view: UIView
   60     private var currentBounds: CGRect = CGRect.zero
   61 
   62     private var tapGestureRecognizer: UITapGestureRecognizer?
   63     private var exitPiPButton: UIButton?
   64 
   65     private let dragController: DragGestureController = DragGestureController()
   66 
   67     public init(withView view: UIView) {
   68         self.view = view
   69     }
   70 
   71     /// Configure the view to be always on top of all the contents
   72     /// of the provided parent view.
   73     /// If a parentView is not provided it will try to use the main window
   74     public func configureAsStickyView(withParentView parentView: UIView? = nil) {
   75         guard
   76             let parentView = parentView ?? UIApplication.shared.keyWindow
   77             else { return }
   78 
   79         parentView.addSubview(view)
   80         currentBounds = parentView.bounds
   81         view.frame = currentBounds
   82         view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
   83     }
   84 
   85     /// Show view with fade in animation
   86     public func show(completion: AnimationCompletion? = nil) {
   87         if view.isHidden || view.alpha < 1 {
   88             view.isHidden = false
   89             view.alpha = 0
   90 
   91             animateTransition(animations: { [weak self] in
   92                 self?.view.alpha = 1
   93             }, completion: completion)
   94         }
   95     }
   96 
   97     /// Hide view with fade out animation
   98     public func hide(completion: AnimationCompletion? = nil) {
   99         if view.isHidden || view.alpha > 0 {
  100             animateTransition(animations: { [weak self] in
  101                 self?.view.alpha = 0
  102                 self?.view.isHidden = true
  103             }, completion: completion)
  104         }
  105     }
  106 
  107     /// Resize view to and change state to custom PictureInPicture mode
  108     /// This will resize view, add a  gesture to enable user to "drag" view
  109     /// around screen, and add a button of top of the view to be able to exit mode
  110     public func enterPictureInPicture() {
  111         isInPiP = true
  112         animateViewChange()
  113         dragController.startDragListener(inView: view)
  114         dragController.insets = dragBoundInsets
  115 
  116         // add single tap gesture recognition for displaying exit PiP UI
  117         let exitSelector = #selector(toggleExitPiP)
  118         let tapGestureRecognizer = UITapGestureRecognizer(target: self,
  119                                                           action: exitSelector)
  120         self.tapGestureRecognizer = tapGestureRecognizer
  121         view.addGestureRecognizer(tapGestureRecognizer)
  122     }
  123 
  124     /// Exit Picture in picture mode, this will resize view, remove
  125     /// exit pip button, and disable the drag gesture
  126     @objc public func exitPictureInPicture() {
  127         isInPiP = false
  128         animateViewChange()
  129         dragController.stopDragListener()
  130 
  131         // hide PiP UI
  132         exitPiPButton?.removeFromSuperview()
  133         exitPiPButton = nil
  134 
  135         // remove gesture
  136         let exitSelector = #selector(toggleExitPiP)
  137         tapGestureRecognizer?.removeTarget(self, action: exitSelector)
  138         tapGestureRecognizer = nil
  139         
  140         delegate?.exitPictureInPicture()
  141     }
  142 
  143     /// Reset view to provide bounds, use this method on rotation or
  144     /// screen size changes
  145     public func resetBounds(bounds: CGRect) {
  146         currentBounds = bounds
  147         exitPictureInPicture()
  148     }
  149 
  150     /// Stop the dragging gesture of the root view
  151     public func stopDragGesture() {
  152         dragController.stopDragListener()
  153     }
  154 
  155     /// Customize the presentation of exit pip button
  156     open func configureExitPiPButton(target: Any,
  157                                      action: Selector) -> UIButton {
  158         let buttonImage = UIImage.init(named: "image-resize",
  159                                        in: Bundle(for: type(of: self)),
  160                                        compatibleWith: nil)
  161         let button = UIButton(type: .custom)
  162         let size: CGSize = CGSize(width: 44, height: 44)
  163         button.setImage(buttonImage, for: .normal)
  164         button.backgroundColor = .gray
  165         button.layer.cornerRadius = size.width / 2
  166         button.frame = CGRect(origin: CGPoint.zero, size: size)
  167         button.center = view.convert(view.center, from: view.superview)
  168         button.addTarget(target, action: action, for: .touchUpInside)
  169         return button
  170     }
  171 
  172     // MARK: - Interactions
  173 
  174     @objc private func toggleExitPiP() {
  175         if exitPiPButton == nil {
  176             // show button
  177             let exitSelector = #selector(exitPictureInPicture)
  178             let button = configureExitPiPButton(target: self,
  179                                                 action: exitSelector)
  180             view.addSubview(button)
  181             exitPiPButton = button
  182 
  183         } else {
  184             // hide button
  185             exitPiPButton?.removeFromSuperview()
  186             exitPiPButton = nil
  187         }
  188     }
  189 
  190     // MARK: - Size calculation
  191 
  192     private func animateViewChange() {
  193         UIView.animate(withDuration: 0.25) {
  194             self.view.frame = self.changeViewRect()
  195             self.view.setNeedsLayout()
  196         }
  197     }
  198 
  199     private func changeViewRect() -> CGRect {
  200         let bounds = currentBounds
  201 
  202         guard isInPiP else {
  203             return bounds
  204         }
  205 
  206         // resize to suggested ratio and position to the bottom right
  207         let adjustedBounds = bounds.inset(by: dragBoundInsets)
  208         let size = CGSize(width: 150, height: 150)
  209         let origin = initialPositionFor(pipSize: size, bounds: adjustedBounds)
  210         return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height)
  211     }
  212     
  213     private func initialPositionFor(pipSize size: CGSize, bounds: CGRect) -> CGPoint {
  214         switch initialPositionInSuperview {
  215         case .lowerLeftCorner:
  216             return CGPoint(x: bounds.minX, y: bounds.maxY - size.height)
  217         case .lowerRightCorner:
  218             return CGPoint(x: bounds.maxX - size.width, y: bounds.maxY - size.height)
  219         case .upperLeftCorner:
  220             return CGPoint(x: bounds.minX, y: bounds.minY)
  221         case .upperRightCorner:
  222             return CGPoint(x: bounds.maxX - size.width, y: bounds.minY)
  223         }
  224     }
  225 
  226     // MARK: - Animation helpers
  227 
  228     private func animateTransition(animations: @escaping () -> Void,
  229                                    completion: AnimationCompletion?) {
  230         UIView.animate(withDuration: 0.1,
  231                        delay: 0,
  232                        options: .beginFromCurrentState,
  233                        animations: animations,
  234                        completion: completion)
  235     }
  236 
  237 }