UIScrollView and Zooming, what is actually going on?

Discussion in 'iOS Programming' started by seepel, Mar 13, 2011.

  1. seepel macrumors 6502

    seepel

    Joined:
    Dec 22, 2009
    #1
    So I get that changing the bounds on the UIScrollView pans, but what is actually happening when it zooms? Does it set it's subviews transforms? The reason I ask is that I am trying to change the contentSize while the view is zoomed and I am getting some strange behavior.

    I have setup a subclass of UIScrollView with a contentView variable that is simply a UIView added as a subview of the UIScrollView. I then add other UIViews to the contentView. I check to see if the added view extends beyond the frame of the contentView. If it does, I adjust the contentView's frame accordingly and set the scrollView's contentSize to the contentView's frame size. I then zoom to the contentView's frame if needed.

    Code:
    - (void)packSubview:(UIView *)view {
        // Animate things slowly so that I can see what is happening
        [UIView beginAnimations:@"packView" context:nil];
        [UIView setAnimationDuration:1.5];
    
        // View's frame is initially set assuming an origin at the center of the view
        view.frame = CGRectMake(self.centerOfContentView.x+view.frame.origin.x, 
                                self.centerOfContentView.y+view.frame.origin.y, 
                                view.frame.size.width, 
                                view.frame.size.height);
    
        // Check if view's origin is negative in either direction, if so, store the offset
        CGPoint offset = CGPointZero;
        BOOL doMove = NO;
    
        if(view.frame.origin.x < 0) {
            offset.x = -view.frame.origin.x;
        }
        if(view.frame.origin.y < 0) {
            offset.y = -view.frame.origin.y;
        }
        [self.scrollView.contentView addSubview:view];
    
        // Move subviews of contentView, such that the new subview doesn't have a negative origin in either direction, moving them by the offset
        // At the same time check to see if the size of the contentView needs to be extended to accomodate the shift
        CGSize size = self.scrollView.contentView.frame.size;
        for(UIView *subview in self.scrollView.contentView.subviews) {
            subview.center = CGPointMake(subview.center.x+offset.x, 
                                         subview.center.y+offset.y);
            // Record the size that would be required such that the view does not extend beyond the frame of the contentView
            CGSize sizeNeededForView = CGSizeMake(subview.frame.origin.x+subview.frame.size.width, 
                                                  subview.frame.origin.y+subview.frame.size.height);
    
            // If the size required for the view is greater than the current size save that size
            if(sizeNeededForView.width > size.width)
                size.width = sizeNeededForView.width;
            if(sizeNeededForView.height > size.height)
                size.height = sizeNeededForView.height;
        }
    
        // Move the center position of the view for the next view that is added
        centerOfContentView_.x += offset.x;
        centerOfContentView_.y += offset.y;
    
        // Set the frame of the contentView with the size stored earlier
        self.scrollView.contentView.frame = CGRectMake(0, 0, size.width, size.height);
    
        // Set the contentSize of the scrollView to be the size of the contentView
        self.scrollView.contentSize = self.scrollView.contentView.frame.size;
    
        // Find the zoomScale needed to show the entire contentView
        CGFloat scaleX = self.frame.size.width/self.scrollView.contentView.frame.size.width;
        CGFloat scaleY = self.frame.size.height/self.scrollView.contentView.frame.size.height;
        CGFloat scale = scaleX;
        if(scaleY < scale)
            scale = scaleY;
    
        // If the scale is less than the current minimumZoomScale set the minimumZoomScale and zoom to the contentView's frame
        if(scale < self.scrollView.minimumZoomScale) {
            BOOL doZoom = self.scrollView.zoomScale == self.scrollView.minimumZoomScale;
            self.scrollView.minimumZoomScale = scale;
    
            // Only zoom if the scrollView is already zoomed all the way out. It would be disruptive if the user zoomed in and we changed that on them.
            if(doZoom)
                [self.scrollView zoomToRect:self.scrollView.contentView.frame animated:YES];
        }
        [UIView commitAnimations];
    }
    
    
    What I expect to happen here is that as I add views to the contentView the contentView's frame and scrollView's contentSize should grow to accomodate any view I insert, while zooming out to show the full contentView.

    If I don't zoom while adding the subviews I get the appropriate frame/contentSize. However, if I zoom while adding the subviews, the frame/contentSize appear to be correct and the view zooms as I would expect it to, however the scrollView region seems to get way too large, meaning I can scroll well past the contentView, which I don't quite understand seeing as the scrollView's contentSize ends up the same either way. So I must not understand what is actually going on when I zoom. Should I adjust the contentSize I am setting by the zoomScale? When I try that it initially looks ok, but then after I zoom manually the large scrollView region returns.

    Any help would be greatly appreciated, this one has me stumped!
     
  2. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #2
    I'm sure I don't know the answer but I think you're not supposed to use the frame when a view has had a transform applied. Only bounds and center.
     
  3. seepel thread starter macrumors 6502

    seepel

    Joined:
    Dec 22, 2009
    #3
    I initially expected that, maybe I just can't figure out the logic yet, but if I only use bounds and center, things go wacky pretty darn quickly. I then thought that since I was doing things relative to the scrollView that I should be using the frame. I'll play around some more using only bounds.
     
  4. seepel thread starter macrumors 6502

    seepel

    Joined:
    Dec 22, 2009
    #4
    After working with it a bit more, I've learned a few things. I was probably making several mistakes that were fixed by cleaning up my code a bit.

    One factor that I learned is that contentSize seems to get automatically set to the bounds size of the UIView returned by viewForZoomingInScrollView: Further it scales by the zoomScale so when setting the contentSize I had to take into account the zoomScale.

    I also realized that as I grew my contentView's bounds I needed to reposition it in the scrollView, I decided to just always keep it centered.

    Now the only thing left buggy is that this method doesn't quite get the bounds size of the contentView, views end up extending passed the bounds by just a little bit (though apparently more than floating point precision errors would account for).

    Code:
    - (void)packSubview:(UIView *)view {
        [self.contentView addSubview:view];
        
        // View's position is currently relative to the center of contentView, position it relative to the top left corner
        view.center = CGPointMake((self.contentView.bounds.size.width - self.contentView.bounds.origin.x)*0.5 + view.center.x, 
                                  (self.contentView.bounds.size.height - self.contentView.bounds.origin.y)*0.5 + view.center.y);
        
        // Using UIEdgeInsets to store portions of the view that lie outside of the contentView bounds
        UIEdgeInsets viewInsets = UIEdgeInsetsMake(self.contentView.bounds.origin.y - view.frame.origin.y, 
                                                   self.contentView.bounds.origin.x - view.frame.origin.x, 
                                                   view.frame.origin.y + view.frame.size.height - (self.contentView.bounds.origin.y + self.contentView.bounds.size.height), 
                                                   view.frame.origin.x + view.frame.size.width - (self.contentView.bounds.origin.x + self.contentView.bounds.size.width));
        
        // Only interested if the insets are positive as negative numbers indicate that it is inside bounds
        if(viewInsets.top <= 0)
            viewInsets.top = 0;
        if(viewInsets.left <= 0)
            viewInsets.left = 0;
        if(viewInsets.bottom <= 0)
            viewInsets.bottom = 0;
        if(viewInsets.right <= 0)
            viewInsets.right = 0;
        
        // sizeDifference for convenience
        CGSize sizeDifference = CGSizeMake(viewInsets.left + viewInsets.right, viewInsets.top + viewInsets.bottom);
        
        // increase the countView's bounds to accomodate the new view
        self.contentView.bounds = CGRectMake(0, 0, self.contentView.bounds.size.width + sizeDifference.width, self.contentView.bounds.size.height + sizeDifference.height);
        
        // set the contentSize of the scrollview to accomodate the new bounds
        self.scrollView.contentSize = CGSizeMake(self.contentView.bounds.size.width*self.scrollView.zoomScale, self.contentView.bounds.size.height*self.scrollView.zoomScale);
        
        [self centerContentView];
        
        [UIView beginAnimations:@"packView" context:nil];
        [UIView setAnimationDuration:0.5];
        
        // Reposition the subviews of contentView based on the new size
        for(UIView *subview in self.contentView.subviews) {
            subview.center = CGPointMake(subview.center.x+sizeDifference.width*0.5, subview.center.y+sizeDifference.height*0.5);
        }
        
        // Want to be able to zoom out to see the entire contentView, find the smallest scale needed
        CGFloat scaleX = self.scrollView.bounds.size.width/self.contentView.bounds.size.width;
        CGFloat scaleY = self.scrollView.bounds.size.height/self.contentView.bounds.size.height;
        CGFloat scale = scaleX;
        if(scaleY < scale)
            scale = scaleY;
        if(scale < self.scrollView.minimumZoomScale) {
            BOOL doZoom = (self.scrollView.zoomScale == self.scrollView.minimumZoomScale);
            self.scrollView.minimumZoomScale = scale;
            // Only want to zoom if the contentView is completely visible otherwise this would be rather jarring
            if(doZoom)
                self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
        }
        
        [UIView commitAnimations];
    }
    
    - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
        [self centerContentView];
    }
    
    - (void)centerContentView {
        // Store any size differences between the contentView and the scrollView so that we can center it
        CGPoint offset = CGPointMake((self.scrollView.frame.size.width-self.scrollView.contentSize.width)*0.5, (self.scrollView.frame.size.height-self.scrollView.contentSize.height)*0.5);
        
        // Only interested if the contentView is smaller than the scrollView
        if(offset.x < 0)
            offset.x = 0;
        if(offset.y < 0)
            offset.y = 0;
        
        // Center the contentView in the scrollView
        self.contentView.center = CGPointMake(self.scrollView.contentSize.width*0.5+offset.x,self.scrollView.contentSize.height*0.5+offset.y);
    }
    
     
  5. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #5
    I've worked with the PhotoScroller sample code recently. It includes a UIScrollview subclass that implements layoutSubviews and centers its subview there. Also the drawing code looks at the current context's transform to get the current scale.
     
  6. seepel thread starter macrumors 6502

    seepel

    Joined:
    Dec 22, 2009
    #6
    Thanks for the tip... I did end up working this part out, though the tiling done there will probably be useful. The trouble now seems to be that my algorithm for resizing the contentView as I add subviews doesn't quite get the job done. It comes close, but seems to miss a handful of pixels here and there, such that some of the subviews I add extend slightly past the bounds of the contentView.
     

Share This Page