Xamarin iOS - UIView Animation with velocity - ios

I've got a UIView laying right above all others. This UIView has a UIPanGestureRecognizer to drag it around of the screen. If the User stopped panning, the View moves to a calculated Position which is the left screen bounds or the right ones. This works quite good but now I want to consider the velocity the View moved around the screen and when the user lifts his Finger off the screen, the UIView should not stop immediately to move to the endpoint, instead it should do it with a curve.
Edit 1
I think I expressed myself wrongly. I meant that the View should move the direction the user dragged it to but then it should make a curve and move to the desired position. Think of it like gravity. The user will drag the View with a speed from the left bottom corner to the middle of the screen and lifts immediately his finger. Then the View should not stop, else it should move in the last direction und slowly move to the desired location, which is in this case the right bottom corner.
This is the code I already have:
private void DidPan()
{
CGPoint translation = PanGesture.TranslationInView(this.Superview);
CGPoint velocity = PanGesture.VelocityInView(this.Superview);
if (PanGesture.State == UIGestureRecognizerState.Changed)
{
this.Center = new CGPoint(lastLocation.X + translation.X, lastLocation.Y + translation.Y);
}
else if (PanGesture.State == UIGestureRecognizerState.Ended)
{
// Calculates the new position the view will be moving to
var newLocation = new CGPoint(SetX(Superview.Bounds), SetY(Superview.Bounds));
// Animate it to the desired location
UIView.Animate(0.3, () =>
{
this.Center = newLocation;
}, null);
}
}
As you can see we already have the velocity but I don't know how to continue from here. Hopefully someone has the right starting point from here and could help me.
Thanks a lot!

I think UIKit Dynamics can meet your requirement.
refer to this Blog : https://blog.xamarin.com/drag-drop-and-snap-with-uikit-dynamics/

Related

Why am I unable to detect when my UIView I push off screen using UIKit Dynamics is no longer visible?

