Search code examples
iosobjective-cxcodecore-animation

Core Animation speed


I'd like to create animation similar to swiping in menus of iPhone. That means if you swipe enough, animation takes you to new page, but if you swipe just a little animation takes you back on starting page after finishing swipe gesture.

So far, I have swipe recognizer which listens to position of finger on screen and follows the movement of finger. Animation looks at the end of gesture whether finger moved more than 1/3 of screen width and decides whether to proceed to its end or go back to beginning.

- (void)panGestureHandler:(UIPanGestureRecognizer*)recognizer
{
    if (recognizer.state == UIGestureRecognizerStateEnded)
    {
        //if our gesture was enough to let animation roll to its end
        if (fabsf(translation.x) > CGRectGetWidth(self.view.frame) * 0.33)
        {
            [self finishLayer:categoryControllerMain.view.layer animation:menuOpenSwipeAnimateMoveLeft withForward:NO];
        }
        //finger movement was below .33% of screen width so animate to initial state
        else
        {
            [self finishLayer:categoryControllerMain.view.layer animation:menuOpenSwipeAnimateMoveLeft withForward:NO];

        }
    }
    else if (recognizer.state == UIGestureRecognizerStateChanged)
    {
        //We put layer speed to zero so we can control animation with timeoffset in relation to finger pan movement
        categoryControllerMain.view.layer.speed = 0.0;
        [categoryControllerMain.view.layer addAnimation:menuOpenSwipeAnimateMoveLeft forKey:@"menuOpenMoveLeft"];
        categoryControllerMain.view.layer.timeOffset = fabs(translation.x / CGRectGetWidth(self.view.frame));
    }
}

And this is the method which says to animation whether to move towards its end or beginning, according to finger amount movement. If I just swiped a bit, animation continues with speed = -1;, i.e. it starts going backwards, but when it gets to its end, or in this case beginning, all layers just disappear. Here is not the case of presentationLayer, because when animation ends, all the layers should be at that exact point, because that is their initial point. Instead, all the layers are gone, and when I try to swipe again, they appear on their correct locations.

- (void)finishLayer:(CALayer*)layer animation:(CAKeyframeAnimation*)animation withForward:(BOOL)shouldGoForward
{

    pan.enabled = NO;
    if (shouldGoForward)
    {
        //animation continues
        CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
        layer.timeOffset = 0.0;
        layer.beginTime = 0.0;
        CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
        layer.beginTime = timeSincePause;
        layer.speed = 1.0;
    }
    else
    {
        //we want to take it back. This is where strange things happen. Perhaps timeoffset is not right
        layer.timeOffset = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
        layer.beginTime = CACurrentMediaTime();
        layer.speed = -1;
    }
}

A final consideration: If my animations were linear, then I could create another animation for every animation from beginning, calculate where to start and make it appear as one animation. But my animations have CAMediaTimingFunctions which don't allow me to calculate exact position in opposite animation.

EDIT

The question is the following: how to perform core animation with speed = -1 so that when animations rolls backwards, all of layers/views stay visible on screen?

Thanks in advance


Solution

  • OK, if anyone ever needs to solve similar issue, here is the deal.

    You shouldn't use same animation object. The correct answer is to set speed = -1.0, but not on that animation you'd been using previously. Instead, create new animation with all the same values except of speed which should be set to -1. In that case, you a have a brand new animation which is not going to have problems caused by change of speed.

    The only you have to be careful about is to set repeatDuration though. You set timeOffset property but if you leave duration to the same value as original animation, it will get to its end and start over. So, the way to cut its length is to set the value of repeatDuration to exact value as timeOffset. So now it stops at last frame of original animation and everything works as expected.

    As mentioned before, using same animation wasn't an option because when animation reached its end (or we might say beginning), the layer would disappear.

    Righty, here's the code which takes you back to initial state following the same properties of initial animation.

    CGFloat elapsedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 1;
    
    CAKeyframeAnimation* animationBackwards = [animation copy];
    animationBackwards.speed = -1.0;
    animationBackwards.timeOffset = elapsedTime;
    animationBackwards.repeatDuration = elapsedTime;
    [layer removeAnimationForKey:animationName];
    [layer addAnimation:animationBackwards forKey:animationName];