15
votes

I've got an NSView (myView) wrapped in an NSScrollView (myScrollView). Using zoom-in/out buttons, the user can alter the scale of myView. If the user is currently scrolled to a particular spot in myView, I'd like to keep that part of the view on-screen after the zooming has taken place.

I've got code that looks like this:

    // preserve current position in scrollview
    NSRect oldVisibleRect = [[myScrollView contentView] documentVisibleRect];
    NSPoint oldCenter = NSPointFromCGPoint(CGPointMake(oldVisibleRect.origin.x + (oldVisibleRect.size.width  / 2.0),
                                                       oldVisibleRect.origin.y + (oldVisibleRect.size.height / 2.0)));

    // adjust my zoom
    ++displayZoom;
    [self scaleUnitSquareToSize:NSSizeFromCGSize(CGSizeMake(0.5, 0.5))];
    [self calculateBounds];  // make sure my frame & bounds are at least as big as the visible content view
    [self display];

    // Adjust scroll view to keep the same position.
    NSRect newVisibleRect = [[myScrollView contentView] documentVisibleRect];
    NSPoint newOffset = NSPointFromCGPoint(CGPointMake((oldCenter.x * 0.5) - (newVisibleRect.size.width  / 2.0),
                                                       (oldCenter.y * 0.5) - (newVisibleRect.size.height / 2.0)));
    if (newOffset.x < 0)
        newOffset.x = 0;
    if (newOffset.y < 0)
        newOffset.y = 0;

    [[myScrollView contentView] scrollToPoint: newOffset];
    [myScrollView reflectScrolledClipView: [myScrollView contentView]];

And it seems sort of close, but it's not quite right and I can't figure out what I'm doing wrong. My two questions are:

1) Is there not a built-in something along the lines of:

   [myView adjustScaleBy: 0.5 whilePreservingLocationInScrollview:myScrollView];

2) If not, can anyone see what I'm doing wrong in my "long way around" approach, above?

Thanks!

3

3 Answers

12
votes

Keeping the same scroll position after scaling isn't easy. One thing you need to decide is what you mean by "the same" - do you want the top, middle, or bottom of the visible area before scaling to stay in place after scaling?

Or, more intuitively, do you want the position that stays in place a percentage down the visible rect equal to the percentage that you are scrolled down the document when you start (eg, so the center of the scroller's thumb doesn't move up or down during a scale, the thumb just grows or shrinks).

If you want the latter effect, one way to do it is get the NSScrollView's verticalScroller and horizontalScroller, and then read their 'floatValue's. These are normalized from 0 to 1, where '0' means you're at the top of the document and 1 means you're at the end. The nice thing about asking the scroller for this is that if the document is shorter than the NSScrollView, the scroller still returns a sane answer in all cases for 'floatValue,' so you don't have to special-case this.

After you resize, set the NSScrollView's scroll position to be the same percentage it was before the scale - but, sadly, here's where I wave my hands a little bit. I haven't done this in a while in my code, but as I recall you can't just set the NSScrollers' 'floatValue's directly - they'll LOOK scrolled, but they won't actually affect the NSScrollView.

So, you'll have to write some math to calculate the new top-left point in your document based on the percentage you want to be through it - on the y axis, for instance, it'll look like, "If the document is now shorter than the scrollView's contentView, scroll to point 0, otherwise scroll to a point that's ((height of contentView - height of documentView) * oldVerticalPercentage) down the document." X axis is of course similar.

Also, I'm almost positive you don't need a call to -display here, and in general shouldn't ever call it, ever. (-setNeedsDisplay: at most.)

-Wil

7
votes

Me thinks you like to type too much… ;-)

// instead of this:
NSPoint oldCenter = NSPointFromCGPoint(CGPointMake(oldVisibleRect.origin.x +
    (oldVisibleRect.size.width  / 2.0),

// use this:
NSPoint oldCenter = NSMakePoint(NSMidX(oldVisibleRect), NSMaxY(oldVisibleRect));

// likewise instead of this:
[self scaleUnitSquareToSize:NSSizeFromCGSize(CGSizeMake(0.5, 0.5))];

// use this:
[self scaleUnitSquareToSize:NSMakeSize(0.5, 0.5)];

// and instead of this
NSPoint newOffset = NSPointFromCGPoint(CGPointMake(
    (oldCenter.x * 0.5) - (newVisibleRect.size.width  / 2.0),
    (oldCenter.y * 0.5) - (newVisibleRect.size.height / 2.0)));

// use this:
NSPoint newOffset NSMakePoint(
    (oldCenter.x - NSWidth(newVisibleRect)) / 2.f,
    (oldCenter.y - NSHeight(newVisibleRect)) / 2.f);
7
votes

This is an old question, but I hope someone looking for this finds my answer useful...

float zoomFactor = 1.3;

-(void)zoomIn
{
    NSRect visible = [scrollView documentVisibleRect];
    NSRect newrect = NSInsetRect(visible, NSWidth(visible)*(1 - 1/zoomFactor)/2.0, NSHeight(visible)*(1 - 1/zoomFactor)/2.0);
    NSRect frame = [scrollView.documentView frame];

    [scrollView.documentView scaleUnitSquareToSize:NSMakeSize(zoomFactor, zoomFactor)];
    [scrollView.documentView setFrame:NSMakeRect(0, 0, frame.size.width * zoomFactor, frame.size.height * zoomFactor)];

    [[scrollView documentView] scrollPoint:newrect.origin];
}

-(void)zoomOut
{
    NSRect visible = [scrollView documentVisibleRect];
    NSRect newrect = NSOffsetRect(visible, -NSWidth(visible)*(zoomFactor - 1)/2.0, -NSHeight(visible)*(zoomFactor - 1)/2.0);

    NSRect frame = [scrollView.documentView frame];

    [scrollView.documentView scaleUnitSquareToSize:NSMakeSize(1/zoomFactor, 1/zoomFactor)];
    [scrollView.documentView setFrame:NSMakeRect(0, 0, frame.size.width / zoomFactor, frame.size.height / zoomFactor)];

    [[scrollView documentView] scrollPoint:newrect.origin];
}