// // NCViewerImageContentView.swift // Nextcloud // // Created by Suraj Thomas K on 7/10/18 Copyright © 2018 Al Tayer Group LLC.. // Modify for Nextcloud by Marino Faggiana on 04/03/2020. // // Author Marino Faggiana // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // public struct ZoomScale { public var minimumZoomScale: CGFloat public var maximumZoomScale: CGFloat public static let `default` = ZoomScale( minimum: 1.0, maximum: 5.0 ) public static let identity = ZoomScale( minimum: 1.0, maximum: 1.0 ) public init(minimum: CGFloat, maximum: CGFloat) { minimumZoomScale = minimum maximumZoomScale = maximum } } public class NCViewerImageContentView: UIScrollView { // MARK: - Exposed variables var metadata = tableMetadata() internal static var interItemSpacing: CGFloat = 0.0 internal var index: Int { didSet { resetZoom() } } internal static var contentTransformer: NCViewerImageContentTransformer = NCViewerImageDefaultContentTransformers.horizontalMoveInOut internal var position: CGFloat { didSet { updateTransform() } } internal var image: UIImage? { didSet { updateImageView() } } internal var isLoading: Bool = false { didSet { indicatorContainer.isHidden = !isLoading if isLoading { indicator.startAnimating() } else { indicator.stopAnimating() } } } internal var zoomLevels: ZoomScale? { didSet { zoomScale = ZoomScale.default.minimumZoomScale minimumZoomScale = zoomLevels?.minimumZoomScale ?? ZoomScale.default.minimumZoomScale maximumZoomScale = zoomLevels?.maximumZoomScale ?? ZoomScale.default.maximumZoomScale } } // MARK: - Private enumerations private enum Constants { static let indicatorViewSize: CGFloat = 60.0 } // MARK: - Private variables private lazy var imageView: UIImageView = { let imageView = UIImageView() imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true return imageView }() private lazy var indicator: UIActivityIndicatorView = { let indicatorView = UIActivityIndicatorView() indicatorView.style = .whiteLarge indicatorView.hidesWhenStopped = true return indicatorView }() private lazy var indicatorContainer: UIView = { let container = UIView() container.backgroundColor = NCBrandColor.sharedInstance.brand container.layer.cornerRadius = Constants.indicatorViewSize * 0.5 container.layer.masksToBounds = true return container }() private lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in let gesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:))) gesture.numberOfTapsRequired = 2 gesture.numberOfTouchesRequired = 1 return gesture }() init(index itemIndex: Int, position: CGFloat, frame: CGRect) { self.index = itemIndex self.position = position super.init(frame: frame) initializeViewComponents() } required init?(coder aDecoder: NSCoder) { fatalError("Do nto use `init?(coder:)`") } } // MARK: - View Composition and Events extension NCViewerImageContentView { private func initializeViewComponents() { addSubview(imageView) imageView.frame = frame setupIndicatorView() configureScrollView() addGestureRecognizer(doubleTapGestureRecognizer) updateTransform() } private func configureScrollView() { isMultipleTouchEnabled = true showsHorizontalScrollIndicator = false showsVerticalScrollIndicator = false contentSize = imageView.bounds.size canCancelContentTouches = false zoomLevels = ZoomScale.default delegate = self bouncesZoom = false } private func resetZoom() { setZoomScale(1.0, animated: false) imageView.transform = CGAffineTransform.identity contentSize = imageView.frame.size contentOffset = .zero } private func setupIndicatorView() { addSubview(indicatorContainer) indicatorContainer.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ indicatorContainer.widthAnchor.constraint(equalToConstant: Constants.indicatorViewSize), indicatorContainer.heightAnchor.constraint(equalToConstant: Constants.indicatorViewSize), indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor), indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor) ]) indicatorContainer.addSubview(indicator) indicator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ indicator.leadingAnchor.constraint(equalTo: indicatorContainer.leadingAnchor), indicator.trailingAnchor.constraint(equalTo: indicatorContainer.trailingAnchor), indicator.topAnchor.constraint(equalTo: indicatorContainer.topAnchor), indicator.bottomAnchor.constraint(equalTo: indicatorContainer.bottomAnchor) ]) indicatorContainer.setNeedsLayout() indicatorContainer.layoutIfNeeded() indicatorContainer.isHidden = true } internal func updateTransform() { NCViewerImageContentView.contentTransformer(self, position) } internal func handleChangeInViewSize(to size: CGSize) { let oldScale = zoomScale zoomScale = 1.0 imageView.frame = CGRect(origin: .zero, size: size) updateImageView() updateTransform() setZoomScale(oldScale, animated: false) contentSize = imageView.frame.size } func zoomScaleOne() { if zoomScale == 1 { return } let width = bounds.size.width let height = bounds.size.height let zoomRect = CGRect( x: bounds.size.width/2 - width * 0.5, y: bounds.size.height/2 - height * 0.5, width: width, height: height ) zoom(to: zoomRect, animated: true) } @objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) { let locationInImage = recognizer.location(in: imageView) let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width && imageView.frame.size.height > bounds.size.height let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale) ? minimumZoomScale : maximumZoomScale guard zoomTo != zoomScale else { return } let width = bounds.size.width / zoomTo let height = bounds.size.height / zoomTo let zoomRect = CGRect( x: locationInImage.x - width * 0.5, y: locationInImage.y - height * 0.5, width: width, height: height ) zoom(to: zoomRect, animated: true) } } // MARK: - UIScrollViewDelegate extension NCViewerImageContentView: UIScrollViewDelegate { public func viewForZooming(in scrollView: UIScrollView) -> UIView? { let shouldAllowZoom = (image != nil && position == 0.0) return shouldAllowZoom ? imageView : nil } public func scrollViewDidZoom(_ scrollView: UIScrollView) { centerImageView() } private func centerImageView() { var imageViewFrame = imageView.frame if imageViewFrame.size.width < bounds.size.width { imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0 } else { imageViewFrame.origin.x = 0.0 } if imageViewFrame.size.height < bounds.size.height { imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0 } else { imageViewFrame.origin.y = 0.0 } imageView.frame = imageViewFrame } private func updateImageView() { imageView.image = image if let contentImage = image { let imageViewSize = bounds.size let imageSize = contentImage.size var targetImageSize = imageViewSize if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height { targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height } else { targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width } imageView.frame = CGRect(origin: .zero, size: targetImageSize) } centerImageView() } }