Dynamic drawing performane in iOS7 with Core Graphic

222 views Asked by At

I need some advices about drawing in iOS and more specifically about performance drawing. I read a lot of articles about the drawing in iOS7 but I didn't succeed to obtain the correct result.

I have dots than I need to link them in the correct order. Thanks to my Dot & Elastic classes, I succeed to have this render. I'm very satisfied : http://d.pr/v/nYzH

This dots represents a card and I need to use a timeline to navigate between each card. I use iCarousel library for realize this objective. I work like a UITableView except we manage view. Views can be reused etc...

But the problem start there. This is the result : http://d.pr/v/y7dq

First problem : dots have low resolution. Second real problem : I got some lags..

You can follow here my files used for draw dot & elastic.

Dot.m file

@interface Dot () {
    CAShapeLayer *_foreground;
    CAShapeLayer *_background;
    UIBezierPath *_path;
}
@end

@implementation Dot

- (id)initWithPoint:(CGPoint)point andRadius:(CGFloat)radius
{
    self = [super init];
    if (self) {
        self.frame    = CGRectMake(point.x, point.y, (radius + 10) * 2, (radius + 10) * 2);
        self.radius   = radius;
        self.color    = [UIColor colorWithHexString:@"#FFFFFF"];
        [self setPosition:point];
        [self setupPath];

        _background = [CAShapeLayer layer];
        [_background setFrame:CGRectMake(0, 0, self.width, self.height)];
        [_background setPath:_path.CGPath];
        [self.layer addSublayer:_background];
        [self drawStrokedDotToLayer:_background];

        _foreground = [CAShapeLayer layer];
        [_foreground setFrame:CGRectMake(0, 0, self.width, self.height)];
        [_foreground setPath:_path.CGPath];
        [self.layer addSublayer:_foreground];
        [self drawPlainDotToLayer:_foreground];

        [self setBackgroundColor:[UIColor clearColor]];

        self.state    = DotStateHidden;
    }
    return self;
}

- (void)setupPath
{
    _path = [UIBezierPath bezierPath];
    [_path addArcWithCenter:CGPointMake(self.width*.5f, self.height*.5f) radius:self.radius startAngle:0 endAngle:M_PI * 2 clockwise:NO];
}

#pragma mark - Setters

- (void)setPosition:(CGPoint)position
{
    self.x = position.x - self.width*.5f;
    self.y = position.y - self.width*.5f;
}

- (CGPoint)position
{
    return CGPointMake(self.x + self.width*.5f, self.y + self.height*.5f);
}

- (void)setState:(DotState)state
{
    _state = state;

    CAKeyframeAnimation *foregroundAnim = nil;
    CAKeyframeAnimation *backgroundAnim = nil;

    switch (_state) {
        case DotStateFeedback:
        {
            self.color = [UIColor colorWithHexString:@"#FFFFFF"];
            [self drawFeedback:_foreground];
            [self removeGlow:_foreground];

            foregroundAnim = [self animation:ScaleIn function:ExponentialEaseOut duration:1.f];
            break;
        }

        case DotStateVisible:
        {
            self.color = [UIColor colorWithHexString:@"#FFFFFF"];
            [self drawPlainDotToLayer:_foreground];
            [self drawGlow:_foreground];
            [self drawStrokedDotToLayer:_background];

            foregroundAnim = [self animation:ScaleIn function:ExponentialEaseOut duration:.2f];
            break;
        }
        case DotStateNext:
        {
            self.color = [UIColor colorWithHexString:@"#FFFFFF"];
            [self drawStrokedDotToLayer:_background];

            foregroundAnim = [self animation:ScaleOut function:ExponentialEaseOut duration:.16f];
            backgroundAnim = [self animation:ScaleIn function:ExponentialEaseOut duration:.25f];
            [foregroundAnim setBeginTime:CACurrentMediaTime() + .04f];
            break;
        }

        case DotStateHidden:
        default:
        {
            self.color = [UIColor colorWithHexString:@"#333333"];
            [self drawPlainDotToLayer:_foreground];
            [self removeGlow:_foreground];

            foregroundAnim = [self animation:ScaleIn function:ExponentialEaseOut duration:.5f];
            backgroundAnim = [self animation:ScaleOut function:ExponentialEaseOut duration:.25f];
            break;
        }
    }

    if (foregroundAnim) [_foreground addAnimation:foregroundAnim forKey:nil];
    if (backgroundAnim) [_background addAnimation:backgroundAnim forKey:nil];
}

#pragma mark - Drawing

- (void)drawStrokedDotToLayer:(CAShapeLayer *)layer
{
    [layer setLineWidth:2.f];
    [layer setStrokeColor:self.color.CGColor];
    [layer setFillColor:[UIColor colorWithHexString:@"#1F1F1F"].CGColor];
}

- (void)drawPlainDotToLayer:(CAShapeLayer *)layer
{
    [layer setLineWidth:2.f];
    [layer setStrokeColor:[UIColor clearColor].CGColor];
    [layer setFillColor:self.color.CGColor];
}

- (void)drawFeedback:(CAShapeLayer *)layer
{
    [layer setLineWidth:2.f];
    [layer setStrokeColor:[UIColor clearColor].CGColor];
    [layer setFillColor:[UIColor colorWithHex:0xFFFFFF andAlpha:.025f].CGColor];
}

- (void)drawGlow:(CAShapeLayer *)layer
{
    [layer setShadowRadius:8];
    [layer setShadowOpacity:1.f];
    [layer setShadowOffset:CGSizeMake(0, 0)];
    [layer setShadowColor:[UIColor whiteColor].CGColor];
    [layer didChangeValueForKey:@"path"];
}

