UIView animation based on UIPanGestureRecognizer velocity

36,493

Solution 1

The docs say

The velocity of the pan gesture, which is expressed in points per second. The velocity is broken into horizontal and vertical components.

So I'd say, given you want to move your view xPoints (measured in pt) to let it go off-screen, you could calculate the duration for that movement like so:

CGFloat xPoints = 320.0;
CGFloat velocityX = [panRecognizer velocityInView:aView].x;
NSTimeInterval duration = xPoints / velocityX;
CGPoint offScreenCenter = moveView.center;
offScreenCenter.x += xPoints;
[UIView animateWithDuration:duration animations:^{
    moveView.center = offScreenCenter;
}];

You might want to use + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion instead though and try out different UIViewAnimationOptions.

Solution 2

An observation on relying upon the velocity in UIPanGestureRecognizer: I don't know about your experience, but I found the system generated velocity on the simulator to be not terribly useful. (It's ok on the device, but problematic on simulator.)

If you pan quickly across and abruptly stop, wait, and only then end the gesture (e.g. user starts a swipe, realizes that this wasn't what they wanted, so they stop and then release their finger), the velocity reported by velocityInView: at the UIGestureRecognizerStateEnded state when your finger was released seems to be the fast speed before I stopped and waited, whereas the right velocity in this example would be zero (or near zero). In short, the reported velocity is that which it was right before the end of the pan, but not the speed at the end of the pan itself.

I ended up calculating the velocity myself, manually. (It seems silly that this is necessary, but I didn't see any way around it if I really wanted to get the final speed of the pan.) Bottom line, when the state is UIGestureRecognizerStateChanged I keep track of the current and previous translationInView CGPoint as well as the time, and then using those values when I was in the UIGestureRecognizerStateEnded to calculate the actual final velocity. It works pretty well.

Here is my code for calculating the velocity. I happen to not be using velocity to figure out the speed of the animation, but rather I'm using it to determine whether the user either panned far enough or flicked quickly enough for the view to move more than half way across the screen and thus triggering the animation between views, but the concept of calculating the final velocity seems applicable to this question. Here's the code:

- (void)handlePanGesture:(UIPanGestureRecognizer *)gesture
{
    static CGPoint lastTranslate;   // the last value
    static CGPoint prevTranslate;   // the value before that one
    static NSTimeInterval lastTime;
    static NSTimeInterval prevTime;

    CGPoint translate = [gesture translationInView:self.view];

    if (gesture.state == UIGestureRecognizerStateBegan)
    {
        lastTime = [NSDate timeIntervalSinceReferenceDate];
        lastTranslate = translate;
        prevTime = lastTime;
        prevTranslate = lastTranslate;
    }
    else if (gesture.state == UIGestureRecognizerStateChanged)
    {
        prevTime = lastTime;
        prevTranslate = lastTranslate;
        lastTime = [NSDate timeIntervalSinceReferenceDate];
        lastTranslate = translate;

        [self moveSubviewsBy:translate];
    }
    else if (gesture.state == UIGestureRecognizerStateEnded)
    {
        CGPoint swipeVelocity = CGPointZero;

        NSTimeInterval seconds = [NSDate timeIntervalSinceReferenceDate] - prevTime;
        if (seconds)
        {
            swipeVelocity = CGPointMake((translate.x - prevTranslate.x) / seconds, (translate.y - prevTranslate.y) / seconds);
        }

        float inertiaSeconds = 1.0;  // let's calculate where that flick would take us this far in the future
        CGPoint final = CGPointMake(translate.x + swipeVelocity.x * inertiaSeconds, translate.y + swipeVelocity.y * inertiaSeconds);

        [self animateSubviewsUsing:final];
    }
}

Solution 3

In most cases setting UIViewAnimationOptionBeginFromCurrentState option for UIView animator is enough to make a flawlessly continued animation.

Share:
36,493
Neigaard
Author by

Neigaard

Updated on September 01, 2020

Comments

  • Neigaard
    Neigaard over 3 years

    I would like to be able to move a subview on and off the screen much like you browse between images in the iPhone's build in Photos app, so if the subview is more than 1/2 off screen when I let go with my finger it must animate off screen, but it must also support swipe so if the swipe/pan velocity is high enough it must animate off screen even though is might be less than 1/2 off screen.

    My idea was to use UIPanGestureRecognizer and then test on the velocity. This works, but how do I set a correct animation duration for the moving of the UIView based on the view's current location and velocity of the pan so that it seems smooth? If I set a fixed value, the animation either starts to slow or to fast compared to my fingers swipe speed.

  • an0
    an0 almost 11 years
    It does not work in iOS 6 because the final translation and the lastTranslation is the same in UIGestureRecognizerStateEnded so the final velocity is always 0.
  • an0
    an0 almost 11 years
    Thanks, I see. It is an issue of simulator. I fired a bug report to Apple.
  • Rob
    Rob almost 11 years
    I've modified the source (changing a few variable names) to hopefully avoid this confusion. This code worked fine as it was, but hopefully the new variable names make it even a little less confusing. Thanks for reporting the bug to Apple.
  • Simon
    Simon about 9 years
    "translate" is always (0,0) on UIGestureRecognizerStateEnded state. to calculate swipeVelocity your probably want to subtract (lastTranslate - prevTranslate) instead
  • DCMaxxx
    DCMaxxx over 8 years
    Just wan to point out that using static variables will make the values persistant within the app lifecycle, even in another object. Think about it before using them.
  • Edward
    Edward over 7 years
    Awesome answer. Would be great to have this in swift :)