Question

I have the following code in drawrect method. i want to draw with animation.

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

CGMutablePathRef pathRef = CGPathCreateMutable();


CGPathMoveToPoint(pathRef, NULL, a.x, a.y);
CGPathAddLineToPoint(pathRef, NULL, b.x, b.y);
CGPathAddLineToPoint(pathRef, NULL, c.x, c.y);
CGPathAddLineToPoint(pathRef, NULL, d.x, d.y);
CGPathCloseSubpath(pathRef);



CGContextAddPath(context, pathRef);
CGContextStrokePath(context);

CGContextSetBlendMode(context, kCGBlendModeClear);

CGContextAddPath(context, pathRef);
CGContextFillPath(context);

// Here i am showing animation but it does not work. is there any way to do this

-(void)showAnimation{
    [UIView beginAnimations:@"movement" context:nil];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationWillStartSelector:@selector(didStart:context:)];
    [UIView setAnimationDidStopSelector:@selector(didStop:finished:context:)];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
    [UIView setAnimationDuration:3.0f];

    [UIView setAnimationRepeatAutoreverses:YES];
    a=CGPointMake(10, 100);
    b=CGPointMake(100, 100);
    c=CGPointMake(100, 30);
    d=CGPointMake(20, 30);
    [UIView commitAnimations];
   }
}
Was it helpful?

Solution

Maybe a custom layer subclass could be what you are searching for. By doing that, you can add your own custom animatable properties, such as a progress variable, which represents the progress of the drawing, and is used in the drawInContext method to determine what to draw.

Now, if you animate that property via a CAAnimation, all that happens is that the CoreAnimation System makes a copy of your layer, changes the property a little towards the end value, calls drawInContext, changes the property again a little, calls drawInContext again and so on. That is how animation works.

The Layer subclass should look like this:

#import <QuartzCore/QuartzCore.h>

@interface AnimatedRect : CALayer
@property (nonatomic) float progress;
@end

@implementation AnimatedRect

//the animated property must be dynamic
@dynamic progress;

//initWithLayer is called when the animation starts, for making the copy
- (id)initWithLayer:(id)layer {
    self = [super initWithLayer:layer];
    if (self) {
        self.progress = layer.progress;
    }
    return self;
}

//drawInContext is called to perform drawing
-(void)drawInContext:(CGContextRef)ctx
{
    //your drawing code for the rect up to the percentage in progress
    //some math is required here, to determine which sides are drawn
    //and where the endpoint is
}

In the drawInContext Method you have to calculate where to end the last drawn line. For example, if you have a square, 12.5% is the half first line, 50% is the first two. The rect below is at 87.5% and started in the upper right corner rect at 87.5%

As an addition: If you want to animate implicitly, you need to implement the additional method -(id<CAAction>)actionForKey:(NSString *)event, in which you create a CAAnimation and return it.

One nice source about that topic is this tutorial

OTHER TIPS

This isn't the right way to go about this. Implementing drawRect: should be a last resort, particularly if all you are doing is drawing a rectangle. It doesn't play well with animation because you'd be forcing a redraw for every frame which would be very bad for performance.

You'd be better off doing one of the following:

  • Having the rectangle as a subview of the appropriate size and background colour, and animating its frame
  • Having a CAShapeLayer and animating its path.

With helpful answer from @TAKeanice I have implemented similar timeout behavior.

XYZTimeoutView

#import <UIKit/UIKit.h>

/**
 *    A view to draw timeout line.
 */
@interface XYZTimeoutView : UIImageView

/**
 *    Current progress in percent.
 */
@property(nonatomic) IBInspectable CGFloat progress;

/**
 *    Padding between outer view edge and timeout line.
 */
@property(nonatomic) IBInspectable CGFloat padding;

/**
 *    A width of timeout line.
 */
@property(nonatomic) IBInspectable CGFloat strokeWidth;

/**
 *    A duration of timeout animation in seconds.
 */
@property(nonatomic) IBInspectable CGFloat durationOfTimeoutAnimation;

/**
 *    A color of timeout line.
 */
@property(nonatomic) IBInspectable UIColor *strokeColor;

@end

#import "XYZTimeoutView.h"
#import "XYZTimeoutLayer.h"

@interface XYZTimeoutView ()

@property(nonatomic) XYZTimeoutLayer *timeoutLayer;

@end

@implementation XYZTimeoutView

#pragma mark - Creation and initialization

- (instancetype)init {
    self = [super init];

    if (self) {
        [self initialization];
        [self update];
    }

    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self) {
        [self initialization];
        [self update];
    }

    return self;
}

- (void)initialization {
    [self setTimeoutLayer:[XYZTimeoutLayer layer]];

    [[self layer] addSublayer:[self timeoutLayer]];
}

