4
votes

Im having trouble figuring out why the animation is flickering from the fromValue to the toValue after my animation block is complete. I know that after you complete an animation you have to set the values of the CALayer to the end state of the animation to keep it looking consistent. Yet no matter what order i call these methods in i keep getting the flickering result. What I'm doing is drawing a checkmark with a biezer path and then once the strokeEnd animation is complete i fill in the check mark by animating the fillColor property. The fill in checkmark function and reset checkmark function are all triggered when the user selects the row of the tableviewcell the checkmark is associated with. Oh ya and I am using AutoLayout if that makes a difference.

So I'm wondering a few things actually one 1) Once the table view cell is being displayed i call the public function shoudSetCheckmarkToCheckedState which sets the boolean self.userSelectedCheckmark to the parameter isChecked thats passed in the function. From there i call [self setNeedsLayout],which triggers layoutSubviews and it calls the shouldDrawCheckmark... function. The reason why I do this is because if I don't the first time the cell is drawn there is no frame set so my drawing looks a mess. So should I be calling setNeedsLayout every time i change the userSelectedCheckmark property or is there a better way.

2) Why is the checkmark flickering once the animation is complete. I think i know why, its because when the animation completes the layers properties are reset to the same state they were in when the layer started animating. So then how can I fix this? I would just trigger a timer to change the fill color a millisecond before the animation ends but that doesn't feel right.

heres the code btw

typedef void (^animationCompletionBlock)(void);
#define kAnimationCompletionBlock @"animationCompletionBlock"

#import "CheckmarkView.h"
#import "UIColor+HexString.h"

@interface CheckmarkView()
@property (nonatomic,strong) CAShapeLayer *checkmarkLayer;
@property (nonatomic,assign) BOOL userSelectedCheckmark;
- (void)shouldDrawCheckmarkToLayerWithAnimation:(BOOL)animateCheckmark;
@end

@implementation CheckmarkView

#pragma mark - Lifecycle
/**********************/
- (id)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        self.translatesAutoresizingMaskIntoConstraints = FALSE;
        self.layer.cornerRadius = 5.0;
        [self setClipsToBounds:TRUE];
    }
    return self;
}
- (void)layoutSubviews{
   [self shouldDrawCheckmarkToLayerWithAnimation:self.userSelectedCheckmark];
}

#pragma mark - Public Methods
/***************************/
- (void)shouldSetCheckmarkToCheckedState:(BOOL)isChecked{
    self.userSelectedCheckmark = isChecked;
    [self setNeedsLayout];
}

