Define custom touch area in custom UIControl object - ios

I am creating a custom UIControl object as detailed here. It is all working well except for the touch area.
I want to find a way to limit the touch area to only part of the control, in the example above I want it to be restricted to the black circumference only rather than the whole control area.
Any idea?
Cheers

You can override UIView's pointInside:withEvent: to reject unwanted touches.
Here's a method that checks if the touch occurred in a ring around the center of the view:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
UITouch *touch = [[event touchesForView:self] anyObject];
if (touch == nil)
return NO;
CGPoint touchPoint = [touch locationInView:self];
CGRect bounds = self.bounds;
CGPoint center = { CGRectGetMidX(bounds), CGRectGetMidY(bounds) };
CGVector delta = { touchPoint.x - center.x, touchPoint.y - center.y };
CGFloat squareDistance = delta.dx * delta.dx + delta.dy * delta.dy;
CGFloat outerRadius = bounds.size.width * 0.5;
if (squareDistance > outerRadius * outerRadius)
return NO;
CGFloat innerRadius = outerRadius * 0.5;
if (squareDistance < innerRadius * innerRadius)
return NO;
return YES;
}
To detect other hits on more complex shapes you can use a CGPath to describe the shape and test using CGPathContainsPoint. Another way is to use an image of the control and test the pixel's alpha value.
All that depends on how you build your control.

Related

UIButton - move and scale

I have an UIButton that I've creates programmatically. Actually it should'n be UIButton, I just need to have possibility to mark some area above the image.
So the features I need it - move object and resize it. For this i have 2 methods:
- (void) objMove:(id) sender withEvent:(UIEvent *) event
{
UIControl *control = sender;
UITouch *t = [[event allTouches] anyObject];
CGPoint pPrev = [t previousLocationInView:control];
CGPoint p = [t locationInView:control];
CGPoint center = control.center;
center.x += p.x - pPrev.x;
center.y += p.y - pPrev.y;
control.center = center;
}
- (void)objScale:(UIPinchGestureRecognizer *)recognizer
{
UIView *pinchView = recognizer.view;
CGRect bounds = pinchView.bounds;
CGPoint pinchCenter = [recognizer locationInView:pinchView];
pinchCenter.x -= CGRectGetMidX(bounds);
pinchCenter.y -= CGRectGetMidY(bounds);
CGAffineTransform transform = pinchView.transform;
transform = CGAffineTransformTranslate(transform, pinchCenter.x, pinchCenter.y);
CGFloat scale = recognizer.scale;
transform = CGAffineTransformScale(transform, scale, scale);
transform = CGAffineTransformTranslate(transform, -pinchCenter.x, -pinchCenter.y);
pinchView.transform = transform;
recognizer.scale = 1.0;
}
Scale works ok. Moving looks ok until I change the size of object - when i increase object it become moves slower than finger, and vice versa - if object smaller than original it moves faster than finger. why it works like this?
I think you should get startPoint and startCenter in
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// get startPoint and startCenter here
}
- (void) objMove:(id) sender withEvent:(UIEvent *) event
{
UIControl *control = sender;
UITouch *t = [[event allTouches] anyObject];
CGPoint p = [t locationInView:control];
startCenter.x += p.x - startPoint.x;
startCenter.y += p.y - startPoint.y;
control.center = startCenter;
}
Change your code like this, maybe it works.
Your center is current center, p is current point, pPrev is previous point.
current center adds previous point moved size is wrong.
You should get relative distance, not dynamic distance.

iOS: Cropping around a drawn path?

