首页 > 解决方案 > 在 ScrollView 内缩放和滚动 ImageView

问题描述

屏幕有一个目标视图居中。我需要更正 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
    }
}

标签: iosswiftuiscrollviewuiimageviewzooming

解决方案


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

  • 使用 aUIView作为滚动视图的“内容”
  • 将所有 4 面的“内容”视图约束到滚动视图的内容布局指南
  • 将 imageView 嵌入到该“内容”视图中
  • 约束 imageView 的顶部和前导,因此当内容视图滚动到时,它将出现在“目标”视图的右下角0,0
  • 约束 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
    }
}

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


推荐阅读