How to Ignore Certain Uitouch Points in Multitouch Sequence

How to ignore certain UITouch Points in multitouch sequence

One solution is to store the topmost tap in touchesBegan and only draw this one.

As you have pointed out, you are not supposed to retain the UITouch instance, so I recommend using a weak reference instead.

This will only draw a single touch. If you wish to draw the touches of multiple fingers, you need another way of filtering out the hand (many drawing apps have user settings for telling the app the pose of the hand, for example, but this is of course more complicated).

Here is an idea on how to do it:

#import <QuartzCore/QuartzCore.h>

@interface TViewController () {
// We store a weak reference to the current touch that is tracked
// for drawing.
__weak UITouch* drawingTouch;
// This is the previous point we drawed to, or the first point the user tapped.
CGPoint touchStartPoint;
}
@end
@interface _TDrawView : UIView {
@public
CGLayerRef persistentLayer, tempLayer;
}
-(void)commitDrawing;
-(void)discardDrawing;
@end

@implementation TViewController

- (void) loadView
{
self.view = [[_TDrawView alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.view.opaque = YES;
self.view.multipleTouchEnabled = YES;
self.view.backgroundColor = [UIColor whiteColor];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Start with what we currently have
UITouch* topmostTouch = self->drawingTouch;
// Find the top-most touch
for (UITouch *touch in touches) {
CGPoint lastPoint = [touch locationInView:self.view];
if(!topmostTouch || [topmostTouch locationInView:self.view].y > lastPoint.y) {
topmostTouch = touch;
touchStartPoint = lastPoint;
}
}
// A new finger became the drawing finger, discard any previous
// strokes since last touchesEnded
if(self->drawingTouch != nil && self->drawingTouch != topmostTouch) {
[(_TDrawView*)self.view discardDrawing];
}
self->drawingTouch = topmostTouch;
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Always commit the current stroke to the persistent layer if the user
// releases a finger. This could need some tweaking for optimal user experience.
self->drawingTouch = nil;
[(_TDrawView*)self.view commitDrawing];
[self.view setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
const CGFloat red=0, green=0, blue=0, brush=1;
for (UITouch *touch in touches) {
// Find the touch that we track for drawing
if(touch == self->drawingTouch) {
CGPoint currentPoint = [touch locationInView:self.view];

// Draw stroke first in temporary layer
CGContextRef ctx = CGLayerGetContext(((_TDrawView*)self.view)->tempLayer);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, brush );
CGContextSetRGBStrokeColor(ctx, red, green, blue, 1.0);
CGContextSetBlendMode(ctx,kCGBlendModeNormal);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, touchStartPoint.x, touchStartPoint.y);
CGContextAddLineToPoint(ctx, currentPoint.x, currentPoint.y);
CGContextStrokePath(ctx);
// Update the points so that the next line segment is drawn from where
// we left off
touchStartPoint = currentPoint;
// repaint the layer
[self.view setNeedsDisplay];
}
}
}

@end

@implementation _TDrawView

- (void) finalize {
if(persistentLayer) CGLayerRelease(persistentLayer);
if(tempLayer) CGLayerRelease(tempLayer);
}

- (void) drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
if(!persistentLayer) persistentLayer = CGLayerCreateWithContext(ctx, self.bounds.size, nil);
if(!tempLayer) tempLayer = CGLayerCreateWithContext(ctx, self.bounds.size, nil);

// Draw the persistant drawing
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), persistentLayer);
// Overlay with the temporary drawing
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), tempLayer);
}

- (void)commitDrawing {
// Persist the temporary drawing
CGContextRef ctx = CGLayerGetContext(persistentLayer);
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), tempLayer);
[self discardDrawing];
}
- (void)discardDrawing {
// Clears the temporary layer
CGContextRef ctx = CGLayerGetContext(tempLayer);
CGContextClearRect(ctx, self.bounds);
CGContextFlush(ctx);
}
@end

EDIT: I added the logic that if a new touch is detected, if there is currently any stroke being drawn with a higher y-value, it is removed, as we discussed in the comments.