I'm using ACEDrawingView to draw within a view.
How would I detect the width and height of the drawing, so that I can crop around it, something like this:
Update: After #Duncan pointed me in the right direction, I was able to look through the source code and found the following:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// save all the touches in the path
UITouch *touch = [touches anyObject];
previousPoint2 = previousPoint1;
previousPoint1 = [touch previousLocationInView:self];
currentPoint = [touch locationInView:self];
if ([self.currentTool isKindOfClass:[ACEDrawingPenTool class]]) {
CGRect bounds = [(ACEDrawingPenTool*)self.currentTool addPathPreviousPreviousPoint:previousPoint2 withPreviousPoint:previousPoint1 withCurrentPoint:currentPoint];
CGRect drawBox = bounds;
drawBox.origin.x -= self.lineWidth * 2.0;
drawBox.origin.y -= self.lineWidth * 2.0;
drawBox.size.width += self.lineWidth * 4.0;
drawBox.size.height += self.lineWidth * 4.0;
self.drawingBounds = bounds; // I added this property to allow me to extract the bounds and use it in my view controller
[self setNeedsDisplayInRect:drawBox];
}
else if ([self.currentTool isKindOfClass:[ACEDrawingTextTool class]]) {
[self resizeTextViewFrame: currentPoint];
}
else {
[self.currentTool moveFromPoint:previousPoint1 toPoint:currentPoint];
[self setNeedsDisplay];
}
}
However I get this when I test the bounds:
I'm going to keep trying to figure it out, but if anyone could help that would be great!
Update 3: Using CGContextGetPathBoundingBox I was finally able to achieve it.
Every time you get a touchesMoved, record the location of the point you are now drawing. When you are all done, you have all the points. Now look at the largest x value and the smallest x value and the largest y value and the smallest y value in all of those points. That's the bounding box of the drawing.
Another approach (which you've already discovered) is to save the CGPath and then call CGContextGetPathBoundingBox. Basically that does exactly the same thing.
Note that a path has no thickness, whereas your stroke does. You will need to inset the bounding box negatively to allow for this (my screencast doesn't do that).
I'm not familiar with the AceDrawingView class. I can tell you how to do it with iOS frameworks though:
Create your path as a UIBezierPath.
Interrogate the bounds property of the path.

touchesMoved, how do I make my view catch up - and stay under the finger?

I have written some code that restricts the movement of a box (UIView) to a grid.
When moving the box, the movement is locked to the grid and the box starts getting behind your finger if you drag diagonally or really fast.
So what is the best way to write a method that makes the box catch up and get back under the finger - it must move on the same path as your finger - and it must also not move through other boxes, so it needs collision detection - so I just can't do an Animate to new center point.
Any suggestions?
This is current code in use:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
lastLocation = location;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
CGPoint offset = CGPointMake(self.center.x + location.x - lastLocation.x, self.center.y + location.y - lastLocation.y);
CGPoint closestCenter = [self closestCenter:offset];
CGRect rect = CGRectMake(offset.x - (self.size.width / 2), offset.y - (self.size.height / 2), self.size.width, self.size.height);
if (fabsf(closestCenter.x - offset.x) < fabsf(closestCenter.y - offset.y)) {
offset.x = closestCenter.x;
}
else {
offset.y = closestCenter.y;
}
// Do collision detection - removed for clarity
lastLocation = location;
self.center = offset;
}
Don't use a relative offset movement. Instead, use the actual touch location as the desired position and then bound (modify) it based on the grid restrictions. In this way you won't get any lag behind the touch.
From your collision detection I guess there is a path that must be followed and a naive implementation will 'jump' the view to the touch across boundaries. A simple solution to this is to limit jumping to a maximum of half a grid square (so the user must bring the touch back to the view if they drop it).

Draggable UIButton Snap To a Circle Path

Hi I am looking to make my UIButton be able to be draggable only around a circle I created through a draw rect. I may need to make a circle path instead of the circle shape however I'm not quite sure about the approach to take as I am new to this. Here is the code I have and I only want the button to be dragged around that circle or invisible path. Any help or examples would be extremely helpful. Thank You!
Edit: I used part of the answer below below but I am not sure how to get this to work correctly. I want it to be the same size as my circle that I created so I am happy to create just a circle path and put it on top of the circle that I made.
//Button Code
timeSetButton = [UIButton buttonWithType:UIButtonTypeCustom];
[timeSetButton addTarget:self
action:nil
forControlEvents:UIControlEventTouchDown];
[timeSetButton setImage:[UIImage imageNamed:#"testButton.png"] forState: UIControlStateNormal];
timeSetButton.frame = CGRectMake(0,415,50,50);
[self.view addSubview:timeSetButton];
//Button Listener
[timeSetButton addTarget:self action:#selector(wasDragged:withEvent:)
forControlEvents:UIControlEventTouchDragInside];
//Draggable Button Code
// get the touch
UITouch *touch = [[event touchesForView:timeSetButton] anyObject];
// get delta
CGPoint previousLocation = [touch previousLocationInView:timeSetButton];
CGPoint location = [touch locationInView:timeSetButton];
CGFloat delta_x = location.x - previousLocation.x;
CGFloat delta_y = location.y - previousLocation.y;
// move button
timeSetButton.center = CGPointMake(timeSetButton.center.x + delta_x,
timeSetButton.center.y + delta_y);
// enforce constraint on locations, by...
// working out the distance from the centre of the circle
CGPoint vectorFromCentreOfCircle =
CGPointMake(150,150);
CGFloat distanceFromCentreOfCircle = hypotf(vectorFromCentreOfCircle.x, vectorFromCentreOfCircle.y);
// working out what you'd need to multiply that distance by in order
// to get the specified radius
CGFloat correctionMultiplier = 20 / distanceFromCentreOfCircle;
// adjust vector from centre of circle
vectorFromCentreOfCircle.x *= correctionMultiplier;
vectorFromCentreOfCircle.y *= correctionMultiplier;
// move button one more time
timeSetButton.center = CGPointMake(
200 + vectorFromCentreOfCircle.x,
200 + vectorFromCentreOfCircle.y);
Here is the Circle Shape
circleView = [[CircleView alloc] initWithFrame:CGRectMake(55, 100, 260, 260)];
circleView.backgroundColor = [UIColor clearColor];
[self.view addSubview:circleView];
[self.view sendSubviewToBack:circleView];
I actually needed the same and was watching your code. What you actually need to do is fetch the angle of the actual circle and then calculate the needed x and y on that. This is how I have done it:
UIButton *button = (UIButton *) sender;
UITouch *touch = [[event touchesForView:button] anyObject];
//Drawing the circle
CGPoint arcCenter = CGPointMake(385.0f, 700.0f);
CGFloat arcRadius = 140.0f;
// get delta
CGPoint previousLocation = [touch previousLocationInView:button];
CGPoint location = [touch locationInView:button];
CGFloat delta_x = location.x - previousLocation.x;
CGFloat delta_y = location.y - previousLocation.y;
// move button
button.center = CGPointMake(button.center.x + delta_x, button.center.y + delta_y);
CGFloat angle = atan2((button.center.y - arcCenter.y), (button.center.x - arcCenter.x));
button.center = CGPointMake(arcCenter.x + arcRadius * cos(angle), arcCenter.y + arcRadius * sin(angle));
This gives me when I add a drag to a button the actual needed behavior. I used Touch Drag Outside and Inside to avoid the button to stop when you move your finger too much away from the button. I hope it will help you too.
For this sort of thing it's probably sufficient just to add a quick bit of code after you've moved the button to enforce the constraint that it must be on the circle. E.g.
// move button
timeSetButton.center = CGPointMake(button.center.x + delta_x,
button.center.y + delta_y);
// enforce constraint on locations, by...
// working out the distance from the centre of the circle
CGPoint vectorFromCentreOfCircle =
CGPointMake(timeSetButton.center.x - centreOfCircle.x,
timeSetButton.center.x - centreOfCircle.y);
CGFloat distanceFromCentreOfCircle =
hypotf(vectorFromCentreOfCircle.x, vectorFromCentreOfCircle.y);
// working out what you'd need to multiply that distance by in order
// to get the specified radius
CGFloat correctionMultiplier = radiusOfCircle / distanceFromCentreOfCircle;
// adjust vector from centre of circle
vectorFromCentreOfCircle.x *= correctionMultiplier;
vectorFromCentreOfCircle.y *= correctionMultiplier;
// move button one more time
timeSetButton.center = CGPointMake(
centreOfCircle.x + vectorFromCentreOfCircle.x,
centreOfCircle.y + vectorFromCentreOfCircle.y);
You can do this simply by knowing the distance of touchPoint from the center of the view
I'll make the following modification in your code. See if this works
- (void)wasDragged:(UIButton *)button withEvent:(UIEvent *)event
{
// get the touch
UITouch *touch = [[event touchesForView:button] anyObject];
// get delta
CGPoint previousLocation = [touch previousLocationInView:button];
CGPoint location = [touch locationInView:button];
//calculate center of button in superView
CGPoint buttonCenter = CGPointMake(button.frame.origin.x+(button.frame.size.width/2.0f),
button.frame.origin.y+(button.frame.size.height/2.0f));
//calculate the distance of current touchPoint from buttonCenter
CGFloat diffx = location.x - buttonCenter.x;
CGFloat diffy = location.y - buttonCenter.y;
CGFloat distance = sqrtf((diffx*diffx)+(diffy*diffy));
//check if the distance is within the radius of circle.
//assuming that your button is always a square to make
//perfect circle within it.
CGFloat radius = button.frame.size.width/2.0f;
if(radius >= distance)//this makes a circular check.
{
CGFloat delta_x = location.x - previousLocation.x;
CGFloat delta_y = location.y - previousLocation.y;
// move button
timeSetButton.center = CGPointMake(button.center.x + delta_x,
button.center.y + delta_y);
}
}

touchesMoved:withEvent: and UIRotationgestureRecognizer don't work together

I'm trying these two methods to move and rotate an UIView. Both methods work separately but if I rotate and then move the UIView it disappears.
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
CGRect rect = self.aView.frame;
UITouch *touch = [touches anyObject];
CGPoint pPoint = [touch previousLocationInView:self.view];
CGPoint cPoint = [touch locationInView:self.view];
float deltaX = cPoint.x - pPoint.x;
float deltaY = cPoint.y - pPoint.y;
rect.origin.x = rect.origin.x + deltaX;
rect.origin.y = rect.origin.y + deltaY;
self.aView.frame = rect;
}
- (void)rotate:(UIRotationGestureRecognizer *) recognizer {
CGFloat rotation = angle + recognizer.rotation;
NSLog(#"%f", angle * 180 / M_PI);
self.aView.transform = CGAffineTransformMakeRotation (rotation);
if (recognizer.state == UIGestureRecognizerStateEnded)
angle = rotation;
}
Gesture recognisers take priority over touchMoved, so it's hard to use them both with the same view.
Use a UIPanGestureRecognizer instead of touchMoved to handle dragging the UIView. You can then get the UIPanGestureRecognizer and UIRotationGestureRecognizer to cooperate with one another by implementing the
– gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
method, which is defined in the UIGestureRecognizerDelegate protocol.

Resources