I have a UIImageView that shows a picture the user has just taken with the camera. On that image i need to draw a transparent movable circle thats a set size. So when the user drags their finger over the image the circle moves with it. Then if the user stops moving their finger, the circle stays in the same place.
I have been reading the apple documentation on CAShapeLayer but i'm still not sure of the best way, should i be drawing a UIView?
Any examples would be brilliant. Thanks.
The following code creates three gestures:
A tap gesture drops a circle on the view (just so you can see how the CAShapeLayer is created);
A pan gesture moves the circle (assuming you started moving from within the circle); and
A pinch gesture resizes the circle.
Thus that might look like:
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface ViewController ()
#property (nonatomic, weak) CAShapeLayer *circleLayer;
#property (nonatomic) CGPoint circleCenter;
#property (nonatomic) CGFloat circleRadius;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// create tap gesture
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
// create pan gesture
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePan:)];
[self.view addGestureRecognizer:pan];
// create pinch gesture
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self
action:#selector(handlePinch:)];
[self.view addGestureRecognizer:pinch];
}
// Create a UIBezierPath which is a circle at a certain location of a certain radius.
// This also saves the circle's center and radius to class properties for future reference.
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
{
self.circleCenter = location;
self.circleRadius = radius;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:self.circleCenter
radius:self.circleRadius
startAngle:0.0
endAngle:M_PI * 2.0
clockwise:YES];
return path;
}
// Create a CAShapeLayer for our circle on tap on the screen
- (void)handleTap:(UITapGestureRecognizer *)gesture
{
CGPoint location = [gesture locationInView:gesture.view];
// if there was a previous circle, get rid of it
[self.circleLayer removeFromSuperlayer];
// create new CAShapeLayer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self makeCircleAtLocation:location radius:50.0] CGPath];
shapeLayer.strokeColor = [[UIColor redColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 3.0;
// Add CAShapeLayer to our view
[gesture.view.layer addSublayer:shapeLayer];
// Save this shape layer in a class property for future reference,
// namely so we can remove it later if we tap elsewhere on the screen.
self.circleLayer = shapeLayer;
}
// Let's move the CAShapeLayer on a pan gesture (assuming we started
// pan inside the circle).
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static CGPoint oldCenter;
if (gesture.state == UIGestureRecognizerStateBegan)
{
// If we're starting a pan, make sure we're inside the circle.
// So, calculate the distance between the circle's center and
// the gesture start location and we'll compare that to the
// radius of the circle.
CGPoint location = [gesture locationInView:gesture.view];
CGPoint translation = [gesture translationInView:gesture.view];
location.x -= translation.x;
location.y -= translation.y;
CGFloat x = location.x - self.circleCenter.x;
CGFloat y = location.y - self.circleCenter.y;
CGFloat distance = sqrtf(x*x + y*y);
// If we're outside the circle, cancel the gesture.
// If we're inside it, keep track of where the circle was.
if (distance > self.circleRadius)
gesture.state = UIGestureRecognizerStateCancelled;
else
oldCenter = self.circleCenter;
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
// Let's calculate the new center of the circle by adding the
// the translationInView to the old circle center.
CGPoint translation = [gesture translationInView:gesture.view];
CGPoint newCenter = CGPointMake(oldCenter.x + translation.x, oldCenter.y + translation.y);
// Update the path for our CAShapeLayer
self.circleLayer.path = [[self makeCircleAtLocation:newCenter radius:self.circleRadius] CGPath];
}
}
// Let's resize circle in the CAShapeLayer on a pinch gesture (assuming we have
// a circle layer).
- (void)handlePinch:(UIPinchGestureRecognizer *)gesture
{
static CGFloat oldCircleRadius;
if (gesture.state == UIGestureRecognizerStateBegan)
{
if (self.circleLayer)
oldCircleRadius = self.circleRadius;
else
gesture.state = UIGestureRecognizerStateCancelled;
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
CGFloat newCircleRadius = oldCircleRadius * gesture.scale;
self.circleLayer.path = [[self makeCircleAtLocation:self.circleCenter radius:newCircleRadius] CGPath];
}
}
#end
Related
I developed an application which allows to the user to draw his finger signature in a canvas.
This feature is implemented using UIPanGestureRecognizer with a specific target action to draw a line in a UIView, but when the “Voice Over” is active the gesture recognizer action is not triggered anymore.
Gesture initialize code
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1;
[self addGestureRecognizer:pan];
Gesture action code
- (void)pan:(UIPanGestureRecognizer *)pan {
CGPoint currentPoint = [pan locationInView:self];
CGPoint midPoint = midpoint(previousPoint, currentPoint);
if (pan.state == UIGestureRecognizerStateBegan)
{
[path moveToPoint:currentPoint];
}
else if (pan.state == UIGestureRecognizerStateChanged)
{
[path addQuadCurveToPoint:midPoint controlPoint:previousPoint];
}
previousPoint = currentPoint;
[self setNeedsDisplay];
}
Is there any way to draw a line in a view using gesture with “Voice Over” active?
Thanks and regards!
I resolved my problem setting both isAccessibilityElement and accessibilityTraits properties for UIView canvas:
canvasView.isAccessibilityElement = YES;
canvasView.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction;
I am masking UIView(240 * 240) in tringular shape using UIBezierPath as follows:
path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 240}];
[path addLineToPoint:(CGPoint){120,0}];
[path addLineToPoint:(CGPoint){240,240}];
[path addLineToPoint:(CGPoint){0,240}];
[path closePath];
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewShape.bounds;
mask.path = path.CGPath;
self.viewShape.layer.mask = mask;
In above Image triangular area is mask. Now I have image of "Coca-Cola" which is to be moved only in triangular mask. So, for that I have apply UIPanGestureRecognizer to UIIMageView and restrict its frame in following way.
- (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint touchLocation = [gestureRecognizer locationInView:self.viewShape];
CGRect boundsRect;
BOOL isInside = [path containsPoint:CGPointMake(self.innerView.center.x, self.innerView.center.y)];
NSLog(#"value:%d",isInside);
if (isInside) {
NSLog(#"inside");
self.innerView.center = touchLocation;
}else{
NSLog(#"outside");
}
}
My above if condition executes successfully but when control goes into else condition I am not able to drag back my ImageView inside mask frame.
So, My question is when else block(outside) called I should be able to drag imageView again inside the Mask's frame.
How can I achieve this?
saving reference to last center of imageview is a way to achieve this.
in your customView;
CGPoint lastValidCenter; //initially it is the center of imageview;
and in the code
NSLog(#"value:%d",isInside);
if (isInside) {
NSLog(#"inside");
self.innerView.center = touchLocation;
lastValidCenter = self.innerView.center;
}else{
NSLog(#"outside");
self.innerView.center = lastValidCenter;
}
There is a problem in your calculation. What you need to do is to check the top-left and top-right corner of the UIImageView for contains point with UIBezirePath instance.
It is so because when the image will move both of these corners are the ones which will try to go out first. So just put a check and you'll get your desired output.
BOOL isInside = [path containsPoint:CGPointMake(CGRectGetMinX(self.innerView.bounds), CGRectGetMinY(self.innerView.bounds))];
isInside = isInside || [path containsPoint:CGPointMake(CGRectGetMaxX(self.innerView.bounds), CGRectGetMinY(self.innerView.bounds))];
if (isInside) {
//Move your UIImageView
}
else {
//Don't move
}
I modified some portion of Meth's code. The code is as follows:
self.innerView.center = touchLocation;
BOOL isInside = [path containsPoint:CGPointMake(self.innerView.center.x, self.innerView.center.y)];
if (isInside) {
NSLog(#"inside");
self.innerView.center = touchLocation;
lastValidCenter = self.innerView.center;
}else{
NSLog(#"outside");
self.innerView.center = lastValidCenter;
}
Using some code I came across that draws a circle shape over an image. Pan and pinch gestures are used on the circle shape being drawn. The movement of the shape crosses over the dimensions of the UIImageView, onto the rest of the UI that is not displaying the image. I've tried to incorporate methods used in other examples where pan and pinch restrictions were needed in order to restrict the circle shape's movement to the bounds of the UIImageView, but could not get any of them to work. I understand that I would need to control the center and radius values of the pan and pinch so that they don't cross outside the UIImageView's border, but haven't the slightest clue as to how to execute this. Attached is the code I'm using. Thanks for your help!
-(void)setUpCropTool:(id)sender{
// create layer mask for the image
CAShapeLayer *maskLayer = [CAShapeLayer layer];
self.imageToBeCropped.layer.mask = maskLayer;
self.maskLayer = maskLayer;
// create shape layer for circle we'll draw on top of image (the boundary of the circle)
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.lineWidth = 50;
circleLayer.fillColor = [[UIColor clearColor] CGColor];
circleLayer.strokeColor = [[UIColor redColor] CGColor];
[self.imageToBeCropped.layer addSublayer:circleLayer];
self.circleLayer = circleLayer;
// create circle path
[self updateCirclePathAtLocation:CGPointMake(self.imageToBeCropped.bounds.size.width / 2.0, self.imageToBeCropped.bounds.size.height / 2.0) radius:self.imageToBeCropped.bounds.size.width/2.5];
// create pan gesture
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
pan.delegate = self;
[self.imageToBeCropped addGestureRecognizer:pan];
self.imageToBeCropped.userInteractionEnabled = YES;
self.pan = pan;
// create pan gesture
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinch:)];
pinch.delegate = self;
[self.imageToBeCropped addGestureRecognizer:pinch];
self.pinch = pinch;
}
- (void)updateCirclePathAtLocation:(CGPoint)location radius:(CGFloat)radius
{
self.circleCenter = location;
self.circleRadius = radius;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:self.circleCenter
radius:self.circleRadius
startAngle:0.0
endAngle:M_PI * 2.0
clockwise:YES];
self.maskLayer.path = [path CGPath];
self.circleLayer.path = [path CGPath];
}
#pragma mark - Gesture recognizers
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static CGPoint oldCenter;
CGPoint tranlation = [gesture translationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan)
{
oldCenter = self.circleCenter;
}
CGPoint newCenter = CGPointMake(oldCenter.x + tranlation.x, oldCenter.y + tranlation.y);
[self updateCirclePathAtLocation:newCenter radius:self.circleRadius];
}
- (void)handlePinch:(UIPinchGestureRecognizer *)gesture
{
static CGFloat oldRadius;
CGFloat scale = [gesture scale];
if (gesture.state == UIGestureRecognizerStateBegan)
{
oldRadius = self.circleRadius;
}
CGFloat newRadius = oldRadius * scale;
[self updateCirclePathAtLocation:self.circleCenter radius:newRadius];
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ((gestureRecognizer == self.pan && otherGestureRecognizer == self.pinch) ||
(gestureRecognizer == self.pinch && otherGestureRecognizer == self.pan))
{
return YES;
}
return NO;
}
Try adding some additional constraints to the pan handling code. Maybe something like:
if (oldCenter.x + tranlation.x >= superView.frame.size.width - self.circleRadius
|| oldCenter.x + tranlation.x <= self.circleRadius)
{
newCenter.x = oldCenter.x;
}
else
{
newCenter.x = oldCenter.x + tranlation.x;
}
This is a pretty simple example, hope it helps.
I have ImageView that I crop to circle with this:
self.contentMode = UIViewContentModeScaleAspectFill;
self.layer.cornerRadius = self.bounds.size.height / 2.0;
self.layer.masksToBounds = YES;
Then I added gesture recognizer to it, but it fires in cropped area.
How can I avoid firing it in cropped area?
A more general and flexible way to mask your image is with a CAShapeLayer. You can create any shape, including a circle, to use as a mask. By using this approach to crop your image view instead of using cornerRadius, you can check if the touch point is within the layer's path (a UIBezierPath). In the UIImageView subclass add the following code to create the mask, and create a property, shape, in the .h file.
self.shape = [UIBezierPath bezierPathWithOvalInRect:self.bounds];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.shape.CGPath;
self.layer.mask = shapeLayer;
In the controller, add the tap gesture recognizer, and use this code in its action method,
-(void)handleTap:(UITapGestureRecognizer *) tapper {
CGPoint touchPoint = [tapper locationInView:tapper.view];
if ([self.imageView.shape containsPoint:touchPoint]) {
NSLog(#"touched");
// do what you want with the touch here
}
}
Set your class conforms to UIGestureRecognizerDelegate
Set your gesture delegate to self
Then use this delegate to decide that if you want it fire or not
-(BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
Example Code
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
CGPoint touchPoint = [touch locationInView:self.imageview];
if (CGRectContainsPoint(self.imageview.bounds, touchPoint)) {
CGFloat centerX = CGRectGetMidX(self.imageview.bounds);
CGFloat centerY = CGRectGetMidY(self.imageview.bounds);
CGFloat radius2 = pow((touchPoint.x -centerX),2)+ pow((touchPoint.y - centerY), 2);
if (radius2 < pow(CGRectGetWidth(self.imageview.frame)/2, 2)) {
return YES;
}
}
return NO;}
My goal is to resize UIView with a handle - its subview. I got my project working perfectly to accomplish that: the handle has a PanGestureRecognizer and its handler method resizes the view (parent) using the following method:
-(IBAction)handleResizeGesture:(UIPanGestureRecognizer *)recognizer {
CGPoint touchLocation = [recognizer locationInView:container.superview];
CGPoint center = container.center;
switch (recognizer.state) {
case UIGestureRecognizerStateBegan: {
deltaAngle = atan2f(touchLocation.y - center.y, touchLocation.x - center.x) -
CGAffineTransformGetAngle(container.transform);
initialBounds = container.bounds;
initialDistance = CGPointGetDistance(center, touchLocation);
break;
}
case UIGestureRecognizerStateChanged: {
CGFloat scale = CGPointGetDistance(center, touchLocation)/initialDistance;
CGFloat minimumScale = self.minimumSize/MIN(initialBounds.size.width,
initialBounds.size.height);
scale = MAX(scale, minimumScale);
CGRect scaledBounds = CGRectScale(initialBounds, scale, scale);
container.bounds = scaledBounds;
[container setNeedsDisplay];
break;
}
case UIGestureRecognizerStateEnded:
break;
default:
break;
}
}
Please note that I use center and bounds properties because frame is NOT reliable when transform is applied to my view.
However, my requirement is really to resize the view in ANY direction - not only proportionally as the code does. The problem is that I am not finding the correct methods or approaches how this handle may resize its superview's bounds (width or height) so it always sticks to the corner while finger is dragging it around.
Here is my project if it is easier to see what I mean.
Updated solution as suggested by an answer below works very well but once transform is applied (e.g. in viewDidLoad I have container.transform = CGAffineTransformMakeRotation(90);) it does not:
case UIGestureRecognizerStateBegan: {
initialBounds = container.bounds;
initialDistance = CGPointGetDistance(center, touchLocation);
initialDistanceX = CGPointGetDistanceX(center, touchLocation);
initialDistanceY = CGPointGetDistanceY(center, touchLocation);
break;
}
case UIGestureRecognizerStateChanged: {
CGFloat scaleX = abs(center.x-touchLocation.x)/initialDistanceX;
CGFloat scaleY = abs(center.y-touchLocation.y)/initialDistanceY;
CGFloat minimumScale = self.minimumSize/MIN(initialBounds.size.width, initialBounds.size.height);
scaleX = MAX(scaleX, minimumScale);
scaleY = MAX(scaleY, minimumScale);
CGRect scaledBounds = CGRectScale(initialBounds, scaleX, scaleY);
container.bounds = scaledBounds;
[container setNeedsDisplay];
break;
}
where
CG_INLINE CGFloat CGPointGetDistanceX(CGPoint point1, CGPoint point2) {
return (point2.x - point1.x);
}
CG_INLINE CGFloat CGPointGetDistanceY(CGPoint point1, CGPoint point2) {
return (point2.y - point1.y);
}
You are setting the same scale parameter in your call to CGRectScale(initialBounds, scale, scale); try this:
case UIGestureRecognizerStateChanged: {
CGFloat scaleX = abs(center.x-touchLocation.x)/initialDistance;
CGFloat scaleY = abs(center.y-touchLocation.y)/initialDistance;
CGFloat minimumScale = self.minimumSize/MIN(initialBounds.size.width, initialBounds.size.height);
scaleX = MAX(scaleX, minimumScale);
scaleY = MAX(scaleY, minimumScale);
CGRect scaledBounds = CGRectScale(initialBounds, scaleX, scaleY);
container.bounds = scaledBounds;
[container setNeedsDisplay];
break;
You may also consider to store initialDistanceX and initialDistanceY.
Use UIPinchGestureRecognizer & UIPanGestureRecognizer.
Try this code
//--Create and configure the pinch gesture
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(pinchGestureDetected:)];
[pinchGestureRecognizer setDelegate:self];
[container.superview addGestureRecognizer:pinchGestureRecognizer];
//--Create and configure the pan gesture
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panGestureDetected:)];
[panGestureRecognizer setDelegate:self];
[container.superview addGestureRecognizer:panGestureRecognizer];
For UIPinchGestureRecognizer:
- (void)pinchGestureDetected:(UIPinchGestureRecognizer *)recognizer
{
UIGestureRecognizerState state = [recognizer state];
if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged)
{
CGFloat scale = [recognizer scale];
[recognizer.view setTransform:CGAffineTransformScale(recognizer.view.transform, scale, scale)];
[recognizer setScale:1.0];
panGesture = YES;
}
}
For UIPanGestureRecognizer :
- (void)panGestureDetected:(UIPanGestureRecognizer *)recognizer
{
UIGestureRecognizerState state = [recognizer state];
if (panGesture==YES)
{
if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [recognizer translationInView:recognizer.view];
[recognizer.view setTransform:CGAffineTransformTranslate(recognizer.view.transform, translation.x, translation.y)];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}}
}
I'd not control it with a subview. It gets messy when you apply transform to the outer view.
Simply add two view to the ViewController, and hook them up with some code.
I've altered the project, find test-001 at GitHub.
ViewController code can be empty for this.
The view you want to transform TransformableView needs a scale property, and a method to apply it (if you want to add other transformations as well).
#interface TransformableView : UIView
#property (nonatomic) CGSize scale;
-(void)applyTransforms;
#end
#implementation TransformableView
-(void)applyTransforms
{
// Do whatever additional transform you want (e.g. concat additional rotations / mirroring to the transform).
self.transform = CGAffineTransformMakeScale(self.scale.width, self.scale.height);
}
#end
And the DraggableCorner can manage the rest (with some basic math).
#class TransformableView;
#interface DraggableCorner : UIView
#property (nonatomic, weak) IBOutlet TransformableView *targetView; // Hook up in IB.
-(IBAction)panGestureRecognized:(UIPanGestureRecognizer*) recognizer;
#end
CG_INLINE CGPoint offsetOfPoints(CGPoint point1, CGPoint point2)
{ return (CGPoint){point1.x - point2.x, point1.y -point2.y}; }
CG_INLINE CGPoint addPoints(CGPoint point1, CGPoint point2)
{ return (CGPoint){point1.x + point2.x, point1.y + point2.y}; }
#interface DraggableCorner ()
#property (nonatomic) CGPoint touchOffset;
#end
#implementation DraggableCorner
-(IBAction)panGestureRecognized:(UIPanGestureRecognizer*) recognizer
{
// Location in superview.
CGPoint touchLocation = [recognizer locationInView:self.superview];
// Began.
if (recognizer.state == UIGestureRecognizerStateBegan)
{
// Finger distance from handler.
self.touchOffset = offsetOfPoints(self.center, touchLocation);
}
// Moved.
if (recognizer.state == UIGestureRecognizerStateChanged)
{
// Drag.
self.center = addPoints(touchLocation, self.touchOffset);
// Desired size.
CGPoint distanceFromTargetCenter = offsetOfPoints(self.center, self.targetView.center);
CGSize desiredTargetSize = (CGSize){distanceFromTargetCenter.x * 2.0, distanceFromTargetCenter.y * 2.0};
// -----
// You can put limitations here, simply clamp `desiredTargetSize` to some value.
// -----
// Scale needed for desired size.
CGSize targetSize = self.targetView.bounds.size;
CGSize targetRatio = (CGSize){desiredTargetSize.width / targetSize.width, desiredTargetSize.height / targetSize.height};
// Apply.
self.targetView.scale = targetRatio;
[self.targetView applyTransforms];
}
}
Set the classes in IB, and hook up the IBAction and the IBOutlet of DraggableView.