The overlaying is done by painting two CGLayers. This code could be optimized a lot for performance, it should be looked at more as an illustration than production-ready code.

Undo with multitouch drawing in iOS

m_redoArray appears to be the big daddy, the one you draw from. I don't understand why you empty this out out in 'touchesBegan...', surely one of these arrays must carry through touchesBegan unaltered, or you'll be dropping stuff from the start of your drawing all the way through, no?

It appears to me that this is how you dropped the 'Hell' in your example here..

Track touch points in Multitouch

You are not populating the touchPaths properly. Try setting it after each drawing instead, something like this:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

for (UITouch *touch in touches)
{
NSString *key = [NSString stringWithFormat:@"%d", (int) touch];

CGPoint lastPoint = [[touchPaths objectForKey:key] CGPointValue];

CGPoint currentPoint1 = [touch locationInView:self.view];

UIGraphicsBeginImageContext(self.view.frame.size);
[self.tempDrawImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint1.x, currentPoint1.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);

CGContextStrokePath(UIGraphicsGetCurrentContext());
self.tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext();
[self.tempDrawImage setAlpha:opacity];
UIGraphicsEndImageContext();
// I changed your code here
[touchPaths setObject:[NSValue valueWithCGPoint:currentPoint1] forKey:key];

}
}

You are currently setting lastPoint here but you do not seem to use it (and it would only work with one touch). There is no need to have lastPoint as a field either.

How to disable multitouch?

If you want only one button to respond to touches at a time, you need to set exclusiveTouch for that button, rather than for the parent view. Alternatively, you could disable the other buttons when a button gets the "Touch Down" event.


Here's an example of the latter, which worked better in my testing. Setting exclusiveTouch for the buttons kind-of worked, but led to some interesting problems when you moved your finger off the edge of a button, rather than just clicking it.

You need to have outlets in your controller hooked up to each button, and have the "Touch Down", "Touch Up Inside", and "Touch Up Outside" events hooked to the proper methods in your controller.

#import "multibuttonsViewController.h"

@implementation multibuttonsViewController

// hook this up to "Touch Down" for each button
- (IBAction) pressed: (id) sender
{
if (sender == one)
{
two.enabled = false;
three.enabled = false;
[label setText: @"One"]; // or whatever you want to do
}
else if (sender == two)
{
one.enabled = false;
three.enabled = false;
[label setText: @"Two"]; // or whatever you want to do
}
else
{
one.enabled = false;
two.enabled = false;
[label setText: @"Three"]; // or whatever you want to do
}
}

// hook this up to "Touch Up Inside" and "Touch Up Outside"
- (IBAction) released: (id) sender
{
one.enabled = true;
two.enabled = true;
three.enabled = true;
}

@end

Multitouch with UIPanGestureRecognizer

From the apple documentation for UIGestureRecogniser

(CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(UIView *)view
Parameters
touchIndex
The index of a UITouch object in a private array maintained by the receiver. This touch object represents a touch of the current gesture.
view
A UIView object on which the gesture took place. Specify nil to indicate the window.
Return Value
A point in the local coordinate system of view that identifies the location of the touch. If nil is specified for view, the method returns the touch location in the window’s base coordinate system.

(NSUInteger)numberOfTouches
Return Value
The number of UITouch objects in a private array maintained by the receiver. Each of these objects represents a touch in the current gesture.

Discussion
Using the value returned by this method in a loop, you can ask for the location of individual touches using the locationOfTouch:inView: method.

For example:

(Objective-C)

    for(int i=0; i<[panGestureRecogniser numberOfTouches]; i++)
{
CGPoint pt = [panGestureRecogniser locationOfTouch:i inView:self];
}

(Swift)

    for i in 0..<panGestureRecogniser.numberOfTouches {
let pt = recognizer.location(ofTouch: i, in: panGestureRecognizer.view)
}

As for velocity I believe there is only one value for it and there is no way to get the velocity of each touch without writing a custom method that calculates the difference between each touch over a series of calls. There is no guarantee that the touches will be at the same index each time however.

Note: As for the minimum and maximum number of touches, these will need to be set accordingly to get multiple touches.



Related Topics



Leave a reply



Submit