【问题标题】:Zoom and scroll ImageView inside the ScrollView在 ScrollView 内缩放和滚动 ImageView
【发布时间】:2021-03-25 01:49:52
【问题描述】:

屏幕有一个目标视图居中。我需要更正 ScrollView:

  1. 缩放后 - 图像应水平/垂直居中 如果 imageView 到屏幕边缘有距离
  2. 缩放后应该可以滚动ScrollView所以 imageView的任何部分都可以进入到aimView下
  3. 打开屏幕时,缩放设置为使图像占据 最大可能面积

现在看起来像这样:

class ScrollViewController: UIViewController, UIScrollViewDelegate {

    var scrollView: UIScrollView!
    var imageView: UIImageView!
    var image: UIImage!
    var aimView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView = UIScrollView()
        scrollView.delegate = self
        setupScrollView()

        image = #imageLiteral(resourceName: "apple")
        imageView = UIImageView(image: image)
        setupImageView()

        aimView = UIView()
        setupAimView()
    }

    func setupScrollView() {
        scrollView.backgroundColor = .yellow
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])

        scrollView.maximumZoomScale = 10
        scrollView.minimumZoomScale = 0.1
        scrollView.zoomScale = 1.0
    }

    func setupImageView() {
        imageView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(imageView)

        NSLayoutConstraint.activate([
            imageView.widthAnchor.constraint(equalToConstant: image.size.width),
            imageView.heightAnchor.constraint(equalToConstant: image.size.height),
            imageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor)
        ])
    }

    func setupAimView() {
        aimView.translatesAutoresizingMaskIntoConstraints = false
        aimView.backgroundColor = .green
        aimView.alpha = 0.7
        aimView.isUserInteractionEnabled = false
        view.addSubview(aimView)
        NSLayoutConstraint.activate([
            aimView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            aimView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
            aimView.widthAnchor.constraint(equalTo: aimView.heightAnchor),
            aimView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    // MARK: - UIScrollViewDelegate

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        imageView
    }
}

【问题讨论】:

    标签: ios swift uiscrollview uiimageview zooming


    【解决方案1】:

    实现此目的的最简单方法是使用 PDFView。

    代码:

    import PDFKit
    
    
    let pdfView = PDFView(frame: self.view.bounds)
    pdfView.displayDirection = .vertical
    pdfView.displayMode = .singlePage
    pdfView.backgroundColor = UIColor.white
            
    if let image = UIImage(named: "sample"),
       let pdfPage = PDFPage(image: image) {
                
         let pdfDoc = PDFDocument()
         pdfDoc.insert(pdfPage, at: 0)
                
         pdfView.document = pdfDoc
         pdfView.autoScales = true
         pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
                
    }
            
    self.view.addSubview(pdfView)
    

    结果:

    【讨论】:

    • 这是一个非常宽敞的解决方案,但我无法更改 pdfView 相对于目标视图的位置。是否可以像在滚动视图中一样从边缘添加填充?
    【解决方案2】:

    有几种方法可以解决这个问题……一种方法:

    • 使用UIView 作为滚动视图的“内容”
    • 将所有 4 个侧面的“内容”视图约束到滚动视图的内容布局指南
    • 将 imageView 嵌入到该“内容”视图中
    • 约束 imageView 的 Top 和 Leading 使其在内容视图滚动到 0,0 时出现在“aim”视图的右下角
    • 限制 imageView 的 Trailing 和 Bottom,使其在内容视图滚动到其最大 x 和 y 时出现在“目标”视图的左上角

    给你一个想法...

    虚线矩形是滚动视图框架。绿色矩形是“目标”视图。黄色矩形是“内容”视图。

    我们将无法使用滚动视图的内置缩放功能,因为它还会“缩放”图像视图边缘和内容视图之间的空间。相反,我们可以在滚动视图中添加UIPinchGestureRecognizer。当用户捏合缩放时,我们将获取手势的.scale 值并使用它来更改imageView 的宽度和高度常量。由于我们已将该 imageView 限制为内容视图,因此内容视图将增大/缩小 而不会更改两侧的间距。

    这是一个示例实现(它需要一个名为“apple”的资产图像):

    class PinchScroller: UIScrollView {
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        func commonInit() -> Void {
            let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
            self.addGestureRecognizer(pinchGesture)
        }
        
        var scaleStartCallback: (()->())?
        var scaleChangeCallback: ((CGFloat)->())?
    
        // assuming minimum scale of 1.0
        var minScale: CGFloat = 1.0
        // assuming maximum scale of 5.0
        var maxScale: CGFloat = 5.0
    
        private var curScale: CGFloat = 1.0
        
        @objc private func handlePinchGesture(_ gesture:UIPinchGestureRecognizer) {
            
            if gesture.state == .began {
                // inform controller scaling started
                scaleStartCallback?()
            }
            
            if gesture.state == .changed {
                // inform controller the scale changed
                let val: CGFloat = gesture.scale - 1.0
                let scale = min(maxScale, max(minScale, curScale + val))
                scaleChangeCallback?(scale)
            }
            
            if gesture.state == .ended {
                // update current scale value
                let val: CGFloat = gesture.scale - 1.0
                curScale = min(maxScale, max(minScale, curScale + val))
            }
            
        }
        
    }
    
    class AimViewController: UIViewController {
        
        var scrollView: PinchScroller!
        var imageView: UIImageView!
        var contentView: UIView!
        var aimView: UIView!
        
        var imageViewTopConstraint: NSLayoutConstraint!
        var imageViewLeadingConstraint: NSLayoutConstraint!
        var imageViewTrailingConstraint: NSLayoutConstraint!
        var imageViewBottomConstraint: NSLayoutConstraint!
    
        var imageViewWidthConstraint: NSLayoutConstraint!
        var imageViewHeightConstraint: NSLayoutConstraint!
        
        var imageViewWidthFactor: CGFloat = 1.0
        var imageViewHeightFactor: CGFloat = 1.0
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // make sure we can load the image
            guard let img = UIImage(named: "apple") else {
                fatalError("Could not load image!!!")
            }
            
            scrollView = PinchScroller()
            imageView = UIImageView()
            contentView = UIView()
            aimView = UIView()
            
            [scrollView, imageView, contentView, aimView].forEach {
                $0?.translatesAutoresizingMaskIntoConstraints = false
            }
            
            view.addSubview(scrollView)
            scrollView.addSubview(contentView)
            contentView.addSubview(imageView)
            scrollView.addSubview(aimView)
            
            // init image view width constraint
            imageViewWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: 0.0)
            imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0.0)
            
            // to handle non-1:1 ratio images
            if img.size.width > img.size.height {
                imageViewHeightFactor = img.size.height / img.size.width
            } else {
                imageViewWidthFactor = img.size.width / img.size.height
            }
            
            // init image view Top / Leading / Trailing / Bottom constraints
            imageViewTopConstraint = imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0)
            imageViewLeadingConstraint = imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0)
            imageViewTrailingConstraint = imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0)
            imageViewBottomConstraint = imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0)
            
            let safeG = view.safeAreaLayoutGuide
            let contentG = scrollView.contentLayoutGuide
            let frameG = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain scroll view to all 4 sides of safe area
                scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
                scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
                scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
                scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
                
                // constrain "content" view to all 4 sides of scroll view's content layout guide
                contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
                contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
                contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
                contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
                
                // activate these constraints
                imageViewTopConstraint,
                imageViewLeadingConstraint,
                imageViewTrailingConstraint,
                imageViewBottomConstraint,
                
                imageViewWidthConstraint,
                imageViewHeightConstraint,
                
                // "aim" view: 200x200, centered in scroll view frame
                aimView.widthAnchor.constraint(equalToConstant: 200.0),
                aimView.heightAnchor.constraint(equalTo: aimView.widthAnchor),
                aimView.centerXAnchor.constraint(equalTo: frameG.centerXAnchor),
                aimView.centerYAnchor.constraint(equalTo: frameG.centerYAnchor),
                
            ])
            
            // set the image
            imageView.image = img
            
            // disable interaction for "aim" view
            aimView.isUserInteractionEnabled = false
            // aim view translucent background color
            aimView.backgroundColor = UIColor.green.withAlphaComponent(0.25)
            
            // probably don't want scroll bouncing
            scrollView.bounces = false
            
            // set the scaling callback closures
            scrollView.scaleStartCallback = { [weak self] in
                guard let self = self else {
                    return
                }
                self.didStartScale()
            }
            scrollView.scaleChangeCallback = { [weak self] v in
                guard let self = self else {
                    return
                }
                self.didChangeScale(v)
            }
            
            contentView.backgroundColor = .yellow
            
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            // set constraint constants here, after all view have been initialized
            let aimSize: CGSize = aimView.frame.size
            
            imageViewWidthConstraint.constant = aimSize.width * imageViewWidthFactor
            imageViewHeightConstraint.constant = aimSize.height * imageViewHeightFactor
            
            let w = (scrollView.frame.width - aimSize.width) * 0.5 + aimSize.width
            let h = (scrollView.frame.height - aimSize.height) * 0.5 + aimSize.height
            
            imageViewTopConstraint.constant = h
            imageViewLeadingConstraint.constant = w
            imageViewTrailingConstraint.constant = -w
            imageViewBottomConstraint.constant = -h
            
            DispatchQueue.main.async {
                // center the content in the scroll view
                let xOffset = aimSize.width - ((aimSize.width - self.imageView.frame.width) * 0.5)
                let yOffset = aimSize.height - ((aimSize.height - self.imageView.frame.height) * 0.5)
                self.scrollView.contentOffset = CGPoint(x: xOffset, y: yOffset)
            }
        }
        
        private var startContentOffset: CGPoint = .zero
        private var startSize: CGSize = .zero
        
        func didStartScale() -> Void {
            startContentOffset = scrollView.contentOffset
            startSize = imageView.frame.size
        }
        
        func didChangeScale(_ scale: CGFloat) -> Void {
            // all sizing is based on the "aim" view
            let aimSize: CGSize = aimView.frame.size
            // starting scroll offset
            var cOffset = startContentOffset
            // starting image view width and height
            let w = startSize.width
            let h = startSize.height
            // new image view width and height
            let newW = aimSize.width * scale * imageViewWidthFactor
            let newH = aimSize.height * scale * imageViewHeightFactor
            // change image view width based on pinch scaling
            imageViewWidthConstraint.constant = newW
            imageViewHeightConstraint.constant = newH
            // adjust content offset so image view zooms from its center
            let xDiff = (newW - w) * 0.5
            let yDiff = (newH - h) * 0.5
            cOffset.x += xDiff
            cOffset.y += yDiff
            // update scroll offset
            scrollView.contentOffset = cOffset
        }
    }
    

    试试看。如果它接近你的目标,那么你就有了一个开始的地方。


    编辑

    在与scrollView.contentInset 玩了更多之后,这是一个更简单的方法。它使用标准的UIScrollView 及其缩放/平移功能,并且不需要任何额外的“缩放”计算或约束更改:

    class AimInsetsViewController: UIViewController {
        
        var scrollView: UIScrollView!
        var imageView: UIImageView!
    
        var aimView: UIView!
        
        var imageViewTopConstraint: NSLayoutConstraint!
        var imageViewLeadingConstraint: NSLayoutConstraint!
        var imageViewTrailingConstraint: NSLayoutConstraint!
        var imageViewBottomConstraint: NSLayoutConstraint!
        
        var imageViewWidthConstraint: NSLayoutConstraint!
        var imageViewHeightConstraint: NSLayoutConstraint!
        
        var imageViewWidthFactor: CGFloat = 1.0
        var imageViewHeightFactor: CGFloat = 1.0
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            var imageName: String = ""
            imageName = "apple"
    
            // testing different sized images
            //imageName = "apple228x346"
            //imageName = "zoom640x360"
            
            // make sure we can load the image
            guard let img = UIImage(named: imageName) else {
                fatalError("Could not load image!!!")
            }
            
            scrollView = UIScrollView()
            imageView = UIImageView()
            aimView = UIView()
            
            [scrollView, imageView, aimView].forEach {
                $0?.translatesAutoresizingMaskIntoConstraints = false
            }
            
            view.addSubview(scrollView)
            scrollView.addSubview(imageView)
            scrollView.addSubview(aimView)
            
            // init image view width constraint
            imageViewWidthConstraint = imageView.widthAnchor.constraint(equalToConstant: 0.0)
            imageViewHeightConstraint = imageView.heightAnchor.constraint(equalToConstant: 0.0)
            
            // to handle non-1:1 ratio images
            if img.size.width > img.size.height {
                imageViewHeightFactor = img.size.height / img.size.width
            } else {
                imageViewWidthFactor = img.size.width / img.size.height
            }
            
            let safeG = view.safeAreaLayoutGuide
            let contentG = scrollView.contentLayoutGuide
            let frameG = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain scroll view to all 4 sides of safe area
                scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
                scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
                scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
                scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
                
                // constrain "content" view to all 4 sides of scroll view's content layout guide
                imageView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
                imageView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
                imageView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
                imageView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
                
                imageViewWidthConstraint,
                imageViewHeightConstraint,
                
                // "aim" view: 200x200, centered in scroll view frame
                aimView.widthAnchor.constraint(equalToConstant: 200.0),
                aimView.heightAnchor.constraint(equalTo: aimView.widthAnchor),
                aimView.centerXAnchor.constraint(equalTo: frameG.centerXAnchor),
                aimView.centerYAnchor.constraint(equalTo: frameG.centerYAnchor),
                
            ])
            
            // set the image
            imageView.image = img
            
            // disable interaction for "aim" view
            aimView.isUserInteractionEnabled = false
            
            // aim view translucent background color
            aimView.backgroundColor = UIColor.green.withAlphaComponent(0.25)
            
            // probably don't want scroll bouncing
            scrollView.bounces = false
            
            // delegate
            scrollView.delegate = self
            
            // set max zoom scale
            scrollView.maximumZoomScale = 10.0
            
            // set min zoom scale to less than 1.0
            //  if you want to allow image view smaller than aim view
            scrollView.minimumZoomScale = 1.0
            
            // scroll view background
            scrollView.backgroundColor = .yellow
            
        }
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            // set constraint constants, scroll view insets and initial content offset here,
            //  after all view have been initialized
            let aimSize: CGSize = aimView.frame.size
            
            // aspect-fit image view to aim view
            imageViewWidthConstraint.constant = aimSize.width * imageViewWidthFactor
            imageViewHeightConstraint.constant = aimSize.height * imageViewHeightFactor
            
            // set content insets
            let f = aimView.frame
            scrollView.contentInset = .init(top: f.origin.y + f.height,
                                            left: f.origin.x + f.width,
                                            bottom: f.origin.y + f.height,
                                            right: f.origin.x + f.width)
        
            // center image view in aim view
            var c = scrollView.contentOffset
            c.x -= (aimSize.width - imageViewWidthConstraint.constant) * 0.5
            c.y -= (aimSize.height - imageViewHeightConstraint.constant) * 0.5
            scrollView.contentOffset = c
            
        }
    
    }
    
    extension AimInsetsViewController: UIScrollViewDelegate {
        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return imageView
        }
    }
    

    我认为这将更接近您的目标。

    【讨论】:

    • 添加了非常酷的手势,并配置了它们之间的交互。但他们轮流进行。仅捏合或仅滚动。在 scrollView 内部进行缩放时,您可以同时放大和滚动它。
    • @flyer2001 - 请参阅我的答案的编辑
    【解决方案3】:

    首先我在缩放后添加了填充 (scrollView.contentInset)

        var horizontalPadding: CGFloat { view.bounds.width / 4 }
        var verticalPadding: CGFloat { view.bounds.height / 4 }
    
        func setPadding() {
            let imageViewSize = imageView.frame.size
            let scrollViewSize = scrollView.bounds.size
    
            let verticalPadding = imageViewSize.height < scrollViewSize.height
                ? (scrollViewSize.height - imageViewSize.height) / 2
                : self.verticalPadding
            let horizontalPadding = imageViewSize.width < scrollViewSize.width
                ? (scrollViewSize.width - imageViewSize.width) / 2
                : self.horizontalPadding
    
            let toAimViewWidthSpacing = aimView.frame.origin.x
            let toAimViewHeightSpacing = aimView.frame.origin.y
    
            scrollView.contentInset = UIEdgeInsets(
                top: verticalPadding + toAimViewHeightSpacing ,
                left: horizontalPadding + toAimViewWidthSpacing ,
                bottom: verticalPadding + toAimViewHeightSpacing ,
                right: horizontalPadding + toAimViewWidthSpacing)
        }
    

    其次,增加了委托方法scrollViewDidZoom和scrollViewDidEndZooming

        func scrollViewDidZoom(_ scrollView: UIScrollView) {
            setPadding()
        }
    
        func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
            scrollView.contentSize = CGSize(
                width: imageView.frame.width,
                height: imageView.frame.height)
        }
    

    最后添加一个方法来使添加到 viewDidLayoutSubviews() 的图像居中

        override func viewDidLayoutSubviews() {
            centerImageInScrollView()
        }
    
        func centerImageInScrollView() {
            scrollView.contentSize = CGSize( width: imageView.frame.width, height: imageView.frame.height)
            let newContentOffsetX = (scrollView.contentSize.width - scrollView.frame.size.width) / 2
            let newContentOffsetY = (scrollView.contentSize.height - scrollView.frame.size.height) / 2
            scrollView.setContentOffset(CGPoint(x: newContentOffsetX, y: newContentOffsetY), animated: true)
        }
    

    整个代码是here

    它看起来如何

    【讨论】:

      猜你喜欢
      • 2018-06-15
      • 1970-01-01
      • 2012-12-19
      • 2012-07-11
      • 1970-01-01
      • 2012-04-25
      • 1970-01-01
      • 2018-02-22
      • 1970-01-01
      相关资源
      最近更新 更多