#pragma mark - Private Methods
/****************************/
- (void)shouldDrawCheckmarkToLayerWithAnimation:(BOOL)animateCheckmark{

    if(self.userSelectedCheckmark){

        CGRect superlayerRect = self.bounds;
        if(!self.checkmarkLayer){

            self.checkmarkLayer = [CAShapeLayer layer];
            [self.checkmarkLayer setStrokeColor:[UIColor whiteColor].CGColor];

            UIBezierPath *checkMarkPath = [UIBezierPath bezierPath];
            //Start Point
            [checkMarkPath moveToPoint:CGPointMake(CGRectGetMinX(superlayerRect) + 5, CGRectGetMinY(superlayerRect) + 14)];

            //Bottom Point
            [checkMarkPath addLineToPoint:CGPointMake(CGRectGetMidX(superlayerRect), CGRectGetMaxY(superlayerRect) - 4)];

            //Top Right of self.checkmarkLayer
            [checkMarkPath addLineToPoint:CGPointMake(CGRectGetMaxX(superlayerRect) - 5, CGRectGetMinY(superlayerRect) + 8)];
            [checkMarkPath addLineToPoint:CGPointMake(checkMarkPath.currentPoint.x - 3, checkMarkPath.currentPoint.y - 4)];

            //Top Middle Point
            [checkMarkPath addLineToPoint:CGPointMake(CGRectGetMidX(superlayerRect) - 1, CGRectGetMidY(superlayerRect) + 2)];

            //Top left of self.checkmarkLayer
            [checkMarkPath addLineToPoint:CGPointMake(CGRectGetMinX(superlayerRect) + 7, CGRectGetMinY(superlayerRect) + 10)];
            [checkMarkPath closePath];
            [self.checkmarkLayer setPath:checkMarkPath.CGPath];
        }

        self.layer.backgroundColor = [UIColor colorWithHexString:UIColorOrangeB0].CGColor;
        [self.checkmarkLayer setFillColor:[UIColor colorWithHexString:UIColorOrangeB0].CGColor];
        [self.layer addSublayer:self.checkmarkLayer];

        if(animateCheckmark){

            animationCompletionBlock block;
            block = ^(void){
                [self.checkmarkLayer setFillColor:[UIColor whiteColor].CGColor];
            };

            CABasicAnimation *strokeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
            [strokeAnimation setBeginTime:0.0];
            [strokeAnimation setFromValue:@(0.0f)];
            [strokeAnimation setToValue:@(1.0f)];
            [strokeAnimation setDuration:.8];

            CABasicAnimation *fillAnimation = [CABasicAnimation animationWithKeyPath:@"fillColor"];
            [fillAnimation setBeginTime:strokeAnimation.duration + .2];
            [fillAnimation setDuration:.2];
            [fillAnimation setFromValue:(id)[UIColor colorWithHexString:UIColorOrangeB0].CGColor];
            [fillAnimation setToValue:(id)[UIColor whiteColor].CGColor];

            CAAnimationGroup *group = [CAAnimationGroup animation];
            group.delegate = self;
            [group setDuration:1.5];
            [group setAnimations:@[strokeAnimation,fillAnimation]];
            [group setValue:block forKey:kAnimationCompletionBlock];
            [self.checkmarkLayer addAnimation:group forKey:nil];
        }
    }
    else{
        self.layer.backgroundColor = [UIColor colorWithHexString:UIColorWhiteOffset].CGColor;
        [self.checkmarkLayer setFillColor:[UIColor colorWithHexString:UIColorOrangeB0].CGColor];
        [self.checkmarkLayer removeFromSuperlayer];
    }

}


#pragma mark - CAAnimationBlock
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
    animationCompletionBlock theBlock = [theAnimation valueForKey: kAnimationCompletionBlock];
    if (theBlock)
        theBlock();
}
1
I removed [self setNeedsLayout] call and the layoutSubviews method and everything worked fine. Maybe you could upload a picture of whats going wrong.Sam
Did you load the view into a tableview cell?Esko918
Yes, but really you should be able to call [self setNeedsLayout] as much as you want. I have updated my answerSam

1 Answers

9
votes

To answer your first question

layoutSubviews will only be called once per run-loop. You can call [self setNeedsLayout] as much as you want without worrying about taking a performance hit laying out your views needlessly.

Referenced From: Kubicek, Jim. "Abusing UIView." OCT 11TH, 2012. http://jimkubicek.com/blog/2012/10/11/using-uiview/

To answer your second question, you are right about why it is flickering. The problem is that there is no guarantee that the animationDidStop callback method will be called before the layer's appearance is reset.

There are a couple of ways to fix this, the following way just involves adding some extra code without changing your existing code.

//kCAFillModeForwards: the animatable properties take on the end value once it has finished
[strokeAnimation setFillMode:kCAFillModeForwards];
[strokeAnimation setRemovedOnCompletion:NO];
[fillAnimation setFillMode:kCAFillModeForwards];
[fillAnimation setRemovedOnCompletion:NO];
[group setFillMode:kCAFillModeForwards];
[group setRemovedOnCompletion:NO];

[self.checkmarkLayer addAnimation:group forKey:nil];

When a CAAnimation completes, it removes itself from the layer and causes the layer to reset, so the first thing we will do is stop the animation from being removed.

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
    animationCompletionBlock theBlock = [theAnimation valueForKey: kAnimationCompletionBlock];
    if (theBlock)
        theBlock();

[self.checkmarkLayer removeAllAnimations];

}

When the animationDidStop method is called, we set the layer's properties like before, then we remove the animations from the layer.

One more thing to think about is that when you change a CALayer's appearance, it gets implicitly (automatically) animated. So when you setup the completion block, you want to explicitly tell core animation not to animate

animationCompletionBlock block;
        block = ^(void){
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            [self.checkmarkLayer setFillColor:[UIColor whiteColor].CGColor];
            [CATransaction commit];
        };