element-ios/Riot/Managers/Call/PiPView.swift

242 lines
8.2 KiB
Swift

//
// Copyright 2020-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import UIKit
@objc enum PiPViewPosition: Int {
case bottomLeft
case bottomRight // default value
case topRight
case topLeft
}
@objc protocol PiPViewDelegate: AnyObject {
@objc optional func pipView(_ view: PiPView, didMoveTo position: PiPViewPosition)
@objc optional func pipViewDidTap(_ view: PiPView)
}
@objcMembers
class PiPView: UIView {
private enum Defaults {
static let margins: UIEdgeInsets = UIEdgeInsets(top: 64, left: 20, bottom: 64, right: 20)
static let cornerRadius: CGFloat = 8
static let animationDuration: TimeInterval = 0.25
}
var margins: UIEdgeInsets = Defaults.margins {
didSet {
guard self.superview != nil else { return }
self.move(to: self.position, animated: true)
}
}
var cornerRadius: CGFloat = Defaults.cornerRadius {
didSet {
layer.cornerRadius = cornerRadius
}
}
var position: PiPViewPosition = .bottomRight
weak var delegate: PiPViewDelegate?
private var originalCenter: CGPoint = .zero
private var isMoving: Bool = false
private lazy var tapGestureRecognizer: UITapGestureRecognizer = {
return UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
}()
private lazy var panGestureRecognizer: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
}()
private var rotationObserver: NSObjectProtocol?
init() {
super.init(frame: .zero)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
var contentView: UIView? {
willSet {
if let contentView = contentView {
// restore properties of old content view
contentView.isUserInteractionEnabled = true
}
} didSet {
if let contentView = contentView {
contentView.isUserInteractionEnabled = false
addSubview(contentView)
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
}
func move(in view: UIView? = nil,
to position: PiPViewPosition = .bottomRight,
targetSize: CGSize? = nil,
animated: Bool = false,
completion: ((Bool) -> Void)? = nil) {
let block = {
let targetFrame = self.targetFrame(for: position, in: view, targetSize: targetSize)
self.frame = targetFrame
}
if animated {
UIView.animate(withDuration: Defaults.animationDuration, animations: block) { (completed) in
self.position = position
completion?(completed)
}
} else {
block()
self.position = position
completion?(true)
}
}
deinit {
if let rotationObserver = rotationObserver {
NotificationCenter.default.removeObserver(rotationObserver)
}
}
// MARK: - Private
private func setup() {
isUserInteractionEnabled = true
clipsToBounds = true
layer.cornerRadius = cornerRadius
addGestureRecognizer(tapGestureRecognizer)
addGestureRecognizer(panGestureRecognizer)
rotationObserver = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [weak self] (_) in
guard let self = self else { return }
guard self.superview != nil else { return }
self.move(to: self.position, animated: true)
}
}
private func targetFrame(for position: PiPViewPosition,
in view: UIView?,
targetSize: CGSize?) -> CGRect {
guard let view = view ?? superview else {
return .zero
}
let targetSize = targetSize ?? frame.size
var superviewWidth: CGFloat = 0
var superviewHeight: CGFloat = 0
if UIDevice.current.orientation.isPortrait {
superviewWidth = min(view.bounds.width, view.bounds.height)
superviewHeight = max(view.bounds.width, view.bounds.height)
} else {
superviewWidth = max(view.bounds.width, view.bounds.height)
superviewHeight = min(view.bounds.width, view.bounds.height)
}
switch position {
case .bottomLeft:
let origin = CGPoint(x: margins.left + view.safeAreaInsets.left,
y: superviewHeight - view.safeAreaInsets.bottom - targetSize.height - margins.bottom)
return CGRect(origin: origin,
size: targetSize)
case .bottomRight:
let origin = CGPoint(x: superviewWidth - view.safeAreaInsets.right - margins.right - targetSize.width,
y: superviewHeight - view.safeAreaInsets.bottom - targetSize.height - margins.bottom)
return CGRect(origin: origin,
size: targetSize)
case .topRight:
let origin = CGPoint(x: superviewWidth - view.safeAreaInsets.right - margins.right - targetSize.width,
y: margins.top + view.safeAreaInsets.top)
return CGRect(origin: origin,
size: targetSize)
case .topLeft:
let origin = CGPoint(x: margins.left + view.safeAreaInsets.left,
y: margins.top + view.safeAreaInsets.top)
return CGRect(origin: origin,
size: targetSize)
}
}
@objc private func tapped(_ sender: UITapGestureRecognizer) {
delegate?.pipViewDidTap?(self)
}
@objc private func panned(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .possible:
isMoving = false
case .began:
originalCenter = center
isMoving = true
case .changed:
let translation = sender.translation(in: superview)
if isMoving {
center = CGPoint(x: originalCenter.x + translation.x,
y: originalCenter.y + translation.y)
}
case .ended:
defer {
isMoving = false
}
if isMoving {
guard let superview = self.superview else { return }
let translation = sender.translation(in: superview)
let bounds = superview.bounds
let midX = bounds.width / 2
let midY = bounds.height / 2
let onLeftHalf = originalCenter.x + translation.x < midX
let onTopHalf = originalCenter.y + translation.y < midY
var newPosition: PiPViewPosition = .bottomLeft
if onLeftHalf {
if onTopHalf {
newPosition = .topLeft
} else {
newPosition = .bottomLeft
}
} else {
if onTopHalf {
newPosition = .topRight
} else {
newPosition = .bottomRight
}
}
move(to: newPosition) { (_) in
self.delegate?.pipView?(self, didMoveTo: newPosition)
}
}
case .cancelled:
isMoving = false
case .failed:
isMoving = false
@unknown default:
isMoving = false
}
}
}