iOS: Gesture recogniser for smooth scrolling and flicking a View - ios

I am building an iPad app where I needed to allow resizing views functionality using divider view provided between two views. The divider view is just a 20px height view between two half screen content views - please refer attached images.
When user scrolls this divider view up or down, both content views changes their sizes appropriately. I have extended UIView and implemented this using touchMoved delegate as code given below in touchesMoved delegate. It works fine. The only thing is missing with TouchMoved is you can't flick divider view to top or bottom directly. You have to drag all the way to top or bottom!
To support flicking the view I have tried UIPanGestureRecognizer but I don't see smooth dragging with it. Setting split-position of parent resizes both content views as shown in the code. When I handle split position change in UIGestureRecognizerStateChanged state, just touching divider view flick it to top or bottom. Handling split position change in UIGestureRecognizerStateEnded does the same but I don't see content view resizing with dividerview scrolling!
Could someone please tell me how could I achieve both smooth scrolling of divider view with resizing content views (like touchMoved) and flicking the view? Any alternative approach would also be fine. Thanks.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
if (touch) {
CGPoint lastPt = [touch previousLocationInView:self];
CGPoint pt = [touch locationInView:self];
float offset = pt.y - lastPt.y;
self.parentViewController.splitPosition = self.parentViewController.splitPosition + offset;
}
}
- (void)handlePan:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan) {
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
// If I change split position here, I don't see smooth scrolling dividerview...it directly jumps to the top or bottom!
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// If I change split position here, the same thing happens at end and I don't see my divider view moving with my scrolling and resizing my views.
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
}
}
Initial screen
Increased top view size by scrolling divider view towards bottom.
Top view is totally hidden here but I have to scroll divider view all
the way to top. I want to flick the divider view so that it directly
goes from any position to top

If I understand you correctly, you have two separate problems here.
1st: The 'unsmooth' dragging when using the GestureRecognizer. The problem in your code is, that you add the translation every time the GR calls it's handle method. Since the translation is measured continuously, you're adding a higher value every time it's called (i.e. the first call adds 10 to the split position, the 2nd 20, the 3rd 30 and so on).
To solve this you should set the translation to zero after you've updated the split position:
- (void)handlePan:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateChanged) {
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
}
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
2nd: To allow the user to flick the split position to the top or bottom, you could check the velocity in UIGestureRecognizerStateEnded, and if it exeeds some value instantly set the splitposition to top or bottom.
But it might be more convenient to use a UISwipeGestureRecognizer.
- (void)handleSwipe:(UISwipeGestureRecognizer *)gr
{
if (gr.direction == UISwipeGestureRecognizerDirectionUp){
[self.parentViewController moveSplitViewToTop];
}
else if (gr.direction == UISwipeGestureRecognizerDirectionDown){
[self.parentViewController moveSplitViewToBottom];
}
}
In this case it might be necessary (not sure, maybe it's not) to require the SwipeGR to fail before the PanGR triggers by setting:
[panGestureRecognizer requireGestureRecognizerToFail:swipeGestureRecognizer];
Hope that helped.
Edit:
Regarding your comment, I assume by frequency you mean velocity. If you only use the PanGR you could modify the code above to sth. like this:
- (void)handlePan:(UIPanGestureRecognizer*)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateChanged) {
self.parentViewController.splitPosition = self.parentViewController.splitPosition + translation.y;
}
else if (recognizer.state == UIGestureRecognizerStateEnded){
if (velocity.y > someValue){
[self.parentViewController moveSplitViewToBottom];
}
else if (velocity.y < someValue){
[self.parentViewController moveSplitViewToTop];
}
}
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
I'm not sure if the velocity is signed, if not you've got to check for the translation again in the if-block.

Related

Velocity issues in setTranslation:inView:

