Is there a way to ensure that any tap that includes more than some amount of movement is discarded? As it is, what counts as a tap can involve a lot of sliding of the finger. I would like to process a "tap and move" differently by using touchesBegan:, touchesMoved:, etc..
Probably not the answer you are looking for. But I've worked around this by instead doing it myself in the regular touches sequence. For this to work, you would also want to have self.multipleTouchEnabled = NO
#interface myView(){
CGPoint _touchStartPoint;
}
#end
#implementation myView
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
_touchStartPoint = [[touches anyObject] locationInView:self];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
[self checkDistance: [[touches anyObject] locationInView:self]];
}
-(void)checkDistance:(CGPoint)p{
static CGFloat dX;
dX = p.x - _touchStartPoint.x;
static CGFloat dY;
dY = p.y - _touchStartPoint.y;
static CGFloat dist;
dist = sqrt(dX*dX + dY*dY);
/* movement of less than 10 pixels */
if(dist < 10){
[self tap];
}
}
-(void)tap{
/* do something with your tap*/
}
#end
Related
I am making a rotatable wheel control. You can drag anywhere on it to spin the control by it's central point.
It's not behaving correctly. It usually works well, but for some portion of a rotation, it starts rotating in the opposite direction for a brief period, and then it starts rotating with your finger again. EDIT: Upon further testing, I realized that you don't need to rotate it 360 degrees for it to rotate in the wrong direction.
A possibly unrelated problem is that the control seems to move slower then your finger. If a solution to the original problem doesn't solve this problem, I'll ask a separate question.
I posted an example project. If you would prefer to reproduce the project yourself, here are the steps with code:
Create a new single view application.
Create a new UIView subclass (in Objective-C) called NNReadyIndicator
Copy and paste this code in `NNReadyIndicator.m
#import "NNReadyIndicator.h"
#interface NNReadyIndicator ()
#property CGAffineTransform startTransform;
#property double originalAngle;
#end
#implementation NNReadyIndicator
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
#pragma mark tracking
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchPoint = [touch locationInView:self];
[self setOriginalAngle:[NNReadyIndicator calculateAngleWithX1:touchPoint.x x2:self.center.x y1:touchPoint.y y2:self.center.y]];
[self setStartTransform:self.transform];
NSLog(#"BEGIN TRACKING | originalAngle: %f", self.originalAngle);
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchPoint = [touch locationInView:self];
double angle = [NNReadyIndicator calculateAngleWithX1:touchPoint.x x2:self.center.x y1:touchPoint.y y2:self.center.y];
CGFloat angleDifference = (CGFloat)(self.originalAngle - angle);
[self setTransform: CGAffineTransformRotate(self.startTransform, -angleDifference)];
NSLog(#"CONTINUE TRACKING | angle: %f angleDifference: %f", angle, angleDifference);
return YES;
}
#pragma mark -- Calculate Angle
+ (double)calculateAngleWithX1:(double)x1 x2:(double)x2 y1:(double)y1 y2:(double)y2 {
double distanceX = [NNReadyIndicator calculateDistanceWithPoint:x1 point:x2];
double distanceY = [NNReadyIndicator calculateDistanceWithPoint:y1 point:y2];
return [self calculateAngleWithDistanceX:distanceX distanceY:distanceY];
}
+ (double)calculateDistanceWithPoint:(double)pointA point:(double)pointB {
double distance = pointA - pointB;
if (distance < 0) {
distance = -distance;
}
return distance;
}
+ (double)calculateAngleWithDistanceX:(double)distanceX distanceY:(double)distanceY {
return atan2(distanceY, distanceX);
}
#end
EDIT: Added logging
Why is the control not consistently rotating in the same direction (and at the same speed?) as a finger that is dragging it?
You need to flip the angle if it is above or below 360 degrees. Also, to make it easier to understand, you will have to convert it into degrees and then back. Based off of this answer: https://stackoverflow.com/a/19617316/2564682
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint touchPoint = [touch locationInView:self];
double angle = [NNReadyIndicator calculateAngleWithX1:touchPoint.x x2:self.center.x y1:touchPoint.y y2:self.center.y];
double angleInDegrees = angle * 180 / M_PI;
// Flip angle if above or below 360 degrees
if (angleInDegrees > 180) {
angleInDegrees -= 360;
} else if (angleInDegrees < -180) {
angleInDegrees += 360;
}
CGFloat angleDifference = (CGFloat)(self.originalAngle - angleInDegrees);
[self.layer setTransform: CATransform3DRotate(self.layer.transform, (float)DEGREES_TO_RADIANS(-angleDifference), .0, .0, 1.0)];
NSLog(#"CONTINUE TRACKING | angle: %f angleDifference: %f", angleInRadians, angleDifference);
return YES;
}
I have the current code:
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.objectPoint = [[touches anyObject] locationInView:self];
float x, y;
if (self.objectPoint.x > self.objectPoint.x) {
x = self.objectPoint.x + 1;
}
else x = self.objectPoint.x - 1;
if (self.fingerPoint.y > self.objectPoint.y) {
y = self.objectPoint.y + 1;
}
else y = self.minionPoint.y - 1;
self.objectPoint = CGPointMake(x, y);
[self setNeedsDisplay];
}
My problem is that I want to keep the object follow your finger until you take your finger off the screen. It will only follow if my finger is moving. touchesEnded only works when I take my finger off the screen, so that's not what I want either. How can I enable something that would solve my problem?
If you want to touch a part of the screen and you want to move the drawn object in that direction as long as you're holding your finger down, there are a couple of approaches.
On approach is the use of some form of timer, something that will repeatedly call a method while the user is holding their finger down on the screen (because, as you noted, you only get updates to touchesMoved when you move). While NSTimer is the most common timer that you'd encounter, in this case you'd want to use a specialized timer called a display link, a CADisplayLink, that fires off when screen updates can be performed. So, you would:
In touchesBegan, capture where the user touched on the screen and start the CADisplayLink;
In touchesMoved, you'd update the user's touch location (but only called if they moved their finger);
In touchesEnded, you'd presumably stop the display link; and
In your CADisplayLink handler, you'd update the location (and you'd need to know the speed with which you want it to move).
So, that would look like:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.velocity = 100.0; // 100 points per second
self.touchLocation = [[touches anyObject] locationInView:self];
[self startDisplayLink];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchLocation = [[touches anyObject] locationInView:self];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self stopDisplayLink];
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.lastTimestamp = CACurrentMediaTime(); // initialize the `lastTimestamp`
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
// figure out the time elapsed, and reset the `lastTimestamp`
CFTimeInterval currentTimestamp = CACurrentMediaTime();
CFTimeInterval elapsed = currentTimestamp - self.lastTimestamp;
self.lastTimestamp = currentTimestamp;
// figure out distance to touch and distance we'd move on basis of velocity and elapsed time
CGFloat distanceToTouch = hypotf(self.touchLocation.y - self.objectPoint.y, self.touchLocation.x - self.objectPoint.x);
CGFloat distanceWillMove = self.velocity * elapsed;
// this does the calculation of the angle between the touch location and
// the current `self.objectPoint`, and then updates `self.objectPoint` on
// the basis of (a) the angle; and (b) the desired velocity.
if (distanceToTouch == 0.0) // if we're already at touchLocation, then just quit
return;
if (distanceToTouch < distanceWillMove) { // if the distance to move is less than the target, just move to touchLocation
self.objectPoint = self.touchLocation;
} else { // otherwise, calculate where we're going to move to
CGFloat angle = atan2f(self.touchLocation.y - self.objectPoint.y, self.touchLocation.x - self.objectPoint.x);
self.objectPoint = CGPointMake(self.objectPoint.x + cosf(angle) * distanceWillMove,
self.objectPoint.y + sinf(angle) * distanceWillMove);
}
[self setNeedsDisplay];
}
and to use that, you'd need a few properties defined:
#property (nonatomic) CGFloat velocity;
#property (nonatomic) CGPoint touchLocation;
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval lastTimestamp;
If you want to drag it with your finger, you want to:
In touchesBegan, save the starting locationInView as well as the "original location" of the object being dragged;
In touchesMoved, get the new locationInView, calculate the delta (the "translation") between that and the original locationInView, add that to the saved "original location" of the view, and use that to update the view.
That way, the object will track 100% with your finger as you're dragging it across the screen.
For example, you might:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.touchBeganLocation = [[touches anyObject] locationInView:self];
self.originalObjectPoint = self.objectPoint;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint location = [[touches anyObject] locationInView:self];
CGPoint translation = CGPointMake(location.x - self.touchBeganLocation.x, location.y - self.touchBeganLocation.y);
self.objectPoint = CGPointMake(self.originalObjectPoint.x + translation.x, self.originalObjectPoint.y + translation.y);
[self setNeedsDisplay];
}
Probably needless to say, you need properties to keep track of these two new CGPoint values:
#property (nonatomic) CGPoint originalObjectPoint;
#property (nonatomic) CGPoint touchBeganLocation;
Frankly, I might use gesture recognizer, but that's an example of dragging with touchesBegan and touchesMoved.
I am having issue with collision behavior.I have 2 types of objects falling from up to bottom of the screen and collisioning with image in the bottom of the screen. Collision are working great, but soon as I move the image, it gets rescaled and blinks very often.Thanks for advice.
Code for moving the image.
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
//CGPoint svpt = [[touches anyObject] locationInView:self.view];
CGPoint pt = [[touches anyObject] locationInView:self.playerMove];
CGRect frame = [self.playerMove frame];
frame.origin.x += (pt.x - self.startLocation.x);
frame.origin.y += (pt.y - self.startLocation.y);
frame.origin.x = MIN(MAX(frame.origin.x, -10), 240);
frame.origin.y = MIN(MAX(frame.origin.y, 430), 430);
self.playerMove.frame = frame;
[_animator updateItemUsingCurrentState:self.playerMove];
}
Code for collision.
_collision = [[UICollisionBehavior alloc]initWithItems:#[self.hammerImage,self.playerMove]];
[_animator addBehavior:_collision];
Although I don't know where the exact problem is, I will tell you this: UIKitDynamics works by changing the view's frame and transform. So, the problem is probably that you are messing with transform by explicitly modifying frame. Try using CGAffineTransformTranslate instead.
If you want to move an object that has been added to an animator, I think you want to add an attachment behavior to playerMove, and drag the anchor point in your touchesMoved method. So first, create the attachment behavior, and add it to the animator:
self.ab = [[UIAttachmentBehavior alloc] initWithItem:self.playerMove attachedToAnchor:self.playerMove.center];
[_animator addBehavior:self.ab];
Then in your touchesBegan and touchesMoves, do something like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint pt = [[touches anyObject] locationInView:self];
self.currentLocation = pt;
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGPoint pt = [[touches anyObject] locationInView:self];
self.ab.anchorPoint = CGPointMake(self.ab.anchorPoint.x + (pt.x - self.currentLocation.x), self.ab.anchorPoint.y + (pt.y - self.currentLocation.y));
self.currentLocation = pt;
}
So,
i am trying to do a very simple disc rotation (2d), according to the user touch on it, just like a DJ or something.
It is working, but there is a problem, after certain amount of rotation, it starts going backwards, this amount is after 180 degrees or as i saw in while logging the angle, -3.14 (pi).
I was wondering, how can i achieve a infinite loop, i mean, the user can keep rotating and rotating to any side, just sliding his finger?
Also a second question is, is there any way to speed up the rotation?
Here is my code right now:
#import <UIKit/UIKit.h>
#interface Draggable : UIImageView {
CGPoint firstLoc;
UILabel * fred;
double angle;
}
#property (assign) CGPoint firstLoc;
#property (retain) UILabel * fred;
#end
#implementation Draggable
#synthesize fred, firstLoc;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
angle = 0;
if (self) {
// Initialization code
}
return self;
}
-(void)handleObject:(NSSet *)touches
withEvent:(UIEvent *)event
isLast:(BOOL)lst
{
UITouch *touch =[[[event allTouches] allObjects] lastObject];
CGPoint curLoc = [touch locationInView:self];
float fromAngle = atan2( firstLoc.y-self.center.y,
firstLoc.x-self.center.x );
float toAngle = atan2( curLoc.y-(self.center.y+10),
curLoc.x-(self.center.x+10));
float newAngle = angle + (toAngle - fromAngle);
NSLog(#"%f",newAngle);
CGAffineTransform cgaRotate = CGAffineTransformMakeRotation(newAngle);
self.transform = cgaRotate;
if (lst)
angle = newAngle;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch =[[[event allTouches] allObjects] lastObject];
firstLoc = [touch locationInView:self];
};
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleObject:touches withEvent:event isLast:NO];
};
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleObject:touches withEvent:event isLast:YES];
}
#end
And in the ViewController:
UIImage *tmpImage = [UIImage imageNamed:#"theDisc.png"];
CGRect cellRectangle;
cellRectangle = CGRectMake(-1,self.view.frame.size.height,tmpImage.size.width ,tmpImage.size.height );
dragger = [[Draggable alloc] initWithFrame:cellRectangle];
[dragger setImage:tmpImage];
[dragger setUserInteractionEnabled:YES];
dragger.layer.anchorPoint = CGPointMake(.5,.5);
[self.view addSubview:dragger];
I am open to new/cleaner/more correct ways of doing this too.
Thanks in advance.
Flip the angle if it's below -180 or above 180 degrees. Consider the following touchesMoved implementation:
#implementation RotateView
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
CGFloat angleBetweenLinesInDegrees(CGPoint beginLineA, CGPoint endLineA, CGPoint beginLineB, CGPoint endLineB)
{
CGFloat a = endLineA.x - beginLineA.x;
CGFloat b = endLineA.y - beginLineA.y;
CGFloat c = endLineB.x - beginLineB.x;
CGFloat d = endLineB.y - beginLineB.y;
CGFloat atanA = atan2(a, b);
CGFloat atanB = atan2(c, d);
// convert radians to degrees
return (atanA - atanB) * 180 / M_PI;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint curPoint = [[touches anyObject] locationInView:self];
CGPoint prevPoint = [[touches anyObject] previousLocationInView:self];
// calculate rotation angle between two points
CGFloat angle = angleBetweenLinesInDegrees(self.center, prevPoint, self.center, curPoint);
// Flip
if (angle > 180) {
angle -= 360;
} else if (angle < -180) {
angle += 360;
}
self.layer.transform = CATransform3DRotate(self.layer.transform, DEGREES_TO_RADIANS(angle), .0, .0, 1.0);
}
#end
When dragging around the outer bounds of the view, it will rotate it continuously like a spinning wheel. Hope it helps.
You have some problems here:
1-)
CGPoint curLoc = [touch locationInView:self];
and
firstLoc = [touch locationInView:self];
You are transforming your view, and then asking for the location of a touch in it. You cannot get the correct location of a touch in a rotated view.
Make them something not transformed. (for example self.superview after putting it in a container)
2-)
cellRectangle = CGRectMake(-1,self.view.frame.size.height,tmpImage.size.width ,tmpImage.size.height );
You are placing your Draggable instance out of the screen by passing self.view.frame.size.height as the CGRect's y parameter.
I just implemented this:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint translation = [panGestureRecognizer translationInView:someView];
return fabs(translation.y) > fabs(translation.x);
}
(As outlined here.)
But if the user pans vertically just over the diagonal it will start. How do I make the tolerance much more strict for what it considers vertical?
Basically, the image below describes what I'm after. The first diagram is what it detects now, anything within that area, and the second is what I want it to do.
You can use atan2f given x and y values to calculate the angle from vertical. For example, to start the gesture if the angle is less than 4 degrees from vertical, you can do something like this:
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gesture {
CGPoint translation = [gesture translationInView:gesture.view];
if (translation.x != 0 || translation.y != 0) {
CGFloat angle = atan2f(fabs(translation.x), fabs(translation.y));
return angle < (4.0 * M_PI / 180.0); // four degrees, but in radians
}
return FALSE;
}
Detecting pure vertical gestures, I assume that translation.x == 0 then.
You should as well, check the correct answer from the post you referenced. Where he compares the previous location with the current one. You can create the sensibility. You can check my project, for example to see that, where I use this sensibility to define, when an action is valid (less or equal than the sensibility) or invalid (bigger than the sensibility). Check the MOVEMENT_SENSIBILITY inside the RPSliderViewController.m.
I've written a UIGestureRecognizer subclass for that purpose once. It only tracks the vertical translation. Maybe that helps you. You can use it as any other gesture recognizer, just set the threshold and track the translation in the it's target's action method.
VerticalPanGestureRecognizer.h
#import <UIKit/UIKit.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface VerticalPanGestureRecognizer : UIGestureRecognizer
#property (assign, nonatomic)float translation;
#property (assign, nonatomic)float offsetThreshold;
#end
VerticalPanGestureRecognizer.m
#import "VerticalPanGestureRecognizer.h"
#interface VerticalPanGestureRecognizer ()
{
CGPoint _startPoint;
}
#end
#implementation VerticalPanGestureRecognizer
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([touches count] > 1) {
self.state = UIGestureRecognizerStateFailed;
}
else
{
_startPoint = [[touches anyObject] locationInView:self.view];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.state == UIGestureRecognizerStateFailed || self.state == UIGestureRecognizerStateCancelled) {
return;
}
CGPoint currentLocation = [[touches anyObject] locationInView:self.view];
CGPoint translation;
translation.x = currentLocation.x - _startPoint.x;
translation.y = currentLocation.y - _startPoint.y;
if (self.state == UIGestureRecognizerStatePossible)
{
//if the x-translation is above our threshold the gesture fails
if (fabsf(translation.x) > self.offsetThreshold)
self.state = UIGestureRecognizerStateFailed;
//if the y-translation has reached the threshold the gesture is recognized and the we start sending action methods
else if (fabsf(translation.y) > self.offsetThreshold)
self.state = UIGestureRecognizerStateBegan;
return;
}
//if we reached this point the gesture was succesfully recognized so we now enter changed state
self.state = UIGestureRecognizerStateChanged;
//we are just insterested in the vertical translation
self.translation = translation.y;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
//if at this point the state is still 'possible' the threshold wasn't reached at all so we fail
if (self.state == UIGestureRecognizerStatePossible)
{
self.state = UIGestureRecognizerStateFailed;
}
else
{
CGPoint currentLocation = [[touches anyObject] locationInView:self.view];
CGPoint translation;
translation.x = _startPoint.x - currentLocation.x;
translation.y = _startPoint.y - currentLocation.y;
self.translation = translation.y;
self.state = UIGestureRecognizerStateEnded;
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
self.state = UIGestureRecognizerStateCancelled;
}
- (void)reset
{
[super reset];
_startPoint = CGPointZero;
}
#end