Keep zoomable image in center of UIScrollView

|▌冷眼眸甩不掉的悲伤 提交于 2019-11-28 22:07:05

Awesome!

Thanks for the code :)

Just thought I'd add to this as I changed it slightly to improve the behaviour.

// make the change during scrollViewDidScroll instead of didEndScrolling...
-(void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    CGSize imgViewSize = self.imageView.frame.size;
    CGSize imageSize = self.imageView.image.size;

    CGSize realImgSize;
    if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
        realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
    }
    else {
        realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
    }

    CGRect fr = CGRectMake(0, 0, 0, 0);
    fr.size = realImgSize;
    self.imageView.frame = fr;

    CGSize scrSize = scrollView.frame.size;
    float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
    float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);

    // don't animate the change.
    scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
}

Here's my solution that works universally with any tab bar or navigation bar combination or w/o both, translucent or not.

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
  // The scroll view has zoomed, so you need to re-center the contents
  CGSize scrollViewSize = [self scrollViewVisibleSize];
  // First assume that image center coincides with the contents box center.
  // This is correct when the image is bigger than scrollView due to zoom
  CGPoint imageCenter = CGPointMake(self.scrollView.contentSize.width/2.0,
                                    self.scrollView.contentSize.height/2.0);

  CGPoint scrollViewCenter = [self scrollViewCenter];

  //if image is smaller than the scrollView visible size - fix the image center accordingly
  if (self.scrollView.contentSize.width < scrollViewSize.width) {
    imageCenter.x = scrollViewCenter.x;
  }

  if (self.scrollView.contentSize.height < scrollViewSize.height) {
    imageCenter.y = scrollViewCenter.y;
  }

  self.imageView.center = imageCenter;
}


//return the scroll view center
- (CGPoint)scrollViewCenter {
  CGSize scrollViewSize = [self scrollViewVisibleSize];
  return CGPointMake(scrollViewSize.width/2.0, scrollViewSize.height/2.0);
}


// Return scrollview size without the area overlapping with tab and nav bar.
- (CGSize) scrollViewVisibleSize {
  UIEdgeInsets contentInset = self.scrollView.contentInset;
  CGSize scrollViewSize = CGRectStandardize(self.scrollView.bounds).size;
  CGFloat width = scrollViewSize.width - contentInset.left - contentInset.right;
  CGFloat height = scrollViewSize.height - contentInset.top - contentInset.bottom;
  return CGSizeMake(width, height);
}

Why it's better than anything else I could find on SO so far:

  1. It doesn't read or modify the UIView frame property of the image view since a zoomed image view has a transform applied to it. See here what Apple says on how to move or adjust a view size when a non identity transform is applied.

  2. Starting iOS 7 where translucency for bars was introduced the system will auto adjust the scroll view size, scroll content insets and scroll indicators offsets. Thus you should not modify these in your code as well.

FYI: There're check boxes for toggling this behavior (which is set by default) in the Xcode interface builder. You can find it in the view controller attributes:

The full view controller's source code is published here.

Also you can download the whole Xcode project to see the scroll view constraints setup and play around with 3 different presets in the storyboard by moving the initial controller pointer to any the following paths:

  1. View with both translucent tab and nav bars.
  2. View with both opaque tab and nav bars.
  3. View with no bars at all.

Every option works correctly with the same VC implementation.

I think I got it. The solution is to use the scrollViewDidEndZooming method of the delegate and in that method set contentInset based on the size of the image. Here's what the method looks like:

- (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
    CGSize imgViewSize = imageView.frame.size;
    CGSize imageSize = imageView.image.size;

    CGSize realImgSize;
    if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
        realImgSize = CGSizeMake(imgViewSize.width, imgViewSize.width / imageSize.width * imageSize.height);
    }
    else {
        realImgSize = CGSizeMake(imgViewSize.height / imageSize.height * imageSize.width, imgViewSize.height);
    }

    CGRect fr = CGRectMake(0, 0, 0, 0);
    fr.size = realImgSize;
    imageView.frame = fr;

    CGSize scrSize = scrollView.frame.size;
    float offx = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
    float offy = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.25];
    scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);
    [UIView commitAnimations];
}

Note that I'm using animation on setting the inset, otherwise the image jumps inside the scrollview when the insets are added. With animation it slides to the center. I'm using UIView beginAnimation and commitAnimation instead of animation block, because I need to have the app run on iphone 3.

Tarsem Singh