I'm using UIKit Dynamics to push a UIView off screen, similar to how Tweetbot performs it in their image overlay.
I use a UIPanGestureRecognizer, and when they end the gesture, if they exceed the velocity threshold it goes offscreen.
[self.animator removeBehavior:self.panAttachmentBehavior];
CGPoint velocity = [panGestureRecognizer velocityInView:self.view];
if (fabs(velocity.y) > 100) {
self.pushBehavior = [[UIPushBehavior alloc] initWithItems:#[self.scrollView] mode:UIPushBehaviorModeInstantaneous];
[self.pushBehavior setTargetOffsetFromCenter:centerOffset forItem:self.scrollView];
self.pushBehavior.active = YES;
self.pushBehavior.action = ^{
CGPoint lowestPoint = CGPointMake(CGRectGetMinX(self.imageView.bounds), CGRectGetMaxY(self.imageView.bounds));
CGPoint convertedPoint = [self.imageView convertPoint:lowestPoint toView:self.view];
if (!CGRectIntersectsRect(self.view.bounds, self.imageView.frame)) {
NSLog(#"outside");
}
};
CGFloat area = CGRectGetWidth(self.scrollView.bounds) * CGRectGetHeight(self.scrollView.bounds);
CGFloat UIKitNewtonScaling = 5000000.0;
CGFloat scaling = area / UIKitNewtonScaling;
CGVector pushDirection = CGVectorMake(velocity.x * scaling, velocity.y * scaling);
self.pushBehavior.pushDirection = pushDirection;
[self.animator addBehavior:self.pushBehavior];
}
I'm having an immense amount of trouble detecting when my view actually completely disappears from the screen.
My view is setup rather simply. It's a UIScrollView with a UIImageView within it. Both are just within a UIViewController. I move the UIScrollView with the pan gesture, but want to detect when the image view is off screen.
In the action block I can monitor the view as it moves, and I've tried two methods:
1. Each time the action block is called, find the lowest point in y for the image view. Convert that to the view controller's reference point, and I was just trying to see when the y value of the converted point was less than 0 (negative) for when I "threw" the view upward. (This means the lowest point in the view has crossed into negative y values for the view controller's reference point, which is above the visible area of the view controller.)
This worked okay, except the x value I gave to lowestPoint really messes everything up. If I choose the minimum X, that is the furthest to the left, it will only tell me when the bottom left corner of the UIView has gone off screen. Often times as the view can be rotating depending on where the user pushes from, the bottom right may go off screen after the left, making it detect it too early. If I choose the middle X, it will only tell me when the middle bottom has gone off, etc. I can't seem to figure out how to tell it "just get me the absolute lowest y value.
2. I tried CGRectIntersectsRect as shown in the code above, and it never says it's outside, even seconds after it went shooting outside of any visible area.
What am I doing wrong? How should I be detecting it no longer being visible?
If you take a look on UIDynamicItem protocol properties, you can see they are center, bounds and transform. So UIDynamicAnimator actually modifies only these three properties. I'm not really sure what happens with the frame during the Dynamics animations, but from my experience I can tell it's value inside the action block is not always reliable. Maybe it's because the frame is actually being calculated by CALayer based on center, transform and bounds, as described in this excellent blog post.
But you for sure can make use of center and bounds in the action block. The following code worked for me in a case similar to yours:
CGPoint parentCenter = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
self.pushBehavior.action = ^{
CGFloat dx = self.imageView.center.x - parentCenter.x;
CGFloat dy = self.imageView.center.y - parentCenter.y;
CGFloat distance = sqrtf(dx * dx + dy * dy);
if(distance > MIN(parentCenter.y + CGRectGetHeight(self.imageView.bounds), parentCenter.x + CGRectGetWidth(self.imageView.bounds))) {
NSLog(#"Off screen!");
}
};

Finger controlled view slide animation

I would like to know how to implement an animation where the user can slide one view over another using his finger. To simplify things I will post some pictures to explain what I mean.
At first we have a view which has a settings icon (that pink thingie in the upper left corner). The view can can be manipulated by any means necessary - including swiping. See picture 1.
When the user presses the settings button the view slides away and reveals settings view underneath. To do this I just use [UIView beginAnimations:nil context:nil] adjust the main view frame and commit animations. The situation looks like this: (settings are purple, main view is still the same colour)
Now when the user decides he want the main screen back he will have to grab it and slide it back into its place. HOW? Now I have been using UITapGestureRecognizer which would trigger a reverse animation so that the view slid back in its original place. That was messy and the user was still able to manipulate that small section of the view visible.
I want the view to follow the user's finger and slide and scale its way back in its place. The main view should not be manipulatable while moving. To understand what I mean you can check the Yahoo Weather app which has a similar thing going on - except that my view would also shrink a bit vertically. The third picture shows things "in motion":
Please provide links or code that can help me accomplish this.
Cheers, Jan.
Look at this code:
MMDrawerController
MSMatrixController
And you can try this
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[touches allObjects] objectAtIndex:0];
CGPoint touchPt = [touch locationInView:self];
float deltaX = touchPt.x - beginPt.x; //where beginPt is CGPoint variable which was set in touches began
float deltaY = touchPt.y - beginPt.y;
if (deltaX < 0) {
deltaX = -deltaX;
}
if (deltaY < 0) {
deltaY = -deltaY;
}
CGPoint toPt;
toPt.x = deltaX;
toPt.y = deltaY;
yourMovedView.center = toPt;
}

iOS: Implement a rotating wheel with custom views being horizontally stationary [duplicate]