I's using this method:
- (void)setTranslation:(CGPoint)translation inView:(UIView *)view
From UIPanGestureRecognizer class, and I don't understand the velocity discussions around that.
Appel's documentation says:
Sets the translation value in the coordinate system of the specified
view. Changing the translation value resets the velocity of the pan.
What's that exactly mean? What is "reset velocity of the pan"?
Any help will be appreciated.. Thanks in advance!
--EDIT
See this code:
if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.cardsScrollView];
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view];
...
}
What it means is that if you're in the middle of the panning gesture (you're dragging something) and you call setTranslation:inView: on that gesture, it's velocity is going to be reset to 0;
A pan gesture not only gives you the translation but also the velocity of the gesture (how fast you're dragging) in units per second (points per second in this case). You can access the velocity by calling velocityInView:.
If you don't use the velocity then don't worry about it, otherwise keep the above in mind.
UPDATE:
I'm guessing you're trying to move the view as you drag it. I would do it slightly differently.
if (recognizer.state == UIGestureRecognizerStateBegan) {
CGAffineTransform transform = recognizer.view.transform;
[recognizer setTranslation:CGPointMake(transform.tx, transform.ty) inView:self];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.cardsScrollView];
recognizer.view.transform = CGAffineTransformMakeTranslation(translation.x, translation.y);
}
The thing to keep in mind is that self.cardsScrollView should be the superview of the view you're trying to move.

Is it possible to know when [UIDynamicItemBehavior addLinearVelocity:forItem:] has finished running?

I'm using a UIPanGestureRecognizer and UIAttachmentBehavior to move a UIView around the screen. When the user ends the gesture I apply the velocity of the gesture recognizer to the view using a UIDynamicItemBehavior and the addLinearVelocity:forItem: method.
Here is the code I use:
- (void)_handlePanGestureRecognized: (UIPanGestureRecognizer *)panGestureRecognizer
{
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan)
{
_attachmentBehavior.anchorPoint = panGestureRecognizer.view.center;
[_dynamicAnimator addBehavior: _attachmentBehavior];
}
else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged)
{
CGPoint point = [panGestureRecognizer locationInView: panGestureRecognizer.view.superview];
_attachmentBehavior.anchorPoint = point;
}
else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded)
{
[_dynamicAnimator removeBehavior: _attachmentBehavior];
CGPoint velocity = [panGestureRecognizer velocityInView: panGestureRecognizer.view.superview];
[_dynamicItemBehavior addLinearVelocity: velocity
forItem: self];
}
}
When the view stops moving I would then like to have it snap to the closest edge of the screen but I currently have no way of knowing when it has stopped moving short of polling the view's center with a CADisplayLink.
Have you tried attaching a UIDynamicAnimatorDelegate to your animator, and using the dynamicAnimatorDidPause: method to trigger snapping to the closest edge?
From reading on the developer forums, it sounds like some have had problems with their views staying in motion for a very long time (jiggling back and forth by 1 pixel, for example), but perhaps this will work for your case.

Objective-C Pan gesture flicker at bounds of superview

I have a pan gesture recogniser acting as a slider, code as follows:
- (void)minutePan:(UIPanGestureRecognizer *)gesture
{
if ((gesture.state == UIGestureRecognizerStateChanged) ||
(gesture.state == UIGestureRecognizerStateEnded)){
CGPoint translation = [gesture translationInView:gesture.view.superview];
float leftBound = gesture.view.bounds.size.width / 2.0f;
float rightBound = gesture.view.bounds.size.width + (leftBound - 60);
float position;
if(gesture.view.center.x < leftBound){
position = leftBound;
}else if(gesture.view.center.x > rightBound){
position = rightBound;
}else{
position = gesture.view.center.x + translation.x;
}
gesture.view.center = CGPointMake(position, gesture.view.center.y);
[gesture setTranslation:CGPointZero inView:gesture.view.superview];
}
}
I have a UIView with the gesture recogniser attached inside another UIView acting as the boundary for it. Its working fine except when I get to the left and right edges. The position variable should not get set lower than half the panned views width but it seems to be updating the view on the screen before the code runs. This causes the view to go outside the bounds and flicker back as long as the user is dragging. Any help would be greatly appreciated thanks.

Simple Drag & Drop for a View?

