How to detect when pan gesture moves really quickly, rather than finely, to adjust control accordingly? - ios

In my app I have the option for the user to pan upward to adjust a control, but it's a little slow when they pan up really quick, which I'd like to jump a lot more.
// If user is panning upwards or downwards, adjust WPM every 8 translations in either direction
if (translation.y<-8 || translation.y>8) {
// Reset translation so we can see when it exceeds 8 again
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
// Figure out direction, if pan down, decrease by 5, if up, increase by 5
int sign = (translation.y > 0) ? -1 : 1;
WPM = #([WPM intValue] + (sign * 5));
if ([WPM intValue] >= 200 && [WPM intValue] <= 1500) {
self.WPMLabel.text = [WPM stringValue];
self.wordsPerMinute = WPM;
[[NSUserDefaults standardUserDefaults] setObject:WPM forKey:#"WPM"];
}
}
How would I go about changing this to account for faster acceleration?

Use the velocity property on the gesture recognizer.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIPanGestureRecognizer_Class/Reference/Reference.html#//apple_ref/occ/instm/UIPanGestureRecognizer/velocityInView:

Speed involves time. So each time your code runs, you need to save in an instance variable what time it is on the event's timestamp. That way, the next time your code runs, you can compare both the translation change and the time elapsed to the previous change.
What I would do is experiment with saving three or four previous times and three or four previous locations in an array so that you can take a moving average and damp out speed variations.

Related

How do I find out which direction a user is panning with UIPanGestureRecognizer?

So I am using UIPanGestureRecognizer in my project which I added to a view. I would like to know when a user either goes up, down,left or right. I am using the left and right feature to scrub through video. The up and down gesture is still to be determined. I have used the following code but I can't seem to figure it out. Thanks for the help!
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else {
// user dragged towards the left
print("left")
}
}
EDIT: Using Slider
if let duration = avPlayer?.currentItem?.duration {
let totalSeconds = CMTimeGetSeconds(duration)
let value = Float64(scrubberSlider.value) * totalSeconds
let seekTime = CMTime(value: Int64(value), timescale: 1)
avPlayer?.seek(to: seekTime, completionHandler: { (completedSeek) in
//perhaps do something later here
})
}
Joe's answer is close, but it won't take into account direct vertical or horizontal pans. (I'd comment on his answer except the formatting won't take.) Try this:
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else if vel.x < 0 {
// user dragged towards the left
print("left")
}
if vel.y > 0 {
// user dragged towards the down
print("down")
}
else vel.y < 0 {
// user dragged towards the up
print("up")
In essence, you are getting the CGPoint of the gesture (x,y) and determining the velocity of the movement. You have an alternative to this - taking the starting and ending point:
var startingPoint = CGPoint.zero
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
startingPoint = recognizer.location(in: self.videoView)
}
if recognizer.state == .ended {
let endingPoint = recognizer.location(in: self.videoView)
[ do the same comparing as above]
}
}
The advantage of the second option is you aren't doing unnecessary calculations during the pan. The disadvantage is that there are certain scenarios (like animating view movements) that are not conducive to it.
EDIT: I'm adding a bit more verbiage after reading your comment. It sounds to me that you may not be fully understanding what a pan gesture really is.
Like most (all?) gestures, it has a beginning, an in-between, and and end.
It is a two-dimensional drag with two components, both x and y.
There are actually SEVEN possible states, but FOUR of them (cancelled, failed, possible, recognized) do not happen with a pan gesture, leaving THREE states (began, changed, ended) that trigger.
I threw out one example - moving a view with a pan gesture - earlier. Now I'll try a second one - tracing an outline of, say, the Statue of Liberty in an image.
Here you want all THREE states, in order to know when to being tracing, when the path changes, and when it ends. And restricting this to the change state, I think you can see where both the X and the Y coordinate changes.
So yes, a logging of "left, up, left, up, left" is quite possible.I would think that if you traced a completely vertical line across the entire screen you might expect all "up" or "down" values in your log, but the odds of any human being panning that perfect is unlikely, so sure, a few "left" or "rights" may happen.
My tweak to Joe's code was to eliminate those moments of perfection. If vel.x == 0 you would have "left", and where bel.y == 0 you would have "down".
Again, if you simply want to know what the "result" of the pan is, use .began and .ended and ignore .changed - do not use recognizer.velocity but recognizer.state.
The "if" statements both of us gave you are really frameworks. If you understand both state and the two-dimensional nature of things, and you need to use .changed, then adapt those "if" statements - maybe compare the velocity of X to Y and take the greater, or eliminate those changes where the change in X or Y was under a threshold.
Try this code: tested in Swift 3.
Updated Answer: Below code will give you a starting and end location of your view when touch began.
if recognizer.state == .began {
let vel = recognizer.velocity(in: view) // view is your UIView
if vel.x > 0 {
print("right")
} else {
print("left")
}
}
if recognizer.state == .ended {
let vel = recognizer.velocity(in: view)
if vel.y > 0 {
print("down")
} else {
print("up")
}
}
Note : Your answer actually hidden in your code ?
#IBAction func panVideo(_ recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self.videoView)
if vel.x > 0 {
// user dragged towards the right
print("right")
}
else {
// user dragged towards the left
print("left")
}
if vel.y > 0 {
// user dragged towards the down
print("down")
}
else {
// user dragged towards the up
print("up")
}
}
hope this helps...
Okay, now I'm getting the correct mental picture. You want scrub control. This is something very different, and I would recommend a UISlider over working with gestures - highly recommend it. For starters, they have the pan gesture already built in! Here's what I think apps like YouTube, QuickTime, etc. do.
(1) Let's take a specific example of having a video that is 1:53:22 in length, or (1*60*60)+(53*60)+22 = 6802 seconds in length.
(2) Add a "scrubber" subview to your main screen. You'll probably want a UISlider, two UILabels (one to each side of the slider), and anything else you think for a polished look.
(3) The UISLider will have a minimumValue of 0 seconds and a maximumValue of 6802 seconds. Of course, you'll want that max value to be calculated on each change of source.
(4) A question you'll want to answer for your app is whether to go the route of iTunes (where this scrubber view is always visible) or YouTube (where it is overly visible when the user or mouse cursor hovers over an area). For the former, you just need to position this scrub view in a position on the screen. For the latter though, you may wish to use a pan gesture - but only for visibility. Hold that thought.
(5a) You need two, maybe three more things on you UISlider. First is an automatic value update. Again it will depend on the visibility of the entire scrub view. You want to update, once a second, both the left hand UILabel and the UISLider value if it's always visible. For a disappearing one you probably can get away with only updating it once a second when it's visible.
(5b) The second thing you need to do with the UISlider is track changes the user makes to it (the "scrubbing") while it's visible. The event you are looking for is UIControl.valueChanged(). It will trigger anytime the user works with the slider, giving you the new seconds value to "scrub" the video to.
(5c) The third thing you might want to do with the UISlider is customize it a few ways - change the thumb image and the slider itself. My app changes the thumb image. These can only be done in code, there are no IB properties available.
Back to #4. All of the above doesn't need a pan gesture, unless you want the scrub view to appear only when needed.
If you have a mental picture of what I've described above, all you want to know is if a pan gesture has happened. No regards for direction. You might wish to have some regards for screen area - do want this scrub view to appear when a user pans over an area where the scrub view will not appear?
Wire up a CALayer (or the entire video view) with the pan gesture. Then code for a state of UIGestureRecognizer.began. Make the scrub view visible by changing it's alpha state from 0 to 1, or "sliding" it into view by changing it's origin or height. Add a UIView.animate(withDuration:) to it for a good effect.
Now, all that's left is setting the scrub view back to it's natural state. You'll need to code the reverse of whatever you did, and attach it to a timer set for however many seconds you want it visible.
TL;DR;
My app uses 4 UISliders that change various things (height, width, saturation, grill thickness) of of a photo effect that uses CoreImage. Performance is very tight, about 5/100 of a second to grab the new values of all 4 sliders and update the image.
These sliders are always visible today, but my next update (about 2 weeks away) will feature a "sliding control board" - think a keyboard with sliders and other controls on it. (There's limitations on the alpha value for a custom keyboard that forced me to write my own, but that's a separate discussion.)
So I know a "sliding scrub view" is possible. What I don't know for you is if you set the alpha value to a view to zero, will it detect pan gestures? I don't know, thus a CALayer may be needed.