I am looking for a little guidance to start figuring out an animation that tracks finger movement and moves a collection of UIButtons along the outer path of a circle
I am picturing it will kind of have a revolver feel to it. Like each one locks into place at the bottom
or like the swiping through one of the slide inserts of these
thanks in advance
(Sample code on GitHub)
It's not really that difficult, there's just a lot of trigonometry involved.
Now, what I'm going to describe now, is not an animation, since you requested for it to track the position of your finger in the title. An animation would involve its own timing function, but since you're using the touch gesture, we can use the inherent timing that this event has and just rotate the view accordingly. (TL;DR: The user keeps the time of the movement, not an implicit timer).
Keeping track of the finger
First of all, let's define a convenient class that keeps track of the angle, I'm gonna call it DialView. It's really just a subclass of UIView, that has the following property:
DialView.h
#interface DialView : UIView
#property (nonatomic,assign) CGFloat angle;
#end
DialView.m
- (void)setAngle:(CGFloat)angle
{
_angle = angle;
self.transform = CGAffineTransformMakeRotation(angle);
}
The UIButtons can be contained within this view (I'm not sure if you want the buttons to be responsible for the rotation? I'm gonna use a UIPanGestureRecognizer, since it's the most convenient way).
Let's build the view controller that will handle a pan gesture inside our DialView, let's also keep a reference to DialView.
MyViewController.h
#class DialView;
#interface ViewController : UIViewController
// The previously defined dial view
#property (nonatomic,weak) IBOutlet DialView *dial;
// UIPanGesture selector method
- (IBAction)didReceiveSpinPanGesture:(UIPanGestureRecognizer*)gesture;
#end
It's up to you on how you hook up the pan gesture, personally, I made it on the nib file. Now, the main body of this function:
MyViewController.m
- (IBAction)didReceiveSpinPanGesture:(UIPanGestureRecognizer*)gesture
{
// This struct encapsulates the state of the gesture
struct state
{
CGPoint touch; // Current touch position
CGFloat angle; // Angle of the view
CGFloat touchAngle; // Angle between the finger and the view
CGPoint center; // Center of the view
};
// Static variable to record the beginning state
// (alternatively, use a #property or an _ivar)
static struct state begin;
CGPoint touch = [gesture locationInView:nil];
if (gesture.state == UIGestureRecognizerStateBegan)
{
begin.touch = touch;
begin.angle = self.dial.angle;
begin.center = self.dial.center;
begin.touchAngle = CGPointAngle(begin.touch, begin.center);
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
struct state now;
now.touch = touch;
now.center = begin.center;
// Get the current angle between the finger and the center
now.touchAngle = CGPointAngle(now.touch, now.center);
// The angle of the view shall be the original angle of the view
// plus or minus the difference between the two touch angles
now.angle = begin.angle - (begin.touchAngle - now.touchAngle);
self.dial.angle = now.angle;
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
// (To be Continued ...)
}
}
CGPointAngle is a method invented by me, it's just a nice wrapper for atan2 (and I'm throwing CGPointDistance in if you call NOW!):
CGFloat CGPointAngle(CGPoint a, CGPoint b)
{
return atan2(a.y - b.y, a.x - b.x);
}
CGFloat CGPointDistance(CGPoint a, CGPoint b)
{
return sqrt(pow((a.x - b.x), 2) + pow((a.y - b.y), 2));
}
The key here, is that there's two angles to keep track:
The angle of the view itself
The angle formed between your finger and the center of the view.
In this case, we want to have the view's angle be originalAngle + deltaFinger, and that's what the above code does, I just encapsulate all the state in a struct.
Checking the radius
If that you want to keep track on "the border of the view", you should use the CGPointDistance method and check if the distance between the begin.center and the now.finger is an specific value.
That's homework for ya!
Snapping back
Ok, now, this part is an actual animation, since the user no longer has control of it.
The snapping can be achieved by having a set defined of angles, and when the finger releases the finger (Gesture ended), snap back to them, like this:
else if (gesture.state == UIGestureRecognizerStateEnded)
{
// Number of "buttons"
NSInteger buttons = 8;
// Angle between buttons
CGFloat angleDistance = M_PI*2 / buttons;
// Get the closest angle
CGFloat closest = round(self.dial.angle / angleDistance) * angleDistance;
[UIView animateWithDuration:0.15 animations:^{
self.dial.angle = closest;
}];
}
The buttons variable, is just a stand-in for the number of buttons that are in the view. The equation is super simple actually, it just abuses the rounding function to approximate to the closes angle.

