Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to zoom a view in UIScrollView with anchor point in center?

I have a view in UIScrollView in which the user can zoom into.

The view has the same size as the UIScrollView frame. However, the subviews of that view are bigger and centered. It's a container with same size as UIScrollView with centered content.

When zooming out, UIScrollView changes the scale of the content with an awkward anchor point to the upper left, rather than a centered one.

Is there a way to change this behavior such that when zooming in or out the zoom happens relative to the center rather than the upper left corner?

like image 229
openfrog Avatar asked Nov 16 '11 15:11

openfrog


1 Answers

The behavior can be changed to zooming around the center, but the technique is tricky.

Suppose you have a scroll view of size (300,300) with a single zoomable subview, self.imageView, located initially at (0,0,300,300). Now you want to zoom out. What is happening is that the contentOffset always remains (0,0) as you zoom out. This leads to the appearance that you are zooming with the anchor point at top-left corner.

However, changing the anchor point on any views will not help here; this is because the zooming behavior of the scroll view is not just a zoom transformation applied to some subview. The appearance of content in scroll view is controlled by contentInset and contentOffset, which are separate variables maintained by the scroll view. You need to modify the behavior of the content offset and the content inset during zoom.

One solution is to adjust the contentInset dynamically as the scrollview scrolls. The purpose is to keep the image always centered in the visible area of the scroll view.

When initializing the scroll view, prepare its frame like this:

 - (void) viewDidLoad {
      //// .......
      self.scrollView.zoomScale = 1;
      self.scrollView.minimumZoomScale = fminf(1, fminf(self.scrollView.frame.size.width/(self.imageView.image.size.width+1), self.scrollView.frame.size.height/(self.imageView.image.size.height+1)));
      CGFloat leftMargin = (self.scrollView.frame.size.width - self.imageView.image.size.width)*0.5;
      CGFloat topMargin = (self.scrollView.frame.size.height - self.imageView.image.size.height)*0.5;
      self.imageView.frame = CGRectMake(0, 0, self.imageView.image.size.width, self.imageView.image.size.height);
      [self assignInsetsOnScroller]; // this has to be done before settings contentOffset!
      self.scrollView.contentOffset = CGPointMake(fmaxf(0,-leftMargin), fmaxf(0,-topMargin));
      self.scrollView.contentSize = CGSizeMake(fmaxf(self.imageView.image.size.width, self.scrollView.frame.size.width+1), fmaxf(self.imageView.image.size.height, self.scrollView.frame.size.height+1));
   }

Set up a delegate for the scroll view, override scrollViewDidScroll:, and do something like this:

 - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [self assignInsetsOnScroller];
 }
 - (void) assignInsetsOnScroller {
    CGFloat leftMargin = (scrollView.frame.size.width - self.imageView.frame.size.width)*0.5;
    CGFloat topMargin = (scrollView.frame.size.height - self.imageView.frame.size.height)*0.5;
    scrollView.contentInset = UIEdgeInsetsMake(fmaxf(0, topMargin), fmaxf(0, leftMargin), 0, 0);
 }

Each pinch gesture will give rise to a scrolling action. The result is that the insets are adjusted dynamically, so that the imageView always appears to be in the center of the scroll view.


Swift 5: It's much simpler. No need to do any setup — just set the contentOffset inside scrollViewDidScroll.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let leftMargin = (scrollView.bounds.width - imageView.frame.width) * 0.5
    let topMargin = (scrollView.bounds.height - imageView.frame.height) * 0.5
    scrollView.contentInset = UIEdgeInsets(top: topMargin, left: leftMargin, bottom: 0, right: 0)
}
like image 94
winitzki Avatar answered Sep 28 '22 00:09

winitzki