Search code examples
core-animationtiming

CoreAnimation layers not all moving at the same speed: what am I missing?


I have an app with CALayers that move from one side of the screen to the other. I am trying to get the timing right so that they all take the same time to pass from the first side to an arbitrary delimiter I've decided on (near the other side), then continue their run to disappear from the screen.

Here's an example of what I mean:

visual description of my problem

Both boxes should move at the same speed (the animation has linear timing), so when the small box hits the delimiter, the small gap between the two should be the same.

However, I rather see this:

visual description of what happens

Expressed with words, it seems that my large box is moving faster than the small box.

Here's the relevant part of my code. I decide a speed in pixels per second, then set the duration of the animation considering the number of pixels boxes have to travel.

const CGRect viewBounds = CGRectMake(0,0, 500, 400);
const CGFloat distanceToCross = viewBounds.size.width - delimiterX;
const CFTimeInterval timeToCross = 5;
const CGFloat pixelsPerSecond = distanceToCross / timeToCross;

const CGFloat y = /* y coordinate of the box */;
CALayer* box = [CALayer layer];
// the layer should lie outside the viewport at first
box.frame = CGRectMake(viewBounds.size.width, y, /* size here */);
box.anchorPoint = CGPointMake(0, 0.5);
// snip

[CATransaction begin];
CATransaction.animationTimingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
CATransaction.animationDuration = (viewBounds.size.width + box.frame.size.width) / pixelsPerSecond;
// and the layer should cross the viewport to outside of it, too
box.position = CGPointMake(-box.frame.size.width, y);
[CATransaction commit];

This is not shown in my graphical example, but boxes of the same size all go at the same speed, so obviously I'm doing something wrong with my time calculation. What am I missing?


Solution

  • CALayer.position, by default, indicates the center point of the box. As such, your ending frame for each box is actually past the point you want, and the amount it goes over is based on the size of the box. This is going to throw off your calculation since your duration is based on the wrong distance. I'm surprised you haven't noticed your boxes drifting slightly downwards as they move, since the y coordinate is used for both frame.origin.y and position.y as well.

    A simple fix is to take the midpoint of the frame. You can do this by doing something like

    CGFrame newFrame = (CGRect){{-box.frame.size.width, y}, box.frame.size};
    box.position = CGPointMake(CGRectGetMidX(newFrame), CGRectGetMidY(newFrame));
    

    Or to make it resilient in the face of anchorPoint changes, you could use something like

    box.position = CGPointMake(-box.frame.size.width + box.frame.size.width*box.anchorPoint.x, y + box.frame.size.height*box.anchorPoint.y);