Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIScrollView zooming out of a view with a -ve origin

I have a UIScrollView. In this I have a UIView which has a frame with a negative origin - I need to limit the scroll view so that you can't scroll around the entire view..

I have implemented Zoom in this scrollview.

When Zooming the Scroll view will adjust the size of the Zoomable view according to the scale. BUT IT DOES NOT ADJUST THE ORIGIN.

So if I have a view with a frame of {0, -500}, {1000, 1000}

The I zoom out to a scale of 0.5, this will give me a new frame of {0, -500}, {500, 500}

Clearly this is not good, the entire view is zoomed out of the scrollview. I want the frame to be {0, -250}, {500, 500}

I can fix things a bit in the scrollViewDidZoom method by adjusting the origin correctly.. This does work, but the zoom is not smooth.. Changing the origin here causes it to jump.

I notice in the documentation for UIView it says (regarding the frame property):

Warning: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.

Not quite sure why that is.

Am I approaching this problem wrong? What is the best way to fix it?

Thanks


Below is some source code from the test app I am using:

In the ViewController..

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.bigView = [[BigView alloc] initWithFrame: CGRectMake(0, -400, 1000, 1000)];

    [self.bigScroll addSubview: bigView];
    self.bigScroll.delegate = self;
    self.bigScroll.minimumZoomScale = 0.2;
    self.bigScroll.maximumZoomScale = 5;
    self.bigScroll.contentSize = bigView.bounds.size;
}

-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView  {
    return bigView;
}

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {    
//    bigView.frame = CGRectMake(0, -400 * scrollView.zoomScale,
//                               bigView.frame.size.width, bigView.frame.size.height);

    bigView.center = CGPointMake(500 * scrollView.zoomScale, 100 * scrollView.zoomScale);
}

And then in the View...

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextFillRect(ctx, CGRectMake(100, 500, 10, 10));

    for (int i = 0; i < 1000; i += 100) {
        CGContextStrokeRect(ctx, CGRectMake(0, i, 1000, 3));        
    }
}

Note that here the jumpiness is more apparent at larger zoom scales. In my real app where there is much more drawing and processing going on the jump is more apparent at all times.

like image 260
Mongus Pong Avatar asked May 03 '13 14:05

Mongus Pong


1 Answers

You don't have to use the frame property - and should not, given Apple's very firm warning. In such cases you can usually use bounds and center to achieve your result.

In your case you can ignore all of the subview's properties. Assuming that your subview is the viewForZoomingInScrollView you can use the scrollView's contentOffset and zoomScale properties

- (void) setMinOffsets:(UIScrollView*)scrollView
    {
        CGFloat minOffsetX = MIN_OFFSET_X*scrollView.zoomScale;
        CGFloat minOffsetY = MIN_OFFSET_Y*scrollView.zoomScale;

        if ( scrollView.contentOffset.x < minOffsetX
          || scrollView.contentOffset.y < minOffsetY ) {

            CGFloat offsetX = (scrollView.contentOffset.x > minOffsetX)?
                               scrollView.contentOffset.x : minOffsetX;

            CGFloat offsetY = (scrollView.contentOffset.y > minOffsetY)?
                               scrollView.contentOffset.y : minOffsetY;

            scrollView.contentOffset = CGPointMake(offsetX, offsetY);
        }
    }

Call it from both scrollViewDidScroll and scrollViewDidZoom in your scrollView delegate. This should work smoothly, but if you have doubts you can also implement it by subclassing the scrollView and invoking it with layoutSubviews. In their PhotoScroller example, Apple centers a scrollView's content by overriding layoutSubviews - although maddeningly they ignore their own warnings and adjust the subview's frame property to achieve this.

update

The above method eliminates the 'bounce' as the scrollView hits it's limits. If you want to retain the bounce, you can directly alter the view's center property instead:

- (void) setViewCenter:(UIScrollView*)scrollView
    {
        UIView* view = [scrollView subviews][0];
        CGFloat centerX = view.bounds.size.width/2-MIN_OFFSET_X;
        CGFloat centerY = view.bounds.size.height/2-MIN_OFFSET_Y;

        centerX *=scrollView.zoomScale;
        centerY *=scrollView.zoomScale;

        view.center = CGPointMake(centerX, centerY);
    }

update 2

