How to optimize drawing of a UIView that contains subviews with CATiledLayer layers

1.7k views Asked by At

I have a UIView within a UIScrollView that contains many UIView subviews. Each of these subviews has a CATiledLayer layer. In addition, I have a magnifying loupe functionality that draws the container UIView within its context (along with all the subviews). The relevant code:

This is drawRect method of the loupe:

- (void)drawRect:(CGRect)rect 
{
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextClipToMask( context , loupeRect, self.maskImage);
CGContextSetFillColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGContextFillRect(context, loupeRect);

CGContextSaveGState( context );
CGContextScaleCTM(context, gridScale,  gridScale);
CGContextTranslateCTM(context, offset.x, offset.y);

CGRect rectToDraw = CGRectMake(-offset.x, -offset.y, 512, 512);
[appDelegate.gridViewController.objectContainerView drawInContext:context forRect:rectToDraw];

CGContextRestoreGState( context );

[overlayImage drawAtPoint:CGPointZero]; 
}

And this is the drawInContext:forRect method of the container UIView where the subviews are drawn:

- (void)drawInContext:(CGContextRef)ctx forRect:(CGRect)rect {

CGRect newrect = CGRectMake(rect.origin.x-1024, rect.origin.y-1024, 2048, 2048);    

for (UIView* v in [self subviews]) {
    float viewscale = v.transform.a;        
    if (CGRectIntersectsRect(newrect,v.frame)) {
        CGContextSaveGState(ctx);
        CGContextScaleCTM(ctx, viewscale, viewscale);
        CGContextTranslateCTM(ctx, v.frame.origin.x/viewscale,  v.frame.origin.y/viewscale);
        [v drawLayer:v.layer inContext:ctx];
        CGContextRestoreGState(ctx);
    }
}   

[super drawLayer:self.layer inContext:ctx];
}

And finally this is the drawRect method of the subviews with CATiledLayer:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGFloat scale = CGContextGetCTM(context).a;
    scale = (scale <= .125) ? .125 : (scale <= .250 ? .250 : (scale <= .5 ? .5 : 1));

    CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
    CGSize tileSize = tiledLayer.tileSize;

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

    int firstCol = floorf(CGRectGetMinX(rect) / tileSize.width);
    int lastCol = floorf((CGRectGetMaxX(rect)-1) / tileSize.width);
    int firstRow = floorf(CGRectGetMinY(rect) / tileSize.height);
    int lastRow = floorf((CGRectGetMaxY(rect)-1) / tileSize.height);


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

        tileRect = CGRectIntersection(self.bounds, tileRect);
            [tile drawInRect:tileRect];
        }
    }
}

Now, everything works that way I intended, however the app significantly slows down when the magnifying loupe is on and is being moved. The issue is that everytime the loupe view is moved, its drawRect method is called (so it can update the magnified contents), which subsequently calls the drawInContext method of the container UIView and so forth... resulting in all the CATiledLayers updating their image tiles everytime the loupe is moved.

As you can see I've attempted to draw a larger portion of the container view within the context of the loupe but this is where I'm stuck. I can't see how I can "buffer" a larger portion of this container view so when the loupe is moved, the subviews are redrawn only if the rect to be redrawn goes beyond the "buffered" rect.

Sorry if the code is sloppy/newby - I'm in middle of it and looking for some help.

Thanks!

1

There are 1 answers

0
Douglas On

Can you do a one time pre-render of anything the loupe/magnifier view will display? I'm not sure if the display you are trying to magnify is relatively static or constantly changing.

In my app the user taps the screen to place other views. As they touch & move their finger a magnifier view shows exactly where the item will be placed. Also, while they are actively using the magnifier the background image does not change (they may pan/zoom/move the views at other times just not while the magnifier happens to be on).

Since the display I'm magnifying is static during magnification, I was able to implement a loupe that does not override the 'draw' methods. Below is the entire implementation; it is instantiated and persisted by the ViewController/touch handler.

The trick below is to take a screenshot of the entire window and feed that to the loupe's layer.contents. Then use the layer.contentsRect to crop, zoom & position the screenshot at the touch point.

  • From the caller's touchesBegin call [_magnifier magnify...
  • And from touchesMoved call [_magnifier touchPointMovedTo...
  • Finally, touchesEnded calls [_magnifier removeFromSuperview];

MagnifierView.h

#import <UIKit/UIKit.h>
@interface MagnifierView : UIView {
    float _xscale;
    float _yscale;
    float _loupewidth;
    float _loupeheight;
    float _xzoom;
    float _yzoom;
}

- (void) magnify:(CGPoint) touchPoint;
- (void) touchPointMovedTo:(CGPoint) touchPoint;
@end

MagnifierView.m

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

#define kMagnifierDiameter 120

@implementation MagnifierView

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:CGRectMake(0, 0, kMagnifierDiameter, kMagnifierDiameter)];

    if (self) {
        self.layer.borderColor = [[UIColor darkGrayColor] CGColor];
        self.layer.borderWidth = 3;
        self.layer.cornerRadius = kMagnifierDiameter / 2;
        self.layer.masksToBounds = YES;


        UIImageView *crosshair = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"crosshairs.png"]];
        [crosshair setCenter:self.center];
        [self addSubview:crosshair];
        [crosshair release];
    }
    return self;
}

- (void) magnify:(CGPoint)touchPoint
{
    UIView *windowview = [[UIApplication sharedApplication] keyWindow];

    /// grab a screenshot of the window (sloppy; goal is to get a CGImageRef to put into this view's layer)
    UIGraphicsBeginImageContext(windowview.bounds.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [windowview.layer renderInContext:context];
    UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();  /// autoreleased
    UIGraphicsEndImageContext();


    /// while we have the image size update our positioning and zooming numbers
    _xscale = 1 / screenshot.size.width;    /// layer units are 0.0 - 1.0 so scale the numbers to that range
    _yscale = 1 / screenshot.size.height;
    _loupewidth = _xscale * CGRectGetWidth(self.frame);     /// scaled size of this view
    _loupeheight = _yscale * CGRectGetHeight(self.frame);
    _xzoom = (_xscale * 18);    /// arbitrary 16; make sure it's in scaled units (negative to zoom out)
    _yzoom = (_yscale * 18);


    /// set our layer contents to the imageRef of the screenshot above (again, sloppy; can we skip UIImage altogether?)
    CGImageRef imageRef = CGImageCreateWithImageInRect([screenshot CGImage], CGRectMake(0, 0, screenshot.size.width, screenshot.size.height));
    self.layer.contents = (id)imageRef;
    CGImageRelease(imageRef);

    /// add us to the window view and move us
    [windowview addSubview:self];
    [self touchPointMovedTo:touchPoint];
}

- (void) touchPointMovedTo:(CGPoint)touchPoint
{
    self.center = CGPointMake(touchPoint.x, touchPoint.y - 80); /// arbitrary 80 so we float 'above' your fat finger

    /// touchPoint is the center; figure out the x,y (top,left)
    float xcenter = (_xscale * touchPoint.x);
    float x = xcenter - (_loupewidth / 2);

    float ycenter = (_yscale * touchPoint.y);
    float y = ycenter - (_loupeheight / 2);

    /// with no additional adjustments this rect will just 'see through' to the screenshot
    CGRect seethrough = CGRectMake(x , y, _loupewidth, _loupeheight);

    /// RectInset with the zoom factor to scale up the contents
    self.layer.contentsRect = CGRectInset(seethrough, _xzoom, _yzoom);

}


- (void)dealloc {
    [super dealloc];
}


@end