- (void)removeGlow:(CAShapeLayer *)layer
{
    [layer setShadowRadius:0];
    [layer setShadowOpacity:0.f];
    [layer setShadowOffset:CGSizeMake(0, 0)];
    [layer setShadowColor:[UIColor clearColor].CGColor];
    [layer didChangeValueForKey:@"path"];
}

- (CAKeyframeAnimation *)animation:(NSInteger)name function:(AHEasingFunction)function duration:(CGFloat)duration
{
    CAKeyframeAnimation *animation;

    switch (name) {
        case ScaleIn:
        {
            animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale" function:function fromValue:0.f toValue:1.f];
            break;
        }

        case ScaleInFeedback:
        {
            animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale" function:function fromValue:0.f toValue:3.f];
            break;
        }

        case ScaleOut:
        {
            animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale" function:function fromValue:1.f toValue:0.f];
            break;
        }

        default:
        {
            animation = [CAKeyframeAnimation animationWithKeyPath:@"opacity" function:function fromValue:0.f toValue:1.f];
            break;
        }
    }

    animation.duration = duration;
    animation.fillMode = kCAFillModeForwards;
    animation.removedOnCompletion = NO;

    return animation;
}

@end

Elastic.m

@interface Elastic () {
    UIBezierPath *_path;
    UIImage *_image;
}

@end

@implementation Elastic

- (id)initWithDotOne:(Dot *)aDotOne DotTwo:(Dot *)aDotTwo
{
    self = [super initWithFrame:[UIScreen mainScreen].bounds];
    if (self) {
        self.dotOne = aDotOne;
        self.dotTwo = aDotTwo;

        self.displayDots = NO;
        self.feedBack    = NO;

        self.curveRadius = 4;

        [self setBackgroundColor:[UIColor clearColor]];

        _path = [UIBezierPath bezierPath];
        [self updatePath];
    }
    return self;
}

- (void)setDisplayDots:(BOOL)displayDots
{
    _displayDots = displayDots;

    if (_displayDots) {
        [self addSubview:self.dotTwo];
    } else {
        [self.dotTwo removeFromSuperview];
    }
}

- (void)drawRect:(CGRect)rect
{
    [_image drawInRect:rect];
}

- (void)updatePath
{
    // Initialize variables
    CGFloat dist  = distance(self.dotOne.position, self.dotTwo.position);
    CGFloat angle = angleBetweenPoints(self.dotOne.position, self.dotTwo.position) + M_PI * 1.5;

    // Points
    CGPoint ptA = CGPointMake(
                              self.dotOne.position.x + cosf(angle) * (self.dotOne.radius - self.curveRadius),
                              self.dotOne.position.y + sinf(angle) * (self.dotOne.radius - self.curveRadius)
                              );
    CGPoint ptB = CGPointMake(
                              self.dotOne.position.x + cosf(angle + M_PI) * (self.dotOne.radius - self.curveRadius),
                              self.dotOne.position.y + sinf(angle + M_PI) * (self.dotOne.radius - self.curveRadius)
                              );

    CGPoint ptC = CGPointMake(
                              self.dotTwo.position.x + cosf(angle) * (self.dotTwo.radius - self.curveRadius),
                              self.dotTwo.position.y + sinf(angle) * (self.dotTwo.radius - self.curveRadius)
                              );
    CGPoint ptD = CGPointMake(
                              self.dotTwo.position.x + cosf(angle + M_PI) * (self.dotTwo.radius - self.curveRadius),
                              self.dotTwo.position.y + sinf(angle + M_PI) * (self.dotTwo.radius - self.curveRadius)
                              );

    // Bezier
    CGFloat mapA = angle + M_PI_2 + map(dist, 150, 350, 0.0001, 0.0005);
    CGFloat mapB = angle + M_PI_2 - map(dist, 150, 350, 0.0001, 0.0005);

    CGPoint bzA = CGPointMake(self.dotOne.position.x + cosf(mapA) * dist*.5f, self.dotOne.position.y + sinf(mapA) * dist*.5f);
    CGPoint bzB = CGPointMake(self.dotOne.position.x + cosf(mapB) * dist*.5f, self.dotOne.position.y + sinf(mapB) * dist*.5f);

    // Start drawing path
    [_path moveToPoint:ptA];
    [_path addQuadCurveToPoint:ptC controlPoint:bzA];
    [_path addLineToPoint:ptD];
    [_path addQuadCurveToPoint:ptB controlPoint:bzB];

    [self drawBitmap];

    [self setNeedsDisplay];
}

- (void)drawBitmap
{
    _image = nil;
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    [_image drawAtPoint:CGPointZero];
    [[UIColor whiteColor] setFill];
    [_path fill];

    _image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    [_path removeAllPoints];
}

@end
1

There are 1 answers

0
jrturton On

Your videos look as if they were recorded on the simulator, not the device. Simulator testing is meaningless for performance, particularly graphics performance, since a dedicated GPU is not available to the simulator.

GPU-heavy operations will often be considerably worse on the simulator than the device for this reason.

So, I can offer you the following advice :

  1. Run performance testing on a device, ideally the slowest one you are targeting.
  2. Use the core animation instrument which will give you frame rates and time profiling, and will highlight the expensive areas of your code.
  3. Investigate the various graphical debugging options available via instruments on the device as well - offscreen rendering, redrawing and so forth

If you like iCarousel, then the author of that project, Nick Lockwood, has an excellent book on core animation for iOS, which contains a chapter about performance tuning.