IOS - Creating a cool Tinder like drag animations
So what are we going to create?
Creating the draggable view
I’m going to skip all the boring project-structure-creation steps, just make sure you have a clean UIViewController you can work with.
Let’s create a new custom view, it’ll be our draggable view, I named it GGDraggableView. We’ll need to add it to our view and set its size. (GGView is the view of our main ViewController).
I spiced the view a bit and added a UIImageView to it with a pretty image of Bar Refaeli (find your own one)
| #import "GGView.h" | |
| #import "GGDraggableView.h" | |
| @interface GGView () | |
| @property (nonatomic, strong) GGDraggableView *draggableView; | |
| @end | |
| @implementation GGView | |
| - (id)init | |
| { | |
| self = [super init]; | |
| if (!self) return nil; | |
| self.backgroundColor = [UIColor whiteColor]; | |
| [self loadDraggableCustomView]; | |
| return self; | |
| } | |
| - (void)loadDraggableCustomView | |
| { | |
| self.draggableView = [[GGDraggableView alloc] init]; | |
| [self addSubview:self.draggableView]; | |
| } | |
| - (void)layoutSubviews { | |
| [super layoutSubviews]; | |
| self.draggableView.frame = CGRectMake(60, 60, 200, 260); | |
| } | |
| @end |
Next step is to add a UIPanGestureRecognizer to our view, this is the gesture recognizer we’ll use to pump our awesome drag animation.
| #import "GGDraggableView.h" | |
| @interface GGDraggableView () | |
| @property (nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer; | |
| @end | |
| @implementation GGDraggableView | |
| - (id)init | |
| { | |
| self = [super init]; | |
| if (!self) return nil; | |
| self.backgroundColor = [UIColor greenColor]; | |
| self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragged:)]; | |
| [self addGestureRecognizer:self.panGestureRecognizer]; | |
| [self loadImageAndStyle]; | |
| return self; | |
| } | |
| - (void)loadImageAndStyle | |
| { | |
| UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"bar"]]; | |
| [self addSubview:imageView]; | |
| self.layer.cornerRadius = 8; | |
| self.layer.shadowOffset = CGSizeMake(7, 7); | |
| self.layer.shadowRadius = 5; | |
| self.layer.shadowOpacity = 0.5; | |
| } | |
| - (void)dragged:(UIGestureRecognizer *)gestureRecognizer | |
| { | |
| // TODO: Write Logic | |
| } | |
| - (void)dealloc | |
| { | |
| [self removeGestureRecognizer:self.panGestureRecognizer]; | |
| } | |
| @end |
This is how the view should look like
Gesture recognizer logic
When we break down how Tinder handles the pan gesture we notice they change two different parameters of the view during the dragging -
- Rotation
- Scale
To get the desired effect we’re going to change these two parameters and link them to the drag “force”, which is how long the finger was dragged over the X axis of the screen. Here’s the flow I came up with to get pretty close results -
- Save the original position of the view when the pan gesture starts (so we can change it or ‘snap’ back to it)
- Find the gesture strength
CGFloat rotationStrength = MIN(xDistance / 320, 1); - Calculate the pan gesture ‘strength’ and calculate the rotation + scale changes
- The rotation angle is in radians, 2π radians is 360° and we’d want a more subtle rotation, I went for 22.5° (1/16 of a full rotation)
CGFloat rotationAngle = (CGFloat) (2*M_PI * rotationStrength / 16);- The scale will start at 1.0x and go down to 0.93x, so we need to “invert” the force when calculating it -
CGFloat scaleStrength = 1 - fabsf(rotationStrength) / 2;CGFloat scale = MAX(scaleStrength, 0.93);
- Every time the gesture change update the following properties of the view
- Scale & rotation transformation
- Position
- When the pan gesture ends animate the view back to the original position and remove the transformations
- (This is the place we can also issue some kind of action when the user finish the gesture, we’ll leave that out for now)
| - (void)dragged:(UIPanGestureRecognizer *)gestureRecognizer | |
| { | |
| CGFloat xDistance = [gestureRecognizer translationInView:self].x; | |
| CGFloat yDistance = [gestureRecognizer translationInView:self].y; | |
| switch (gestureRecognizer.state) { | |
| case UIGestureRecognizerStateBegan:{ | |
| self.originalPoint = self.center; | |
| break; | |
| }; | |
| case UIGestureRecognizerStateChanged:{ | |
| CGFloat rotationStrength = MIN(xDistance / 320, 1); | |
| CGFloat rotationAngel = (CGFloat) (2*M_PI * rotationStrength / 16); | |
| CGFloat scaleStrength = 1 - fabsf(rotationStrength) / 4; | |
| CGFloat scale = MAX(scaleStrength, 0.93); | |
| self.center = CGPointMake(self.originalPoint.x + xDistance, self.originalPoint.y + yDistance); | |
| CGAffineTransform transform = CGAffineTransformMakeRotation(rotationAngel); | |
| CGAffineTransform scaleTransform = CGAffineTransformScale(transform, scale, scale); | |
| self.transform = scaleTransform; | |
| break; | |
| }; | |
| case UIGestureRecognizerStateEnded: { | |
| [self resetViewPositionAndTransformations]; | |
| break; | |
| }; | |
| case UIGestureRecognizerStatePossible:break; | |
| case UIGestureRecognizerStateCancelled:break; | |
| case UIGestureRecognizerStateFailed:break; | |
| } | |
| } | |
| - (void)resetViewPositionAndTransformations | |
| { | |
| [UIView animateWithDuration:0.2 | |
| animations:^{ | |
| self.center = self.originalPoint; | |
| self.transform = CGAffineTransformMakeRotation(0); | |
| }]; | |
| } |
This is what we have up until now
Adding an overlay and performing action on release
After getting the cool scale & rotation effect we’ll probably want to add some kind of ‘action overlay’ when the image is dragged left / right. We have several options on where to implement this behaviour, I’ll throw that in the same view but be advices that it’s probably better to extract this code to a different class.
Our pretty overlay view (I used two images for the different overlay modes)
| #import <Foundation/Foundation.h> | |
| typedef NS_ENUM(NSUInteger , GGOverlayViewMode) { | |
| GGOverlayViewModeLeft, | |
| GGOverlayViewModeRight | |
| }; | |
| @interface GGOverlayView : UIView | |
| @property (nonatomic) GGOverlayViewMode mode; | |
| @end |
| #import "GGOverlayView.h" | |
| @interface GGOverlayView () | |
| @property (nonatomic, strong) UIImageView *imageView; | |
| @end | |
| @implementation GGOverlayView | |
| - (id)initWithFrame:(CGRect)frame { | |
| self = [super initWithFrame:frame]; | |
| if (!self) return nil; | |
| self.backgroundColor = [UIColor whiteColor]; | |
| self.imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"trollface_300x200"]]; | |
| [self addSubview:self.imageView]; | |
| return self; | |
| } | |
| - (void)setMode:(GGOverlayViewMode)mode | |
| { | |
| if (_mode == mode) return; | |
| _mode = mode; | |
| if (mode == GGOverlayViewModeLeft) { | |
| self.imageView.image = [UIImage imageNamed:@"trollface_300x200"]; | |
| } else { | |
| self.imageView.image = [UIImage imageNamed:@"thumbs_up_300x300"]; | |
| } | |
| } | |
| - (void)layoutSubviews | |
| { | |
| [super layoutSubviews]; | |
| self.imageView.frame = CGRectMake(50, 50, 100, 100); | |
| } | |
| @end |
Now let’s create and hide the overlay
| - (id)initWithFrame:(CGRect)frame | |
| { | |
| self = [super initWithFrame:frame]; | |
| if (!self) return nil; | |
| self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragged:)]; | |
| [self addGestureRecognizer:self.panGestureRecognizer]; | |
| [self loadImageAndStyle]; | |
| self.overlayView = [[GGOverlayView alloc] initWithFrame:self.bounds]; | |
| self.overlayView.alpha = 0; | |
| [self addSubview:self.overlayView]; | |
| return self; | |
| } |
And connect it to the gesture recognizer
| - (void)dragged:(UIPanGestureRecognizer *)gestureRecognizer | |
| { | |
| CGFloat xDistance = [gestureRecognizer translationInView:self].x; | |
| CGFloat yDistance = [gestureRecognizer translationInView:self].y; | |
| switch (gestureRecognizer.state) { | |
| case UIGestureRecognizerStateBegan:{ | |
| self.originalPoint = self.center; | |
| break; | |
| }; | |
| case UIGestureRecognizerStateChanged:{ | |
| CGFloat rotationStrength = MIN(xDistance / 320, 1); | |
| CGFloat rotationAngel = (CGFloat) (2*M_PI/16 * rotationStrength); | |
| CGFloat scaleStrength = 1 - fabsf(rotationStrength) / 4; | |
| CGFloat scale = MAX(scaleStrength, 0.93); | |
| CGAffineTransform transform = CGAffineTransformMakeRotation(rotationAngel); | |
| CGAffineTransform scaleTransform = CGAffineTransformScale(transform, scale, scale); | |
| self.transform = scaleTransform; | |
| self.center = CGPointMake(self.originalPoint.x + xDistance, self.originalPoint.y + yDistance); | |
| [self updateOverlay:xDistance]; | |
| break; | |
| }; | |
| case UIGestureRecognizerStateEnded: { | |
| [self resetViewPositionAndTransformations]; | |
| break; | |
| }; | |
| case UIGestureRecognizerStatePossible:break; | |
| case UIGestureRecognizerStateCancelled:break; | |
| case UIGestureRecognizerStateFailed:break; | |
| } | |
| } | |
| - (void)updateOverlay:(CGFloat)distance | |
| { | |
| if (distance > 0) { | |
| self.overlayView.mode = GGOverlayViewModeRight; | |
| } else if (distance <= 0) { | |
| self.overlayView.mode = GGOverlayViewModeLeft; | |
| } | |
| CGFloat overlayStrength = MIN(fabsf(distance) / 100, 0.4); | |
| self.overlayView.alpha = overlayStrength; | |
| } | |
| - (void)resetViewPositionAndTransformations | |
| { | |
| [UIView animateWithDuration:0.2 | |
| animations:^{ | |
| self.center = self.originalPoint; | |
| self.transform = CGAffineTransformMakeRotation(0); | |
| self.overlayView.alpha = 0; | |
| }]; | |
| } |
We’re almost done
Performing the action
This is the easy part, we just need to check if the distance strength is large enough when the gestures ends.
We perform this check inside the UIGestureRecognizerStateEnded case and can do anything we want.
That’s about it. Hope you enjoyed and that you’re a bit less scared about animations and gestures now :)
Everything is available on GitHub. Have fun!
Guti