Here is the swift 3 version of Genk's Answer

    func scrollViewDidZoom(_ scrollView: UIScrollView){
        let imgViewSize:CGSize! = self.imageView.frame.size;
        let imageSize:CGSize! = self.imageView.image?.size;
        var realImgSize : CGSize;
        if(imageSize.width / imageSize.height > imgViewSize.width / imgViewSize.height) {
            realImgSize = CGSize(width: imgViewSize.width,height: imgViewSize.width / imageSize.width * imageSize.height);
        }
        else {
            realImgSize = CGSize(width: imgViewSize.height / imageSize.height * imageSize.width, height: imgViewSize.height);
        }
        var fr:CGRect = CGRect.zero
        fr.size = realImgSize;
        self.imageView.frame = fr;

        let scrSize:CGSize = scrollView.frame.size;
        let offx:CGFloat = (scrSize.width > realImgSize.width ? (scrSize.width - realImgSize.width) / 2 : 0);
        let offy:CGFloat = (scrSize.height > realImgSize.height ? (scrSize.height - realImgSize.height) / 2 : 0);
        scrollView.contentInset = UIEdgeInsetsMake(offy, offx, offy, offx);

        // The scroll view has zoomed, so you need to re-center the contents
        let scrollViewSize:CGSize = self.scrollViewVisibleSize();

        // First assume that image center coincides with the contents box center.
        // This is correct when the image is bigger than scrollView due to zoom
        var imageCenter:CGPoint = CGPoint(x: self.scrollView.contentSize.width/2.0, y:
                                          self.scrollView.contentSize.height/2.0);

        let scrollViewCenter:CGPoint = self.scrollViewCenter()

        //if image is smaller than the scrollView visible size - fix the image center accordingly
        if (self.scrollView.contentSize.width < scrollViewSize.width) {
            imageCenter.x = scrollViewCenter.x;
        }

        if (self.scrollView.contentSize.height < scrollViewSize.height) {
            imageCenter.y = scrollViewCenter.y;
        }

        self.imageView.center = imageCenter;

    }
    //return the scroll view center
    func scrollViewCenter() -> CGPoint {
        let scrollViewSize:CGSize = self.scrollViewVisibleSize()
        return CGPoint(x: scrollViewSize.width/2.0, y: scrollViewSize.height/2.0);
    }
    // Return scrollview size without the area overlapping with tab and nav bar.
    func scrollViewVisibleSize() -> CGSize{

        let contentInset:UIEdgeInsets = self.scrollView.contentInset;
        let scrollViewSize:CGSize = self.scrollView.bounds.standardized.size;
        let width:CGFloat = scrollViewSize.width - contentInset.left - contentInset.right;
        let height:CGFloat = scrollViewSize.height - contentInset.top - contentInset.bottom;
        return CGSize(width:width, height:height);
    }

Here is an extension tested on Swift 3.1. Just create a separate *.swift file and paste the code below:

import UIKit

extension UIScrollView {

    func applyZoomToImageView() {
        guard let imageView = delegate?.viewForZooming?(in: self) as? UIImageView else { return }
        guard let image = imageView.image else { return }
        guard imageView.frame.size.valid && image.size.valid else { return }
        let size = image.size ~> imageView.frame.size
        imageView.frame.size = size
        self.contentInset = UIEdgeInsets(
            x: self.frame.size.width ~> size.width,
            y: self.frame.size.height ~> size.height
        )
        imageView.center = self.contentCenter
        if self.contentSize.width < self.visibleSize.width {
            imageView.center.x = self.visibleSize.center.x
        }
        if self.contentSize.height < self.visibleSize.height {
            imageView.center.y = self.visibleSize.center.y
        }
    }

    private var contentCenter: CGPoint {
        return CGPoint(x: contentSize.width / 2, y: contentSize.height / 2)
    }

    private var visibleSize: CGSize {
        let size: CGSize = bounds.standardized.size
        return CGSize(
            width:  size.width - contentInset.left - contentInset.right,
            height: size.height - contentInset.top - contentInset.bottom
        )
    }
}

fileprivate extension CGFloat {

    static func ~>(lhs: CGFloat, rhs: CGFloat) -> CGFloat {
        return lhs > rhs ? (lhs - rhs) / 2 : 0.0
    }
}

fileprivate extension UIEdgeInsets {

    init(x: CGFloat, y: CGFloat) {
        self.bottom = y
        self.left = x
        self.right = x
        self.top = y
    }
}

fileprivate extension CGSize {

    var valid: Bool {
        return width > 0 && height > 0
    }

    var center: CGPoint {
        return CGPoint(x: width / 2, y: height / 2)
    }

    static func ~>(lhs: CGSize, rhs: CGSize) -> CGSize {
        switch lhs > rhs {
        case true:
            return CGSize(width: rhs.width, height: rhs.width / lhs.width * lhs.height)
        default:
            return CGSize(width: rhs.height / lhs.height * lhs.width, height: rhs.height)
        }
    }

    static func >(lhs: CGSize, rhs: CGSize) -> Bool {
        return lhs.width / lhs.height > rhs.width / rhs.height
    }
}

The way to use:

extension YOUR_SCROLL_VIEW_DELEGATE: UIScrollViewDelegate {

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

    func scrollViewDidZoom(_ scrollView: UIScrollView){
        scrollView.applyZoomToImageView()
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!