Question

This has been driving me crazy.. I have a large image, and need to have a view that is both zoomable, and scrollable (ideally it should also be able to rotate, but I've given up on that part). Since the image is very large, I plan on using CATiledLayer, but I simply can't get it to work. My requirements are:

  • I need to be able to zoom (on mouse center) and pan

  • The image should not change its width:height ratio (shouldn't resize, only zoom).

  • This should run on Mac OS 10.9 (NOT iOS!)

  • Memory use shouldn't be huge (although up to like 100 MB should be ok).

I have the necessary image both complete in one file, and also tiled into many (even have it for different zoom levels). I prefer using the tiles, as that should be easier on memory, but both options are available.

Most of the examples online refer to iOS, and thus use UIScrollView for the zoom/pan, but I can't get to copy that behaviour for NSScrollView. The only example for Mac OS X I found is this, but his zoom always goes to the lower left corner, not the middle, and when I adapt the code to use png files instead of pdf, the memory use gets around 400 MB...

This is my best try so far:

@implementation MyView{
    CATiledLayer *tiledLayer;
}
-(void)awakeFromNib{
    NSLog(@"Es geht los");
    tiledLayer = [CATiledLayer layer];

    // set up this view & its layer
    self.wantsLayer = YES;
    self.layer = [CALayer layer];
    self.layer.masksToBounds = YES;
    self.layer.backgroundColor = CGColorGetConstantColor(kCGColorWhite);

    // set up the tiled layer
    tiledLayer.delegate = self;
    tiledLayer.levelsOfDetail = 4;
    tiledLayer.levelsOfDetailBias = 5;
    tiledLayer.anchorPoint = CGPointZero;
    tiledLayer.bounds = CGRectMake(0.0f, 0.0f, 41*256, 22*256);
    tiledLayer.autoresizingMask = kCALayerNotSizable;
    tiledLayer.tileSize = CGSizeMake(256, 256);
    self.frame = CGRectMake(0.0f, 0.0f, 41*256, 22*256);
    self.layer = tiledLayer;
    //[self.layer addSublayer:tiledLayer];
    [tiledLayer setNeedsDisplay];

}

-(void)drawRect:(NSRect)dirtyRect{
    CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];


    CGFloat scale = CGContextGetCTM(context).a;

    CGSize tileSize = tiledLayer.tileSize;


    tileSize.width /= scale;
    tileSize.height /= scale;

    // calculate the rows and columns of tiles that intersect the rect we have been asked to draw
    int firstCol = floorf(CGRectGetMinX(dirtyRect) / tileSize.width);
    int lastCol = floorf((CGRectGetMaxX(dirtyRect)-1) / tileSize.width);
    int firstRow = floorf(CGRectGetMinY(dirtyRect) / tileSize.height);
    int lastRow = floorf((CGRectGetMaxY(dirtyRect)-1) / tileSize.height);

    for (int row = firstRow; row <= lastRow; row++) {
        for (int col = firstCol; col <= lastCol; col++) {
            NSImage *tile = [self tileForScale:scale row:row col:col];
            CGRect tileRect = CGRectMake(tileSize.width * col, tileSize.height * row,
                                         tileSize.width, tileSize.height);

            // if the tile would stick outside of our bounds, we need to truncate it so as
            // to avoid stretching out the partial tiles at the right and bottom edges
            tileRect = CGRectIntersection(self.bounds, tileRect);

            [tile drawInRect:tileRect];
        }
    }
}

-(BOOL)isFlipped{
    return YES;
}

But this deforms the image, and doesn't zoom or pan correctly (but at least the tile selection works)...

I can't believe this is so hard, any help would be greatly appreciated. Thanks :)

Was it helpful?

Solution

After a lot of research and tries, I finally managed to get this to work using this example. Decided to post it for future reference. Open the ZIP > CoreAnimationLayers> TiledLayers, there's a good example there. That's how CATiledLayer works with OS X, and since the example there doesn't handle zoom very well, I leave here my zoom code

-(void)magnifyWithEvent:(NSEvent *)event{
    [super magnifyWithEvent:event];

    if (!isZooming) {
        isZooming = YES;

        BOOL zoomOut = (event.magnification > 0) ? NO : YES;

        if (zoomOut) {
            [self zoomOutFromPoint:event.locationInWindow];

        } else {
            [self zoomInFromPoint:event.locationInWindow];;
        }

    }

}

-(void)zoomInFromPoint:(CGPoint)mouseLocationInWindow{
    if(zoomLevel < pow(2, tiledLayer.levelsOfDetailBias)) {
        zoomLevel *= 2.0f;

        tiledLayer.transform = CATransform3DMakeScale(zoomLevel, zoomLevel, 1.0f);
        tiledLayer.position = CGPointMake((tiledLayer.position.x*2) - mouseLocationInWindow.x, (tiledLayer.position.y*2) - mouseLocationInWindow.y);

    }

}

-(void)zoomOutFromPoint:(CGPoint)mouseLocationInWindow{
    NSInteger power = tiledLayer.levelsOfDetail - tiledLayer.levelsOfDetailBias;
    if(zoomLevel > pow(2, -power)) {
        zoomLevel *= 0.5f;

        tiledLayer.transform = CATransform3DMakeScale(zoomLevel, zoomLevel, 1.0f);
        tiledLayer.position = CGPointMake((tiledLayer.position.x + mouseLocationInWindow.x)/2, (tiledLayer.position.y + mouseLocationInWindow.y)/2);

    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top