123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- //
- // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- // SPDX-License-Identifier: GPL-3.0-or-later
- //
- import Foundation
- @objc protocol NCZoomableViewDelegate {
- @objc func contentViewZoomDidChange(_ view: NCZoomableView, _ scale: Double)
- }
- @objcMembers class NCZoomableView: UIView, UIGestureRecognizerDelegate {
- public weak var delegate: NCZoomableViewDelegate?
- public var disablePanningOnInitialZoom = false
- var pinchGestureRecognizer: UIPinchGestureRecognizer?
- var panGestureRecognizer: UIPanGestureRecognizer?
- var doubleTapGestureRecoginzer: UITapGestureRecognizer?
- private(set) var contentView = UIView()
- var contentViewSize = CGSize()
- public var isZoomed: Bool {
- let scaleFactor = self.contentView.transform.a
- return scaleFactor != 1
- }
- override init(frame: CGRect) {
- super.init(frame: frame)
- self.addSubview(self.contentView)
- self.initGestureRecognizers()
- }
- required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- self.addSubview(self.contentView)
- self.initGestureRecognizers()
- }
- func initGestureRecognizers() {
- self.pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
- self.pinchGestureRecognizer?.delegate = self
- self.addGestureRecognizer(self.pinchGestureRecognizer!)
- self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
- self.panGestureRecognizer?.delegate = self
- self.addGestureRecognizer(self.panGestureRecognizer!)
- self.doubleTapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
- self.doubleTapGestureRecoginzer?.delegate = self
- self.doubleTapGestureRecoginzer?.numberOfTapsRequired = 2
- self.contentView.addGestureRecognizer(self.doubleTapGestureRecoginzer!)
- }
- public func replaceContentView(_ newView: UIView) {
- if let pinchGestureRecognizer = self.pinchGestureRecognizer {
- self.removeGestureRecognizer(pinchGestureRecognizer)
- }
- if let panGestureRecognizer = self.panGestureRecognizer {
- self.removeGestureRecognizer(panGestureRecognizer)
- }
- if let doubleTapGestureRecoginzer = self.doubleTapGestureRecoginzer {
- self.contentView.removeGestureRecognizer(doubleTapGestureRecoginzer)
- }
- self.contentView.removeFromSuperview()
- self.contentView = newView
- self.contentViewSize = newView.frame.size
- self.addSubview(self.contentView)
- self.initGestureRecognizers()
- self.resizeContentView()
- }
- func handlePinch(_ recognizer: UIPinchGestureRecognizer) {
- self.zoomView(view: self.contentView, toPoint: recognizer.location(in: self.contentView), usingScale: recognizer.scale)
- recognizer.scale = 1
- if recognizer.state == .ended {
- let bounds = self.contentView.bounds
- let zoomedSize = self.contentView.frame.size
- let aspectRatioContentViewSize = AVMakeRect(aspectRatio: self.contentViewSize, insideRect: bounds).size
- // Don't zoom smaller than the original size
- if zoomedSize.width < aspectRatioContentViewSize.width || zoomedSize.height < aspectRatioContentViewSize.height {
- UIView.animate(withDuration: 0.3) {
- self.resizeContentView()
- }
- } else {
- self.adjustViewPosition()
- }
- }
- }
- func handlePan(_ recognizer: UIPanGestureRecognizer) {
- if self.disablePanningOnInitialZoom, !self.isZoomed {
- return
- }
- let point = recognizer.translation(in: self.contentView)
- // We need to take the current scaling into account when panning
- // As we have the same scale factor for X and Y, we can take only one here
- let scaleFactor = self.contentView.transform.a
- self.contentView.center = CGPoint(x: self.contentView.center.x + point.x * scaleFactor, y: self.contentView.center.y + point.y * scaleFactor)
- recognizer.setTranslation(.zero, in: self.contentView)
- if recognizer.state == .ended {
- self.adjustViewPosition()
- }
- }
- func handleDoubleTap(_ recognizer: UITapGestureRecognizer) {
- if recognizer.state == .recognized {
- // We need to take the current scaling into account when panning
- // As we have the same scale factor for X and Y, we can take only one here
- let scaleFactor = self.contentView.transform.a
- UIView.animate(withDuration: 0.3) {
- if scaleFactor > 1 {
- // Set screenView's original size
- self.resizeContentView()
- } else {
- // Zoom 3x screenView into the tap point
- self.zoomView(view: recognizer.view!, toPoint: recognizer.location(in: recognizer.view!), usingScale: 3)
- }
- }
- self.adjustViewPosition()
- }
- }
- func zoomView(view: UIView, toPoint point: CGPoint, usingScale scale: CGFloat) {
- let bounds = view.bounds
- var resultPoint = point
- resultPoint.x -= bounds.midX
- resultPoint.y -= bounds.midY
- var transform = view.transform
- transform = CGAffineTransformTranslate(transform, resultPoint.x, resultPoint.y)
- transform = CGAffineTransformScale(transform, scale, scale)
- transform = CGAffineTransformTranslate(transform, -resultPoint.x, -resultPoint.y)
- view.transform = transform
- self.delegate?.contentViewZoomDidChange(self, transform.a)
- }
- func adjustViewPosition() {
- let parentSize = self.frame.size
- let size = self.contentView.frame.size
- var position = self.contentView.frame.origin
- let viewLeft = position.x
- let viewRight = position.x + size.width
- let viewTop = position.y
- let viewBottom = position.y + size.height
- // Left align screenView if it has been moved to the center (and it is wide enough)
- if viewLeft > 0, size.width >= parentSize.width {
- position = CGPoint(x: 0, y: position.y)
- }
- // Top align screenView if it has been moved to the center (and it is tall enough)
- if viewTop > 0, size.height >= parentSize.height {
- position = CGPoint(x: position.x, y: 0)
- }
- // Right align screenView if it has been moved to the center (and it is wide enough)
- if viewRight < parentSize.width, size.width >= parentSize.width {
- position = CGPoint(x: parentSize.width - size.width, y: position.y)
- }
- // Bottom align screenView if it has been moved to the center (and it is tall enough)
- if viewBottom < parentSize.height, size.height >= parentSize.height {
- position = CGPoint(x: position.x, y: parentSize.height - size.height)
- }
- // Align screenView vertically
- if size.width <= parentSize.width {
- position = CGPoint(x: parentSize.width / 2 - size.width / 2, y: position.y)
- }
- // Align screenView horizontally
- if size.height <= parentSize.height {
- position = CGPoint(x: position.x, y: parentSize.height / 2 - size.height / 2)
- }
- var frame = self.contentView.frame
- frame.origin.x = position.x
- frame.origin.y = position.y
- UIView.animate(withDuration: 0.3) {
- self.contentView.frame = frame
- }
- }
- public func resizeContentView() {
- self.contentView.transform = .identity
- self.delegate?.contentViewZoomDidChange(self, self.contentView.transform.a)
- let bounds = self.bounds
- let contentSize = self.contentViewSize
- if contentSize.width > 0, contentSize.height > 0 {
- let aspectFrame = AVMakeRect(aspectRatio: contentSize, insideRect: bounds)
- self.contentView.frame = aspectFrame
- self.contentView.center = CGPoint(x: bounds.midX, y: bounds.midY)
- } else {
- self.contentView.frame = bounds
- }
- }
- func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
- return true
- }
- }
|