From your updated question (with code), I can see that neither of these solutions fix you problem. What seems to be happening is that the greater you make your offset, the jerkier the zoom movement becomes. With an offset of 100points the action is still quite smooth, but with an offset of 500points, it is unacceptably rough. This is partly related to your drawRect routine, and partly related to (too much) recalculation going on in the scrollView to display the right content. So I have another solution…

In your viewController, set your customView's bounds/frame origin to the normal (0,0). We will offset the content using layers instead. You will need to add the QuartzCore framework to your project, and #import it into your custom view.

In the custom view initialise two CAShapeLayers - one for the box, the other for the lines. If they share the same fill and stroke you would only need one CAShapeLayer (for this example I changed your fill and stroke colors). Each CAShapeLayer comes with it's own CGContext, which you can initialise once per layer with colors, linewidths etc. Then to make a CAShapelayer do it's drawing all you have to do is set it's path property with a CGPath.

#import "CustomView.h"
#import <QuartzCore/QuartzCore.h>

@interface CustomView()
@property (nonatomic, strong) CAShapeLayer* shapeLayer1;
@property (nonatomic, strong) CAShapeLayer* shapeLayer2;
@end

@implementation CustomView

    #define MIN_OFFSET_X 100
    #define MIN_OFFSET_Y 500

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initialiseLayers];
    }
    return self;
}


- (void) initialiseLayers
{
    CGRect layerBounds  = CGRectMake( MIN_OFFSET_X,MIN_OFFSET_Y
                          , self.bounds.size.width + MIN_OFFSET_X
                          , self.bounds.size.height+ MIN_OFFSET_Y);

    self.shapeLayer1 = [[CAShapeLayer alloc] init];
    [self.shapeLayer1 setFillColor:[UIColor clearColor].CGColor];
    [self.shapeLayer1 setStrokeColor:[UIColor yellowColor].CGColor];
    [self.shapeLayer1 setLineWidth:1.0f];
    [self.shapeLayer1 setOpacity:1.0f];

    self.shapeLayer1.anchorPoint = CGPointMake(0, 0);
    self.shapeLayer1.bounds = layerBounds;
    [self.layer addSublayer:self.shapeLayer1];

Setting the bounds is the critical bit. Unlike views, which clip their subviews, CALayers will draw beyond the bounds of their superlayer. You are going to start drawing MIN_OFFSET_Y points above the top of your View and MIN_OFFSET_X to the left. This allows you to draw content beyond your scrollView's content view without the scrollView having to do any extra work.

Unlike views, a superlayer does not automatically clip the contents of sublayers that lie outside its bounds rectangle. Instead, the superlayer allows its sublayers to be displayed in their entirety by default.
(Apple Docs, Building a Layer Hierarchy)

    self.shapeLayer2 = [[CAShapeLayer alloc] init];

    [self.shapeLayer2 setFillColor:[UIColor blueColor].CGColor];
    [self.shapeLayer2 setStrokeColor:[UIColor clearColor].CGColor];
    [self.shapeLayer2 setLineWidth:0.0f];
    [self.shapeLayer2 setOpacity:1.0f];

    self.shapeLayer2.anchorPoint = CGPointMake(0, 0);
    self.shapeLayer2.bounds = layerBounds;
    [self.layer addSublayer:self.shapeLayer2];

    [self drawIntoLayer1];
    [self drawIntoLayer2];
}

Set a bezier path for each shape layer, then pass it in:

- (void) drawIntoLayer1 {

    UIBezierPath* path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointMake(0,0)];

    for (int i = 0; i < self.bounds.size.height+MIN_OFFSET_Y; i += 100) {
        [path moveToPoint:
                CGPointMake(0,i)];
        [path addLineToPoint:
                CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i)];
        [path addLineToPoint:
                CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i+3)];
        [path addLineToPoint:
                CGPointMake(0, i+3)];
        [path closePath];
    }

    [self.shapeLayer1 setPath:path.CGPath];
}

- (void) drawIntoLayer2 {
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(100+MIN_OFFSET_X, MIN_OFFSET_Y, 10, 10)];
    [self.shapeLayer2 setPath:path.CGPath];
}

This obviates the need for drawRect - you only need to redraw your layers if you change the path property. Even if you do change the path property as often as you would call drawRect, the drawing should now be significantly more efficient. And as path is an animatable property, you also get animation thrown in for free if you need it.

In your case we only need to set the path once, so all of the work is done once, on initialisation.

Now you can remove any centering code from your scrollView delegate methods, it isn't needed any more.

like image 65
foundry Avatar answered Nov 30 '22 23:11

foundry