- Touches
- Recipe: Adding a Simple Direct Manipulation Interface
- Recipe: Adding Pan Gesture Recognizers
- Recipe: Using Multiple Gesture Recognizers Simultaneously
- Recipe: Constraining Movement
- Recipe: Testing Touches
- Recipe: Testing Against a Bitmap
- Recipe: Drawing Touches Onscreen
- Recipe: Smoothing Drawings
- Recipe: Using Multi-Touch Interaction
- Recipe: Detecting Circles
- Recipe: Creating a Custom Gesture Recognizer
- Recipe: Dragging from a Scroll View
- Recipe: Live Touch Feedback
- Recipe: Adding Menus to Views
- Summary
Recipe: Testing Against a Bitmap
Unfortunately, most views don’t fall into the simple geometries that make the hit test from Recipe 1-5 so straightforward. The flowers shown in Figure 1-1, for example, offer irregular boundaries and varied transparencies. For complicated art, it helps to test touches against a bitmap. Bitmaps provide byte-by-byte information about the contents of an image-based view, allowing you to test whether a touch hits a solid portion of the image or should pass through to any views below.
Recipe 1-6 extracts an image bitmap from a UIImageView. It assumes that the image used provides a pixel-by-pixel representation of the view in question. When you distort that view (normally by resizing a frame or applying a transform), update the math accordingly. CGPoints can be transformed via CGPointApplyAffineTransform() to handle scaling and rotation changes. Keeping the art at a 1:1 proportion to the actual view pixels simplifies lookup and avoids any messy math. You can recover the pixel in question, test its alpha level, and determine whether the touch has hit a solid portion of the view.
This example uses a cutoff of 85. This corresponds to a minimum alpha level of 33% (that is, 85 / 255). This custom pointInside: method considers any pixel with an alpha level below 33% to be transparent. This is arbitrary. Use any level (or other test, for that matter) that works with the demands of your actual GUI.
Recipe 1-6 Testing Touches Against Bitmap Alpha Levels
// Return the offset for the alpha pixel at (x,y) for RGBA // 4-bytes-per-pixel bitmap data static NSUInteger alphaOffset(NSUInteger x, NSUInteger y, NSUInteger w) {return y * w * 4 + x * 4;} // Return the bitmap from a provided image NSData *getBitmapFromImage(UIImage *image) { if (!sourceImage) return nil; // Establish color space CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); if (colorSpace == NULL) { NSLog(@"Error creating RGB color space"); return nil; } // Establish context int width = sourceImage.size.width; int height = sourceImage.size.height; CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, (CGBitmapInfo) kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (context == NULL) { NSLog(@"Error creating context"); return nil; } // Draw source into context bytes CGRect rect = (CGRect){.size = sourceImage.size}; CGContextDrawImage(context, rect, sourceImage.CGImage); // Create NSData from bytes NSData *data = [NSData dataWithBytes:CGBitmapContextGetData(context) length:(width * height * 4)]; CGContextRelease(context); return data; } // Store the bitmap data into an NSData instance variable - (instancetype)initWithImage:(UIImage *)anImage { self = [super initWithImage:anImage]; if (self) { self.userInteractionEnabled = YES; data = getBitmapFromImage(anImage); } return self; } // Does the point hit the view? - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if (!CGRectContainsPoint(self.bounds, point)) return NO; Byte *bytes = (Byte *)data.bytes; uint offset = alphaOffset(point.x, point.y, self.image.size.width); return (bytes[offset] > 85); }