Prevent draggable uiimage being moved beyond point on y axis

I have a uiimage that has a pan gesture enabling the user to move the image along the y axis. How can I make it impossible for the image position from becoming <100 for example.
Basically, I have a draggable image that I don't want to be dragged beyond a certain point on the y axis.
I had a long search for something like this but found nothing that I thought was relevent enough to me.
I am very new to programming and would appreciate any help.
Thanks in advance.
If you can drag the image, you have surely a gesture recognizer that calls a callback method during dragging repeatedly. And you probably use the current coordinate provided by the gesture recognizer to update the center property of the image that you drag.
So you could check the current coordinate in the callback method, and if it crossed a certain border, you simply do no longer update the center property of the image.
Or did I understand something wrong?
Maybe you could provide some code. This would make an answer easier.
In the pan handling method you can use something like this:
- (void)panHandle:(UIPanGestureRecognizer*)gesture
{
CGPoint point = [gesture locationInView:self.view];//get the coordinate
//check for current position of image and update only
//if it is below 100 or a desired value
if(point.y <= 100)
{
//update the coordinate of image
}
else
{
point.y = 100;
//update the coordinate. This will make your view move on x axis keeping y limits
}
}

Panning a UIImage with a Gesture Recognizer on a placeholder UIView

I need to crop an image to match a specific dimension. I have three layers in my view starting from the bottom:
I have the raw image in a UIImage. This image comes from the camera. (called cameraImage)
I have a UIView holding this image. When user clicks "crop", the UIView's bounds are used to crop the raw image inside it.
Above all of this I have a guide image which shows the user the dimensions they need to pan, rotate, and pinch their image to fit into.
I want to add the pan gesture to the top guide image and have it control the raw image at the bottom. So the guide image never moves but it is listening for the pan gesture.
I can't figure out how to reset the recognizer without it making my raw image jump back to zero. Maybe someone could help?
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.center = CGPointMake(recognizer.view.center.x+translation.x, recognizer.view.center.y+ translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view];
}
The above code works great in my gesture is attached to the bottom image. The problem is when the user goes outside the view's bounds, the image stops panning and is basically stuck. You can't touch it anymore so it sits there. So I thought if i attached the gesture to the top it would solve this problem.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
cameraImage.center = CGPointMake(recognizer.view.center.x+translation.x, recognizer.view.center.y+ translation.y);
}
This almost works. I set the cameraImage's center and removed the third line which resets the recognizer. If I don't remove it, the cameraImage jumps back into the same position every time I try and pan. It almost works because when you click the image again it doesn't start from the pixel you touched. It moves the image back to the original position and then lets you pan.
First option:
when the recognizer enter the UIGestureRecognizerStateEndedstate
if(recofnizer.state == UIGestureRecognizerStateEnded )
{
...
}
You store the translation at that point in time in an instance varibale (#property) of your class.
And then you always add the saved translation to the new translation.
In code this would look like this:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:recognizer.view];
CGPoint updatedTranslation = CGPointMake(translation.x+self.savedTranslation.x,translation.y+self.savedTranslation.y);
cameraImage.center = CGPointMake(recognizer.view.center.x+updatedTranslation.x, recognizer.view.center.y+ updatedTranslation.y);
if(recofnizer.state == UIGestureRecognizerStateEnded )
{
self.savedTranslation = updatedTranslation;
}
}
Dont forget to add #property (nonatomic, assign) CGPoint savedTranslation; to your interface.
Also make sure the savedTranslation variable is initialized in the init method of your class to self.savedTranslation = CGPointMake(0,0);
Second option:
You should think about doing all that what you want in an scrollview with the imageview as the viewForZooming of the scrollview. This allows very smooth interaction, like users are used to!
Above this scrollview you can then put your mask/guide, but make sure to disable the userInteraction of the mask/guide view to make your user touch the scrollview below!

Resources