Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an issue with this use of kCAMediaTimingFunctionEaseIn?

I'm using CALayer, CAReplicatorLayer and CABasicAnimation to animate a bar chart into place. The bar chart is made of up "segments" which I render as small rectangles with a CALayer. Then I add them as a sublayers to CAReplicatorLayer in a loop. Finally, I add an animation for each small rectangle that drops the rectangle into its place in the chart.

Here is the final state of the animation:

Correct final state of the animation

And here is what it looks like in the middle of the animation:

Animating state

Here's the code that makes it happen:

#define BLOCK_HEIGHT 6.0f
#define BLOCK_WIDTH 8.0f

@interface TCViewController ()

@property (nonatomic, strong) NSMutableArray * blockLayers;         // blocks that fall from the sky
@property (nonatomic, strong) NSMutableArray * replicatorLayers;    // replicator layers that will replicate the blocks into position

@end

@implementation TCViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor lightGrayColor];

    self.blockLayers = [NSMutableArray array];
    self.replicatorLayers = [NSMutableArray array];

    NSArray * dataBlocksData = @[@1,@2,@3,@1,@5,@2,@11,@14,@5,@12,@10,@3,@7,@6,@3,@4,@1];

    __block CGFloat currentXPosition = 0.0f;

    [dataBlocksData enumerateObjectsUsingBlock:^(NSNumber * obj, NSUInteger idx, BOOL *stop) {

        currentXPosition += BLOCK_WIDTH + 2.0f;

        if (obj.integerValue == 0) return;

        // replicator layer for data blocks in a column
        CAReplicatorLayer * replicatorLayer = [[CAReplicatorLayer alloc] init];
        replicatorLayer.frame = CGRectMake(currentXPosition, 0.0f, BLOCK_WIDTH, 200.0f);

        // single data block layer (must be new for each replicator layer. can't reuse existing layer)
        CALayer * blockLayer = [[CALayer alloc] init];
        blockLayer.frame = CGRectMake(0, 200-BLOCK_HEIGHT, BLOCK_WIDTH, BLOCK_HEIGHT);
        blockLayer.backgroundColor = [UIColor whiteColor].CGColor;
        [replicatorLayer addSublayer:blockLayer];

        replicatorLayer.instanceCount = obj.integerValue;
        replicatorLayer.instanceDelay = 0.1f;
        replicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, -BLOCK_HEIGHT, 0);    // negative height moves back up the column

        [self.blockLayers addObject:blockLayer];                
        [self.replicatorLayers addObject:replicatorLayer];      
    }];
}

- (void)viewDidLayoutSubviews {
    // add replicator layers as sublayers
    [self.replicatorLayers enumerateObjectsUsingBlock:^(CAReplicatorLayer * obj, NSUInteger idx, BOOL *stop) {
        [self.view.layer addSublayer:obj];
    }];
}

- (void)viewDidAppear:(BOOL)animated {

    // add animations to each block layer
    [self.blockLayers enumerateObjectsUsingBlock:^(CALayer * obj, NSUInteger idx, BOOL *stop) {
        // position animation
        CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"];
        animation.fromValue = @(-300);
        animation.toValue = @(0);
        animation.duration = 1.0f;
        animation.fillMode = kCAFillModeBoth;
        animation.removedOnCompletion = NO;
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
        [obj addAnimation:animation forKey:@"falling-block-animation"];
    }];
}

Finally, here's the catch. If you specify no timingFunction, a Linear function or an EaseIn function the animations don't fully complete. Like the blocks don't fall completely into place. It looks like they're close to finishing but not quite. Just a frame or two off. Swap out the timing function assignment in -viewDidAppear: and you'll get this weirdness:

enter image description here

Go ahead, try it out. I even tried specifying a function with control points (link) that are exactly like an EaseIn function:

// use control points for an EaseIn timing:
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.42 :0.00 :1.0 :1.0];

// or simply specify the ease in timing function:
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

For some reason EaseOut and Default timing functions work fine but an ease in style function appears to take longer to complete, visually, perhaps because of the length of the curve of interpolation?

I have tried many things to narrow down the issue:

  1. Not looping over these structures and using only 1 layer and 1 replicator layer with the same animation. By that I mean, animate just one column. No luck. Looping isn't the issue.
  2. Put the creation, sublayer-adding and animation-adding code all in -viewDidAppear. Didn't seem to make a difference if the animations were added earlier or later in the view's life cycle.
  3. Using properties for my structures. No difference.
  4. Not using properties for my structures. Again, no difference.
  5. Added callbacks for animation completion to remove the animation so as to leave the columns in the correct "final" state. This was not good but worth a try.
  6. The issue is present in both iOS 6 and 7.

I really want to use an EaseIn timing function because its the best looking animation for something falling into place. I also want to use the CAReplicatorLayer because it does exactly what I want to do.

So what's wrong with the EaseIn timing functions as I'm using them? Is this a bug?

like image 647
Aaron Avatar asked Nov 11 '22 05:11

Aaron


1 Answers

I agree that this appears to be a bug in the combination of CAReplicatorLayer and the kCAMediaTimingFunctionEaseIn timing function.

As a workaround, I suggest using a keyframe animation with three keyframes. The first two keyframes define your desired animation with the ease-in timing. The last keyframe is defined to be identical to the second keyframe, but gives the animation additional time to sit on the final keyframe, which seems to let the replicator layer animate that last replicated layer into the final state.

My suggestion therefore is:

CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];

//  extra keyframe duplicating final state
animation.values = @[@-300.0f, @0.0f, @0.0f];

//  double length of original animation, with last half containing no actual animation
animation.keyTimes = @[@0.0, @0.5, @1.0];
animation.duration = 2.0f;

animation.fillMode = kCAFillModeBoth;
animation.removedOnCompletion = NO;

//  ease-in for first half of animation; default after
animation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn], [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];
[obj addAnimation:animation forKey:@"falling-block-animation"];
like image 78
GBegen Avatar answered Nov 15 '22 12:11

GBegen