Smooth snake movement

This is language-agnostic question, more about model of my game.
I have a snake game with elements, but I move the elements smoothly, they don't just move 1 block each time, but instead they move some amount of pixels every frame.
I have an update loop that calculates the positions of the element, but I am stuck on correct calculations.
I have heading for each element:
typedef NS_ENUM(int, kElementHeading)
{
kElementHeadingNorth = 1,
kElementHeadingSouth,
kElementHeadingEast,
kElementHeadingWest
};
I also have velocity (x, y) that determines in what direction snake is going. I have problem with snake movement, because my elements are in wrong positions. I managed to localize the thing for 2 elements, but my solution fails on more elements.
First solution I tried is to save point of rotation where the head changes direction. This worked, but due to different circumstances element can move different amount of pixels each turn. Often the element would skip the point. I tried increasing the zone where it should rotate, but it adds up error. I tried fixing this error, but element would still separate from snake (quite often).
On the second try I decided to keep the snake head in center of the screen and move the world around it. It worked good for 2 elements, as I just smoothly move the next element to desired position relatively to head. But this fails badly on more elements. If you make fast turns they start dancing and not following the path.
Third thing that I tried is leaving a path for other elements to follow. But that didn't work because I intend to keep my snake on center of the screen and technically it never moves to create a path.
I'm looking to replicate the movement pattern like in Nimble Quest (or any snake).
How should I implement snake elements moving to have no errors?
Here is my code for the first method, problem with it is that often the elements would fall off. The code is pretty self-explanatory. Rotation points are the places where to change direction.
CFTimeInterval delta = self.lastTime - currentTime;
CGPoint currentPosition = self.playerSnake.head.sprite.position;
CGPoint velocity = self.playerSnake.velocity;
self.playerSnake.head.sprite.position = CGPointMake(currentPosition.x + velocity.x * delta * CONSTANTSPEEDFACTOR , currentPosition.y + velocity.y * delta * CONSTANTSPEEDFACTOR);
for (SnakeElement *element in self.playerSnake.elements) {
CGPoint currentPositionE = element.sprite.position;
CGPoint velocityE = element.velocity;
element.sprite.position = CGPointMake(currentPositionE.x + velocityE.x * delta * CONSTANTSPEEDFACTOR , currentPositionE.y + velocityE.y * delta * CONSTANTSPEEDFACTOR);
}
BOOL markToDelete = NO;
NSDictionary *deleteDictionary;
for (NSDictionary *dict in self.playerSnake.rotationPoints) {
CGPoint positionCoordinate = CGPointFromString(dict[#"position"]);
CGPoint velocityNew = CGPointFromString(dict[#"velocity"]);
double newAngle = [dict[#"angle"] doubleValue];
for (SnakeElement *element in self.playerSnake.elements) {
int xDifference = element.sprite.position.x - positionCoordinate.x;
int yDifference = element.sprite.position.y - positionCoordinate.y;
if ((xDifference > -2 && xDifference < 2) && (yDifference > -2 && yDifference < 2) ) {
element.velocity = velocityNew;
element.sprite.position = CGPointMake(element.sprite.position.x + xDifference, element.sprite.position.y + yDifference);
SKAction *action = [SKAction rotateToAngle:newAngle duration:0.2 shortestUnitArc:YES];
[element.sprite runAction:action];
if ([element isEqual:[self.playerSnake.elements lastObject]]) {
markToDelete = YES;
deleteDictionary = dict;
}
}
}
}
[self.playerSnake.rotationPoints removeObject:deleteDictionary];
If I try increase the catch zone for the turning point, the elements tend to fall off more often then when it is 1 or 2 pixels wide. I'm not sure why this happens.
This is what I was suggesting you do in the comments in terms of handling your turning on points :
1.. calculate the distance that the element should move that frame based on speed and your elapsed time since last frame. (delta)
2.. calculate distance from element's current position to the turn point. This is the beforeDistance I spoke of in the comments
3.. calculate the distance the element should move towards the NEW target turning point AFTER the turn
afterDistance = distanceToMoveThisFrame - beforeDistance
4.. Calculate the new position for your element, starting at the current turning point towards the next target turning point of the element using afterDistance
If you follow this logic, you will NEVER overshoot or undershoot the turning point.

How do I calculate the speed of which a user drags an image?

I have an image that the user can drag to the right and it will spring back when the user releases it. I want to execute some code when a user drags it quickly and releases it. Now I have a very awkward requirement that the user can drag the image, then keep it still for any length of time (for example 5 seconds), then drag it quickly and release it. As long as the image is moving above a certain speed when it is released, it will execute the code. If it falls below the minimum speed, it executes some different code. So that means I can't calculate the length of time between the beginning of the gesture and the end and execute the code depending on the length of time. What can I do? I guess I somehow need to know the speed at which the image is moving in it's last 500 milliseconds before the gesture ends. However I've hit a brick wall figuring out how to do that. Any help would be greatly appreciated.
Can you please include an explanation and possible example code with your answer as that would be a great help.
If you get the start X,Y coordinates of when the image is dragged, and the X,Y coordinates for when the mouse is released, you can use pythagoras' theorm to calculate the distance between the two points: http://en.wikipedia.org/wiki/Pythagorean_theorem
Also, if you start a timer when the mouse is moved (and mouse button is down), and stop it in the mouseup event, you can calculate the speed using the time and distance (speed = distance / time)
edit following comments:
point delayedMousePos;
point previousMousePos;
bool secondDrag = false;
bool isStopped = false;
var timeFirstStopped;
var positionCount = 0;
array previousMousePositions[3];
// timer which monitors mouse position (set to an interval of say, 10ms)
function timerMonitorMousePos_Elapsed() {
point currentMousePos = getMousePos();
if (isStopped == false) {
if (positionCount >= 2) {
array_shift(previousMousePositions); // remove the first element of the array and move everything down to reindex numerical array to start counting from zero
positionCount = 2; // keep positionCount within array bounds
}
previousMousePositions[positionCount] = currentMousePos; // add the new position to the end of the 'stack'
positionCount++;
}
if (currentMousePos == previousMousePos) { // start check for stationary
isStopped = true;
if (timeFirstStopped == null) {
timeFirstStopped = NOW();
} else {
if (NOW() - timeFirstStopped >= 500) { // we have been stopped for at least 500ms (assumes time is counted in milliseconds)
secondDrag = true;
// previousMousePositions[0] = the mouse position 30ms before the mouse stopped
}
}
} else {
isStopped = false;
timeFirstStopped = null;
}
previousMousePos = currentMousePos;
}
I wouldn't use a timer. I would just save the starting date/time along with x,y position when the dragging starts.
When the dragging has ended, save the ending date/time and position. From those information, I can calculate the distance in pixel and duration in milliseconds.
After searching some more on the internet, I finally answered my own question.
I worked out what I needed to do:
My UIPanGestureRecognizer:
- (IBAction)handlePan3:(UIPanGestureRecognizer *)recognizer3
Get the velocity of the users finger moving across the screen:
CGPoint vel = [recognizer velocityInView:self.myView];
Then:
if (vel.x > /*value*/) {
// Your code
}
I was about to give up, but no! I got there in the end. Thanks for everyones help. I've upvoted one or two answers because they were helpful. bobnoble actually gave the suggestion to use velocityInView and I found this other stack overflow question which gave me the info I needed: iOS - Making sense of velocityInView on UIPanGesture

Decelerate Update of UiLabel

I've spent a few hours tonight trying to get two UILabels I have that are already being refreshed as a gesture is recognized (based on the translation.y and velocity.y). In the
if (recognizer.state == UIGestureRecognizerStateEnded)
I want to animate a deceleration of the refresh of the UILabels after the gesture is complete. The UILabels are refreshed just by calling a
[self refreshLabels];
I've spent a lot of time tonight trying to do this with an infinite scrollView and tracker of the .contentOffset.y which failed miserably. I also tried to do a for and while loop in the above if statement with an animation block which also didn't work.
Does anyone have any suggestions/done this before?
How about a recursive call of of refreshLabels using performSelector:withObject:afterDelay:?
When the gesture ends, set a private counter and call refreshLabels. Within refreshLabels, calculate a simple deceleration curve using the counter as the "time taken" variable and use the resulting value as the delay for the next recursive call.
Example code:
int counter; // Set this value to 1 when the gesture completes
- (void)refreshLabels:(id)sender{
// Update the labels using whatever code you have
// This code will execute the recursive deceleration
if (counter > -1){
float somereasonablenumber = 0.1F;
float delaytime = (1.0-0.1)/counter; // deceleration = (finaltime-initialtime)/timetaken
counter++;
if (delaytime > somereasonablenumber){
[self performSelector:#selector(refreshLabels:) withObject:nil afterDelay:delaytime];
} else {
counter = -1;
}
}
}
You might need to play with the values I used for finaltime and initialtime to get the curve you want. Right now, this code would execute on a delay curve like:
0.9s
0.45s
0.3s
0.225s
0.18s
0.15s
0.128571429s
0.1125s

Jumping effect in games

I'm currently trying to make a basic platformer with XNA and I'm wondering how to create a "jumping effect." I currently have basic keyboard input which allows for sideways movement, but I would like my sprite to slowly progress into a jump rather than instantly teleporting there (right now I have something like Rectangle.Y += 40 every time I jump, making the sprite instantly appear there). Does anyone have any insight?
I'm not totally across how to implement this in XNA/C#, but in Flash games I've made I just added a vertical velocity property. I'll try write everything as C# as I can..
Example; create the velocity property:
float verticalVelocity = 0;
Vertical velocity should be constantly reduced (by gravity). Set up a gravity property somewhere accessible from your player:
float Gravity = 2.5;
And in your update() method for the player, increment the verticalVelocity by Gravity. Also increment the Y position of your player by the verticalVelocity. This will simulate falling:
verticalVelocity += Gravity;
Position.Y += verticalVelocity; // this may be -= in XNA, not sure where the y axis beings
When you hit a surface, the velocity should be reset to 0.
And finally, to jump, simply subtract a given value from verticalVelocity:
public void Jump(float height)
{
// Only jump if standing on a surface.
if(verticalVelocity == 0)
verticalVelocity -= height;
}
You'll eventually want to add gravity and possibly other forces to your game, so I highly recommend you save yourself a lot of pain and implement some kind of basic force system. This can be done using Vector2s, as you can just add them to the speed of your character. Then just apply an instantaneous force to your character to push it up.
If you really don't want to use a physics engine, you can make a Vector2 with the high point of the jump for the Y and the characters X, and then use the Vector2.Lerp method to interpolate between the characters position and the end point of the jump.
This is generally a very bad system to use, and I highly recommend you either use an existing physics engine, or make your own simple one.
use a sinusoidcode should look something like this:
float ground = 0.0f;
float angle = 330.0f;
jump(){
if(ground == 0.0f)ground = Rectangle.Y;
if(Rectangle.Y <= ground)
{
Rectangle.Y+=Math.Sin(angle/(Math.Pi*180));
angle++;
}
}
You can accurately create a gravity effect if you modify the ySpeed dynamically, as opposed to just adding 40.
You want to declare a ySpeed
ySpeed = 0;
Then you want to use an acceleration variable
acceleration = 0.25;
Okay, now that we've done that, let's add gravity, provided that our player isn't touching the floor.
if(playerLocationY + playerHeight > floorLocationY)
{
gravity = false;
}
else
{
gravity = true;
}
if(gravity)
{
ySpeed += acceleration;
}
Now that we've got that down, we want to include something that allows us to jump.
if(KeyPressed == UP)
{
ySpeed -= acceleration;
}
This will move our player in the upward direction
We now want to make sure we actually move, so let's add one last line and we're done.
playerLocationY += ySpeed;
Congratulations, you made it.

Resources