I'm learning iOS and I can't find how to add drag and drop behavior to a UIView.
I tried:
[_view addTarget:self action:#selector(moved:withEvent:) forControlEvents:UIControlEventTouchDragInside];
It says "no visible interface for UIView declares selector addTarget (etc)"
Also I tried adding a pan gesture recognizer but not sure if that's what I need
- (IBAction)test:(id)sender {
NSLog(#"dfsdfsf");
}
It's called, but don't know how to get the coordinates of the event. What't the standard, simple way in iOS to register a callback for move events / do drag and drop?
Thanks in advance.
A UIPanGestureRecognizer is definitely the way to go. If you want the user to drag the view around, you'll need the “translation” (movement) of the gesture in the superview's coordinate system:
- (IBAction)panWasRecognized:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:_view.superview];
Once you have the translation, you can move (“drag”) the view by changing its center:
CGPoint center = _view.center;
center.x += translation.x;
center.y += translation.y;
_view.center = center;
Finally, you want to set the pan gesture recognizer's translation back to zero, so the next time you get the message, it only tells you how much the gesture has moved since the last message:
[recognizer setTranslation:CGPointZero inView:_view.superview];
}
Here it is all together for easy copy/paste:
- (IBAction)panWasRecognized:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:_view.superview];
CGPoint center = _view.center;
center.x += translation.x;
center.y += translation.y;
_view.center = center;
[recognizer setTranslation:CGPointZero inView:_view.superview];
}
Start with touchesBegan, touchesMoved, touchesEnded. Override these in your UIView subclass and you'll be on your way to learning the event system. You can get the event coordinates like so:
- (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event
{
float x = [[touches anyObject] locationInView:self].x;
float y = [[touches anyObject] locationInView:self].y;
}
Then there's a ton of stuff for converting coordinates between different views and so on. Once you've understood that, you can work with the UIGestureRecognizer stuff you've already found which is what you need.
You are going to need a pan gesture recognizer to do drag/drop. You can use the locationInView: selector in UIPanGestureRecognizer to find out where you are at any given moment.
You add your gesture recognizer like so, not with the target-action stuff you were trying:
UIPanGestureRecognizer *dragDropRecog = [[UIPanGestureRecognizer alloc] initWithTarget:yourView action:#selector(thingDragged:)];
[yourView addGestureRecognizer:dragDropRecog];
Then you have to implement the selector thingDragged: in your view:
- (void) thingDragged:(UIPanGestureRecognizer *) gesture
{
CGPoint location = [gesture locationInView:self];
if ([gesture state] == UIGestureRecognizerStateBegan) {
// Drag started
} else if ([gesture state] == UIGestureRecognizerStateChanged) {
// Drag moved
} else if ([gesture state] == UIGestureRecognizerStateEnded) {
// Drag completed
}
}
You'll be translating the view being dragged in the changed bit, and handling the drop in the ended section.

iOS Animate sliding an imageview from off-screen to on-screen with gesture

I'm looking to animate bubbles with text on them to slide on and off the screen. The ideal implementation for this animation is iOS's horizonatal scroll with paging enabled. I definitely want the "bounce" when I reach the end of the speech bubbles and I definetely want the bubbles to track the finger until a certain point before they will slide off the screen. I believe this is not the same as a swipe (which is just a flick in one direction).
However, the problem with the horizontal scroll is that it is optimized for a static number of images. I will be having a dynamic number of images and as far as I can tell, you cannot dynamically append images to horizontal scroller. The idea is the app dynamically adds content to the scroller as you continue to progress through it.
The scroller was easy enough to get going but I'm going to have to tear it down now. How can I get started with the gesture (I'm not sure if the standard gesture recognizers will work for me at this point) as well as the animation? I've never worked with that portion of iOS code before.
I'm not sure if I follow your question entirely, but if you want to animate the movement of something based upon a gesture, you can use a UIPanGestureRecognizer and change the center of whatever subview you want. For example, in viewDidLoad you would:
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(movePiece:)];
[whateverViewYouWantToAnimate addGestureRecognizer:panGesture];
You can then have your gesture recognizer move it where ever you want:
- (void)movePiece:(UIPanGestureRecognizer *)gestureRecognizer
{
static CGPoint originalCenter;
if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
originalCenter = [gestureRecognizer view].center;
}
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [gestureRecognizer translationInView:self.view];
gestureRecognizer.view.center = CGPointMake(originalCenter.x + translation.x, originalCenter.y);
// if you wanted to animate both left/right and up/down, it would be:
// gestureRecognizer.view.center = CGPointMake(originalCenter.x + translation.x, originalCenter.y + translation.y);
}
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded)
{
// replace this offscreen CGPoint with something that makes sense for your app
CGPoint offscreen = CGPointMake(480, gestureRecognizer.view.center.y);
[UIView animateWithDuration:0.5
animations:^{
gestureRecognizer.view.center = offscreen;
}
completion:^(BOOL finished){
// when you're done, you might want to do whatever cleanup
// is appropriate for your app (e.g. do you want to remove it?)
[gestureRecognizer.view removeFromSuperview];
}];
}
}

Resources