Question

I am working on a drawing app for ipad, I am providing full screen for drawing. So as we all now, user might write with his wrist support or by resting his hand on the screen. So my aim is to allow the user to write freely with his wrist/hand support.

But the app should only detect finger drawing, or ignore/reject wrist and hand touches and delete them

I started working on it , I created a sample project with multitouch enabled.

Below is my code

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

    mouseSwiped = NO;

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

        [touchPaths setObject:[NSValue valueWithCGPoint:lastPoint] forKey:key];
    }

}


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{    
    mouseSwiped = YES;
    CGPoint lastPoint3;

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

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


        currentPoint1 = [touch locationInView:self.view];

        NSLog(@"Y:%f",currentPoint1.y);


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

        CGContextStrokePath(UIGraphicsGetCurrentContext());
        self.tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext();
        [self.tempDrawImage setAlpha:opacity];
        UIGraphicsEndImageContext();

       [touchPaths setObject:[NSValue valueWithCGPoint:currentPoint1] forKey:key];
    }
}

So this works fine with any number of touches, But I am not understanding how can I reject those palm/hand touches while drawing and only draw, what user draws with his finger/ stylus.

Presently if I draw, I get this thing, below is the image

Here I have drawn with my hand support, you can see below "Hello" their is few weird drawing has happened. How I can reject those touches and delete them and only draw hello

Thanks

Ranjit

Was it helpful?

Solution

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.

OTHER TIPS

Use a UIPanGestureRecognizer to handle the touches instead, and set its maximumNumberOfTouches property to 1, so that it will only recognize one touch at a time.

The gesture recognizer will handle ignoring things that are just taps, because it's specifically for recognizing pans. Also by setting the max number of touches to 1, once they start writing, no other touches will have any effect on it, it'll automatically continue to track just the first touch.

EDIT:

Here's a simple example, starting with a basic single view application template and replacing everything below the #import statement with the following:

@interface ViewController ()

@property (nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.imageView];

    self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(performPanGesture:)];
    self.panGestureRecognizer.maximumNumberOfTouches = 1;
    [self.view addGestureRecognizer:self.panGestureRecognizer];
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    self.imageView.frame = self.view.bounds;
}

- (void)performPanGesture:(UIPanGestureRecognizer *)panGesture
{
    if (panGesture == self.panGestureRecognizer) {
        CGPoint touchLocation = [panGesture locationInView:self.view];
//        NSLog(@"%f, %f", touchLocation.x, touchLocation.y);

        UIGraphicsBeginImageContextWithOptions(self.view.frame.size, YES, 0.0);

        CGContextRef context = UIGraphicsGetCurrentContext();

        [self.view.layer renderInContext:context];

        CGContextAddEllipseInRect(context, CGRectMake(touchLocation.x - 1, touchLocation.y - 1, 3, 3));

        CGContextDrawPath(context, kCGPathFill);

        UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();

        UIGraphicsEndImageContext();

        self.imageView.image = outputImage;
    }
}

@end

This will draw little circles on the screen wherever a touch is received. You could also modify it to keep track of the last touch point and draw lines between the points so you have something that's continuous. You can also see that touching with a second finger won't start making points in a new spot, because it only handles one touch, and a second touch is simply ignored.

When the touches start, take a touch from the set and keep a reference to it. How you decide which one to take is up to you - hopefully there would be only one, or you could check the location of each touch and choose the 'highest' on the screen.

As the touches moved, check that your stored touch is still valid (contained in touches) and, if it is, use it only (you can ignore all the other touches).


At its most trivial:

self.trackingTouch = [touches anyObject];

but there are other (better) ways of choosing which touch to store.


Not really sure why the docs say You should never retain an UITouch object when handling an event when they also say A UITouch object is persistent throughout a multi-touch sequence.

I haven't seen issues with storing the touch previously, but that doesn't mean it couldn't cause issues in the future (somehow). The alternative, based on A UITouch object is persistent throughout a multi-touch sequence is to store only the pointer to the touch and use it for pointer comparison only (so calling no methods on the UITouch object behind the pointer).

The Documentation for UITouch has changed over the past years, and now it says:

A touch object persists throughout a multi-touch sequence. You may store a reference to a touch while handling a multi-touch sequence, as long as you release that reference when the sequence ends.

So simply keep a reference to the touch while you're tracking the draw gesture, and discard all moved/canceled/ended events for other touches.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top