- (void)update {
    [[self timeoutLayer] setPadding:[self padding]];
    [[self timeoutLayer] setProgress:[self progress]];
    [[self timeoutLayer] setStrokeWidth:[self strokeWidth]];
    [[self timeoutLayer] setStrokeColor:[self strokeColor]];
    [[self timeoutLayer] setDurationOfTimeoutAnimation:[self durationOfTimeoutAnimation]];
}

#pragma mark - Setter methods

- (void)setProgress:(CGFloat)progress {
    _progress = MAX(0.0f, MIN(progress, 100.0f));

    [[self timeoutLayer] setFrame:[self bounds]];
    [[self timeoutLayer] setProgress:[self progress]];
}

- (void)setPadding:(CGFloat)padding {
    _padding = padding;

    [[self timeoutLayer] setPadding:[self padding]];
}

- (void)setStrokeWidth:(CGFloat)strokeWidth {
    _strokeWidth = strokeWidth;

    [[self timeoutLayer] setStrokeWidth:[self strokeWidth]];
}

- (void)setDurationOfTimeoutAnimation:(CGFloat)durationOfTimeoutAnimation {
    _durationOfTimeoutAnimation = durationOfTimeoutAnimation;

    [[self timeoutLayer]
     setDurationOfTimeoutAnimation:[self durationOfTimeoutAnimation]];
}

- (void)setStrokeColor:(UIColor *)strokeColor {
    _strokeColor = strokeColor;

    [[self timeoutLayer] setStrokeColor:[self strokeColor]];
}

@end

XYZTimeoutLayer

@import QuartzCore;
@import UIKit;

/**
 *    A layer used to animate timeout line.
 */
@interface XYZTimeoutLayer : CALayer

/**
 *    Current progress in percent.
 */
@property(nonatomic) CGFloat progress;

/**
 *    Padding between outer view edge and timeout line.
 */
@property(nonatomic) CGFloat padding;

/**
 *    A width of timeout line.
 */
@property(nonatomic) CGFloat strokeWidth;

/**
 *    A duration of timeout animation in seconds.
 */
@property(nonatomic) CGFloat durationOfTimeoutAnimation;

/**
 *    A color of timeout line.
 */
@property(nonatomic) UIColor *strokeColor;

@end

#import "XYZTimeoutLayer.h"
#import "XYZTimeoutLocation.h"

@implementation XYZTimeoutLayer

@dynamic progress;

#pragma mark - Layer creation and initialization

- (instancetype)initWithLayer:(id)layer {
    self = [super initWithLayer:layer];

    if (self && [layer isKindOfClass:[XYZTimeoutLayer class]]) {
        [self copyFrom:(XYZTimeoutLayer *)layer];
    }

  return self;
}

#pragma mark - Property methods

+ (BOOL)needsDisplayForKey:(NSString *)key {
    if ([NSStringFromSelector(@selector(progress)) isEqualToString:key]) {
        return YES;
    }

    return [super needsDisplayForKey:key];
}

#pragma mark - Animation methods

- (id<CAAction>)actionForKey:(NSString *)event {
    if ([NSStringFromSelector(@selector(progress)) isEqualToString:event]) {
        return [self createAnimationForKey:event];
    }

    return [super actionForKey:event];
}

#pragma mark - Draw

- (void)drawInContext:(CGContextRef)ctx {
    // Initialization
    CGRect rect = [self drawRect];
    CGPoint pointTopLeft = CGPointMake(rect.origin.x, rect.origin.y);
    CGPoint pointTopRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y);
    CGPoint pointBottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);
    CGPoint pointBottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
    XYZTimeoutLocation *location = [XYZTimeoutLocation endLocationForPercent:[self progress] rect:rect];

    // Draw initialization
    CGContextSetLineWidth(ctx, [self strokeWidth]);
    CGContextSetStrokeColorWithColor(ctx, [[self strokeColor] CGColor]);

    // Move to start point
    CGContextMoveToPoint(ctx, pointTopRight.x, pointTopRight.y - ([self strokeWidth] / 2));

    // Set draw path
    if(TOP == [location edge]){
        CGContextAddLineToPoint(ctx, pointBottomRight.x, pointBottomRight.y);
        CGContextAddLineToPoint(ctx, pointBottomLeft.x, pointBottomLeft.y);
        CGContextAddLineToPoint(ctx, pointTopLeft.x, pointTopLeft.y);
        CGContextAddLineToPoint(ctx, rect.origin.x + [location scope], pointTopRight.y);
    } else if(LEFT == [location edge]) {
        CGContextAddLineToPoint(ctx, pointBottomRight.x, pointBottomRight.y);
        CGContextAddLineToPoint(ctx, pointBottomLeft.x, pointBottomLeft.y);
        CGContextAddLineToPoint(ctx, pointTopLeft.x, rect.origin.y + rect.size.height - [location scope]);
    } else if(BOTTOM == [location edge]) {
        CGContextAddLineToPoint(ctx, pointBottomRight.x, pointBottomRight.y);
        CGContextAddLineToPoint(ctx, rect.origin.x + rect.size.width - [location scope], pointBottomLeft.y);
    } else if(RIGHT == [location edge]) {
        CGContextAddLineToPoint(ctx, pointBottomRight.x, rect.origin.y + [location scope] - ([self strokeWidth] / 2));
    }

    // Draw
    CGContextStrokePath(ctx);
}

