Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIView animation based on UIPanGestureRecognizer velocity

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.

like image 988
Neigaard Avatar asked Nov 14 '11 12:11

Neigaard


People also ask

Does UIView animate need weak self?

You don't need to use [weak self] in static function UIView. animate() You need to use weak when retain cycle is possible and animations block is not retained by self.

Does UIView animate run on the main thread?

The contents of your block are performed on the main thread regardless of where you call [UIView animateWithDuration:animations:] . It's best to let the OS run your animations; the animation thread does not block the main thread -- only the animation block itself.

Is UIView animate asynchronous?

UIView. animate runs on the main thread and is asynchronous.


3 Answers

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];     } } 
like image 23
Rob Avatar answered Oct 06 '22 15:10

Rob


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.

like image 182
Dennis Avatar answered Oct 06 '22 15:10

Dennis


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

like image 39
nikans Avatar answered Oct 06 '22 16:10

nikans