#pragma mark - Helper Methods

- (void)copyFrom:(XYZTimeoutLayer *)layer {
    [self setPadding:[layer padding]];
    [self setProgress:[layer progress]];
    [self setStrokeWidth:[layer strokeWidth]];
    [self setStrokeColor:[layer strokeColor]];
    [self setDurationOfTimeoutAnimation:[layer durationOfTimeoutAnimation]];
}

- (CABasicAnimation *)createAnimationForKey:(NSString *)key {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key];

    [animation setDuration:[self durationOfTimeoutAnimation]];
    [animation setFromValue:[[self presentationLayer] valueForKey:key]];
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];

    return animation;
}

- (CGRect)drawRect {
    CGRect rect = [self bounds];

    CGRect drawRect = CGRectMake([self padding], [self padding],
                                 rect.size.width - (self.padding * 2),
                                 rect.size.height - (self.padding * 2));

    return drawRect;
}

@end

XYZTimeoutLocation

@import Foundation;
@import CoreGraphics;

/**
 *    An enum used to mark end of timeout line.
 */
typedef NS_ENUM(NSUInteger, Edge) {
    /**
     *    The end of timeout line is at top.
     */
    TOP = 0,
    /**
     *    The end of timeout line is at left.
     */
    LEFT,
    /**
     *    The end of timeout line is at bottom.
     */
    BOTTOM,
    /**
     *    The end of timeout line is at right.
     */
    RIGHT
};

/**
 *    A class that holds end of timeout line.
 */
@interface XYZTimeoutLocation : NSObject

/**
 *    The edge of the view where timeout line ended.
 */
@property(nonatomic) Edge edge;

/**
 *    The end scope to draw.
 */
@property(nonatomic) CGFloat scope;

/**
 *    Calculates scope for specified percent value for given rect.
 *
 *    @param percent A percent to calculate scope for it.
 *    @param rect    A rect to calculate scope for it.
 *
 *    @return A scope of rect for specified percent.
 */
+ (CGFloat)scopeForPercent:(CGFloat)percent rect:(CGRect)rect;

/**
 *    Returns an instance of MVPTimeoutLocation that holds edge of view and scope to draw for specified percent value for given rect.
 *
 *    @param percent A percent to calculate scope for it.
 *    @param rect    A rect to calculate scope for it.
 *
 *    @return An instance of MVPTimeoutLocation that holds edge of view and scope to draw.
 */
+ (XYZTimeoutLocation *)endLocationForPercent:(CGFloat)percent rect:(CGRect)rect;

@end

#import "XYZTimeoutLocation.h"

@implementation XYZTimeoutLocation

+ (XYZTimeoutLocation *)endLocationForPercent:(CGFloat)percent rect:(CGRect)rect {
    CGFloat scope = [XYZTimeoutLocation scopeForPercent:percent rect:rect];
    XYZTimeoutLocation *location = [[XYZTimeoutLocation alloc] init];

    if (scope > rect.size.height) {
        scope -= rect.size.height;

        if (scope > rect.size.width) {
            scope -= rect.size.width;

            if (scope > rect.size.height) {
                scope -= rect.size.height;

                location.edge = TOP;
                location.scope = scope;
            } else {
                location.edge = LEFT;
                location.scope = scope;
            }
        } else {
            location.edge = BOTTOM;
            location.scope = scope;
        }
    } else {
        location.edge = RIGHT;
        location.scope = scope;
    }

    return location;
}

+ (CGFloat)scopeForPercent:(CGFloat)percent rect:(CGRect)rect {
    CGFloat scope = (rect.size.width * 2) + (rect.size.height * 2);
    CGFloat scopeForPercent = (scope / 100) * percent;

    return scopeForPercent;
}

@end

I'm not satisfied with [XYZTimeoutLocation endLocationForPercent] method. If somebody have a better idea how to do this, please update/edit.

In UIViewController insert code below to test:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self.timeoutView setTranslatesAutoresizingMaskIntoConstraints:NO];
    [self.timeoutView setStrokeColor:[UIColor greenColor]];
    [self.timeoutView setDurationOfTimeoutAnimation:1];
    [self.timeoutView setStrokeWidth:10.0f];
    [self.timeoutView setPadding:10.0f];

    self.percent = 0;

    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        if ((self.percent = (self.percent + 10)) > 100) {
            self.percent = 0;
        }


        [self.timeoutView setProgress:self.percent];
    }];
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top