Using a UIPanGestureRecognizer in my view controller, I'm trying to draw a view (ArrowView) at an angle based upon the touch location. I'm trying to use CGAffineTransformRotate to rotate the view based up the angle between the first touch and the current touch, but this isn't working unless the view has already been drawn by at lease 20 or more pixels. Also, when drawing, the view doesn't always line up under my finger. Is this the correct approach for this situation? If not, does anyone recommend a better way of accomplishing this? If so, what am I doing wrong?
ViewController.m
#implementation ViewController {
ArrowView *_selectedArrowView;
UIColor *_selectedColor;
CGFloat _selectedWeight;
CGPoint _startPoint;
}
- (void)viewDidLoad {
[super viewDidLoad];
_selectedColor = [UIColor yellowColor];
_selectedWeight = 3;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panHandler:)];
[self.view addGestureRecognizer:pan];
}
- (void) panHandler: (UIPanGestureRecognizer *) sender {
if (sender.state == UIGestureRecognizerStateBegan) {
//Instantiate the arrow
CGPoint touchPoint = [sender locationInView:sender.view];
_startPoint = touchPoint;
_selectedArrowView = [[ArrowView alloc] initWithFrame:CGRectMake(touchPoint.x, touchPoint.y, 0, 25) withColor:_selectedColor withWeight:_selectedWeight];
_selectedArrowView.delegate = self;
[self.view addSubview:_selectedArrowView];
[self.view bringSubviewToFront:_selectedArrowView];
} else if (sender.state == UIGestureRecognizerStateChanged) {
//"Draw" the arrow based upon finger postion
CGPoint touchPoint = [sender locationInView:sender.view];
[_selectedArrowView drawArrow:_startPoint to:touchPoint];
}
}
#end
ArrowView.m
- (void) drawArrow: (CGPoint) startPoint to: (CGPoint) endPoint {
startPoint = [self convertPoint:startPoint fromView:self.superview];
endPoint = [self convertPoint:endPoint fromView:self.superview];
if (_initialAngle == -1000 /*Initially set to an arbitrary value so I know when the draw began*/) {
_initialAngle = atan2(startPoint.y - endPoint.y, startPoint.x - endPoint.x);
[self setPosition:0];
} else {
CGFloat ang = atan2(startPoint.y - endPoint.y, startPoint.x - endPoint.x);
ang -= _initialAngle;
self.transform = CGAffineTransformRotate(self.transform, ang);
CGFloat diff = (endPoint.x - self.bounds.size.width);
NSLog(#"\n\n diff: %f \n\n", diff);
self.bounds = CGRectMake(0, 0, self.bounds.size.width + diff, self.bounds.size.height);
_endPoint = CGPointMake(self.bounds.size.width, self.bounds.size.height);
[self setNeedsDisplay];
}
}
- (void) setPosition: (CGFloat) anchorPointX {
CGPoint layerLoc;
if (anchorPointX == 0) {
layerLoc = CGPointMake(self.layer.bounds.origin.x, self.layer.bounds.origin.y + (self.layer.bounds.size.height / 2));
} else {
layerLoc = CGPointMake(self.layer.bounds.origin.x + self.layer.bounds.size.width, self.layer.bounds.origin.y + (self.layer.bounds.size.height / 2));
}
CGPoint superLoc = [self convertPoint:layerLoc toView:self.superview];
self.layer.anchorPoint = CGPointMake(anchorPointX, 0.5);
self.layer.position = superLoc;
}
- (CGFloat) pToA: (CGPoint) touchPoint {
CGPoint start;
if (_dotButtonIndex == kDotButtonFirst) {
start = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds));
} else {
start = CGPointMake(CGRectGetMinX(self.bounds), CGRectGetMinY(self.bounds));
}
return atan2(start.y - touchPoint.y, start.x - touchPoint.x);
}
Link to project on GitHub: Project Link
Figured it out.
I had to make it have an initial width so the angle would work.
_initialAngle = atan2(startPoint.y - endPoint.y, startPoint.x - (endPoint.x + self.frame.size.width));
Related
I've searched for some time (days) for a solution, but none really do what I need. (iOS, Objective C, BTW).
I have a UIImageView that I resize with a UIPanGestureRecognizer. The typical pan works fine. It seems like I am so close.
But I want to resize the ImageView by dragging a corner of the image and only resizing dimensions relevant to the selected corner. It works great if I only do my "handleResize" UIPanGesture method. But if I pinch or rotate the image, the bounds or frame get messed up. I think I need some sort of CGAffineTransform but I have not been able to get it to work.
I need help to point me in the right direction. I've been working with CGAffineTransforms but I may be on the wrong track.
In my ViewController.h I have a float, touchRadius, set to 25:
float touchRadius = 25;
I have a UIPanGestureRecognizer in my ViewController.m:
- (IBAction)handleResize:(UIPanGestureRecognizer *)recognizer {
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:self.view];
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged) {
CGRect frame = recognizer.view.frame;
CGRect topLeft = CGRectMake(frame.origin.x,
frame.origin.y,
touchRadius, touchRadius);
CGRect bottomLeft = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
CGRect topRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y,
touchRadius, touchRadius);
CGRect bottomRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
Boolean useNewFrame = YES;
CGRect newFrame = frame;
if (CGRectContainsPoint(topLeft, touch)) {
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
newFrame.size.width -= translation.x;
newFrame.size.height -= translation.y;
recognizer.view.frame = newFrame;
} else if (CGRectContainsPoint(topRight, touch)) {
newFrame.origin.y += translation.y;
newFrame.size.width += translation.x;
newFrame.size.height -= translation.y;
recognizer.view.frame = newFrame;
} else if (CGRectContainsPoint(bottomLeft, touch)) {
newFrame.origin.x += translation.x;
newFrame.size.width -= translation.x;
newFrame.size.height += translation.y;
recognizer.view.frame = newFrame;
} else if (CGRectContainsPoint(bottomRight, touch)) {
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
recognizer.view.frame = newFrame;
} else {
useNewFrame = NO;
}
if (useNewFrame) {
// make sure it doesn't go too small to touch
if (newFrame.size.width < touchRadius)
newFrame.size.width = touchRadius;
if (newFrame.size.height < touchRadius)
newFrame.size.height = touchRadius;
recognizer.view.frame = newFrame;
// I THINK I NEED A TRANSFORM HERE
} else {
// use the fallback translate
[recognizer.view setTransform:CGAffineTransformTranslate(recognizer.view.transform, translation.x, translation.y)];
}
}
[recognizer setTranslation:CGPointZero inView:self.view];
}
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged)
{
// make sure it stays visible
float scale = recognizer.scale;
if (recognizer.view.frame.size.width * scale > touchRadius * 2 ||
recognizer.view.frame.size.height * scale > touchRadius * 2) {
[recognizer.view setTransform:CGAffineTransformScale(recognizer.view.transform, scale, scale)];
recognizer.scale = 1;
}
}
}
- (void)handleRotate:(UIRotationGestureRecognizer *)recognizer {
UIGestureRecognizerState state = [recognizer state];
if (state == UIGestureRecognizerStateBegan || state == UIGestureRecognizerStateChanged)
{
CGFloat rotation = [recognizer rotation];
[recognizer.view setTransform:CGAffineTransformRotate(recognizer.view.transform, rotation)];
}
[recognizer setRotation:0];
}
Do I need a transform after I modify the view frame? Or should I pursue another path?
I'm trying to do this natively without using libs from others so I understand it. This simplistic example will be part of a larger project.
After some research I figured out how to modify the transform for a corner/side dragging pan, to resize an image. Here are the key elements to make it work:
Save the CGAffineTransform in each gesture (pan, pinch, rotate) if
the recognizer state = UIGestureRecognizerStateBegan
Apply new CGAAffineTransforms on the saved initial transform in each gesture (pan, pinch, rotate)
Save the corner/side detected in the UIGestureRecognizerStateBegan state
Clear the corner/side detected in the UIGestureRecognizerStateEnded state
For pan gesture, adjust the translation x/y values based on corner/side detected
Make sure touch radius is large enough to be useful (24 was too small, 48 works well)
The transform worked like this:
// pan the image
recognizer.view.transform = CGAffineTransformTranslate(initialTransform, tx, ty);
if (scaleIt) {
// the origin or size changed
recognizer.view.frame = newFrame;
}
The tx and ty values were the defaults returned from the recognizer if the pan was from the center of the image. But if the user touch was near a corner or side of the view frame, the tx/ty and the frame origin are adjusted to resize the view making it appear as if that corner or side were being dragged to resize the view.
For example:
CGRect newFrame = recognizer.view.frame;
if (currentDragType == DRAG_TOPLEFT) {
tx = -translation.x;
ty = -translation.y;
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
newFrame.size.width -= translation.x;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_TOPRIGHT) {
tx = translation.x;
ty = -translation.y;
newFrame.origin.y += translation.y;
newFrame.size.width += translation.x;
newFrame.size.height -= translation.y;
}
This makes the top left corner or top right corner move in or out according to how far the touch moved. Unlike a center pan, where the whole view moves along with the touch, the opposite corner (or side) remains fixed.
There were 2 problems I did not resolve:
if the image is rotated significantly, the corner/side detection (pan) does not work because I did not check for the touch in the rotated coordinate system (but the center pan still works fine)
once the image is rotated, pinch gestures work erratically and can resize an image to zero, making it invisible
I created a simple demo and uploaded it to GitHub: https://github.com/ByteSlinger/ImageGestureDemo
Yes, I know that link could go away someday, so here's the code:
ViewController.h
//
// ViewController.h
// ImageGestureDemo
//
// Created by ByteSlinger on 6/21/18.
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import <UIKit/UIKit.h>
NSString *APP_TITLE = #"Image Gesture Demo";
NSString *INTRO_ALERT = #"\nDrag, Pinch and Rotate the Image!"
"\n\nYou can also Drag, Pinch and Rotate the background image."
"\n\nDouble tap an image to reset it";
float touchRadius = 48; // max distance from corners to touch point
typedef NS_ENUM(NSInteger, DragType) {
DRAG_OFF,
DRAG_ON,
DRAG_CENTER,
DRAG_TOP,
DRAG_BOTTOM,
DRAG_LEFT,
DRAG_RIGHT,
DRAG_TOPLEFT,
DRAG_TOPRIGHT,
DRAG_BOTTOMLEFT,
DRAG_BOTTOMRIGHT
};
#interface ViewController : UIViewController <UIGestureRecognizerDelegate>
//callback to process gesture events
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer;
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer;
- (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
#end
ViewController.m
//
// ViewController.m
// ImageGestureDemo
//
// This is a DEMO. It shows how to pan, pinch, rotate and drag/resize a UIImageView.
//
// There is a background image and a foreground image. Both images can be
// panned, pinched and rotated, but only the foreground image can be resized
// by dragging one of it's corners or it's sides.
//
// NOTE: Sure, much of this code could have been put into a subclass of UIView
// or UIImageView. But for simplicity and reference sake, all code and
// methods are in one place, this ViewController subclass. There is no
// error checking at all. App tested on an iPhone 6+ and an iPad gen3.
//
// Features:
// - allows an image to be resized with pan gesture by dragging corners and sides
// - background image can be modified (pan, pinch, rotate)
// - foreground image can be modified (pan, pinch, rotate, drag/resize)
// - all image manipulation done within gestures linked from storyboard
// - all finger touches on screen show with yellow circles
// - when dragging, the touch circles turn to red (so you know when gestures start)
// - double tap on foreground image resets it to original size and rotation
// - double tap on background resets it and also resets the foreground image
// - screen and image touch and size info displayed on screen
// - uses CGAffineTransform objects for image manipulation
// - uses UIGestureRecognizerStateBegan in gestures to save transforms (the secret sauce...)
//
// Known Issues:
// - when the image is rotated, determining if a touch is on a corner or side
// does not work for large rotations. Need to check touch points against
// non rotated view frame and adjust accordingly.
// - after rotations, pinch and resize can shrink image to invisibility despite
// code attempts to prevent it.
//
// Created by ByteSlinger on 6/21/18.
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
#interface ViewController ()
#property (strong, nonatomic) IBOutlet UIImageView *backgroundImageView;
#property (strong, nonatomic) IBOutlet UIImageView *foregroundImageView;
#property (strong, nonatomic) IBOutlet UILabel *screenInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *touchInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *imageInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *backgroundInfoLabel;
#property (strong, nonatomic) IBOutlet UILabel *changeInfoLabel;
#property (strong, nonatomic) IBOutlet UITapGestureRecognizer *backgroundTapGesture;
#property (strong, nonatomic) IBOutlet UITapGestureRecognizer *foregroundTapGesture;
#end
#implementation ViewController
CGRect originalImageFrame;
CGRect originalBackgroundFrame;
CGAffineTransform originalImageTransform;
CGAffineTransform originalBackgroundTransform;
NSMutableArray* touchCircles = nil;
DragType currentDragType = DRAG_OFF;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// set this to whatever your desired touch radius is
touchRadius = 48;
// In Storyboard this must have set to 1, then this seems to work ok
// when setting the double tap here
_foregroundTapGesture.numberOfTapsRequired = 2;
_backgroundTapGesture.numberOfTapsRequired = 2;
[self centerImageView:_foregroundImageView];
originalImageFrame = _foregroundImageView.frame;
originalBackgroundFrame = _backgroundImageView.frame;
originalImageTransform = _foregroundImageView.transform;
originalBackgroundTransform = _backgroundImageView.transform;
_backgroundImageView.contentMode = UIViewContentModeCenter;
_foregroundImageView.contentMode = UIViewContentModeScaleToFill; // allow stretch
[_backgroundImageView setUserInteractionEnabled:YES];
[_backgroundImageView setMultipleTouchEnabled:YES];
[_foregroundImageView setUserInteractionEnabled:YES];
[_foregroundImageView setMultipleTouchEnabled:YES];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:[UIDevice currentDevice]];
[_touchInfoLabel setText:nil];
[_changeInfoLabel setText:nil];
[_imageInfoLabel setText:nil];
[_backgroundInfoLabel setText:nil];
touchCircles = [[NSMutableArray alloc] init];
}
- (void)viewDidAppear:(BOOL)animated {
[self alert:APP_TITLE :INTRO_ALERT];
}
- (void) orientationChanged:(NSNotification *)note
{
UIDevice * device = note.object;
switch(device.orientation)
{
case UIDeviceOrientationPortrait:
/* start special animation */
break;
case UIDeviceOrientationPortraitUpsideDown:
/* start special animation */
break;
default:
break;
};
[_screenInfoLabel setText:[NSString stringWithFormat:#"Screen: %.0f/%.0f",
self.view.frame.size.width,self.view.frame.size.height]];
}
//
// Update the info labels from the passed objects
//
- (void) updateInfo:(UIView *)imageView touch:(CGPoint)touch change:(CGPoint)change {
NSString *label;
UILabel *infoLabel;
if (imageView == _foregroundImageView) {
label = #"Image: %0.f/%0.f, %0.f/%0.f";
infoLabel = _imageInfoLabel;
} else {
label = #"Background: %0.f/%0.f, %0.f/%0.f";
infoLabel = _backgroundInfoLabel;
}
[infoLabel setText:[NSString stringWithFormat:label,
imageView.layer.frame.origin.x,
imageView.layer.frame.origin.y,
imageView.layer.frame.size.width,
imageView.layer.frame.size.height]];
[_touchInfoLabel setText:[NSString stringWithFormat:#"Touch: %0.f/%.0f",
touch.x,touch.y]];
[_changeInfoLabel setText:[NSString stringWithFormat:#"Change: %0.f/%.0f",
change.x,change.y]];
}
//
// Center the passed image frame within it's bounds
//
- (void)centerImageView:(UIImageView *)imageView {
CGSize boundsSize = self.view.bounds.size;
CGRect frameToCenter = imageView.frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
else
frameToCenter.origin.x = 0;
// center vertically
if (frameToCenter.size.height < boundsSize.height)
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
else
frameToCenter.origin.y = 0;
imageView.frame = frameToCenter;
}
//
// Remove all touch circles
//
- (void)removeTouchCircles {
[touchCircles makeObjectsPerformSelector: #selector(removeFromSuperview)];
[touchCircles removeAllObjects];
}
//
// Draw a circle around the passed point where the user has touched the screen
//
- (void)drawTouchCircle:(UIView *)view fromCenter:(CGPoint)point ofRadius:(float)radius {
CGRect frame = CGRectMake(point.x - view.frame.origin.x - radius,
point.y - view.frame.origin.y - radius,
radius * 2, radius * 2);
UIView *circle = [[UIView alloc] initWithFrame:frame];
circle.alpha = 0.5;
circle.layer.cornerRadius = radius;
circle.backgroundColor = currentDragType == DRAG_OFF ? [UIColor yellowColor] : [UIColor redColor];
[circle.layer setBorderWidth:1.0];
[circle.layer setBorderColor:[[UIColor blackColor]CGColor]];
[view addSubview:circle];
[touchCircles addObject:circle];
}
//
// Draw a touch circle for the passed user touch
//
- (void)handleTouchEvent:(UIView *) view
atPoint:(CGPoint) point
forState:(UIGestureRecognizerState) state
clear:(Boolean) clear {
//NSLog(#"handleTouchEvent");
if (clear) {
[self removeTouchCircles];
}
if (state == UIGestureRecognizerStateEnded) {
[self removeTouchCircles];
} else {
[self drawTouchCircle:self.view fromCenter:point ofRadius:touchRadius];
}
[_touchInfoLabel setText:[NSString stringWithFormat:#"Touch: %0.f/%.0f",
point.x,point.y]];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesBegan");
[self removeTouchCircles];
NSSet *allTouches = [event allTouches];
NSArray *allObjects = [allTouches allObjects];
for (int i = 0;i < [allObjects count];i++)
{
UITouch *touch = [allObjects objectAtIndex:i];
CGPoint location = [touch locationInView: self.view];
[self handleTouchEvent:touch.view atPoint:location forState:UIGestureRecognizerStateBegan clear:NO];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesMoved");
[self removeTouchCircles];
NSSet *allTouches = [event allTouches];
NSArray *allObjects = [allTouches allObjects];
for (int i = 0;i < [allObjects count];i++)
{
UITouch *touch = [allObjects objectAtIndex:i];
CGPoint location = [touch locationInView: self.view];
[self handleTouchEvent:touch.view atPoint:location forState:UIGestureRecognizerStateChanged clear:NO];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesEnded");
[self removeTouchCircles];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//NSLog(#"touchesCancelled");
[self removeTouchCircles];
}
//
// Double tap resets passed image. If background image, also reset foreground image.
//
- (IBAction)handleDoubleTap:(UITapGestureRecognizer *)recognizer {
CGPoint touch = [recognizer locationInView: self.view];
if (recognizer.state == UIGestureRecognizerStateBegan ||
recognizer.state == UIGestureRecognizerStateChanged) {
[self handleTouchEvent:recognizer.view atPoint:touch forState:recognizer.state clear:NO];
} else {
[self removeTouchCircles];
}
[self alert:#"Reset" :#"The Image has been Reset!"];
CGRect frame = originalImageFrame;
CGAffineTransform transform = originalImageTransform;
if (recognizer.view == _backgroundImageView) {
_foregroundImageView.transform = transform;
_foregroundImageView.frame = frame;
[self updateInfo:_foregroundImageView touch:touch change:CGPointZero];
frame = originalBackgroundFrame;
transform = originalBackgroundTransform;
}
recognizer.view.transform = transform;
recognizer.view.frame = frame;
[self updateInfo:recognizer.view touch:touch change:CGPointZero];
}
- (void) setDragType:(CGRect)frame withTouch:(CGPoint)touch {
// the corners and sides of the current view frame
CGRect topLeft = CGRectMake(frame.origin.x,frame.origin.y,
touchRadius, touchRadius);
CGRect bottomLeft = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
CGRect topRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y,
touchRadius, touchRadius);
CGRect bottomRight = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y + frame.size.height - touchRadius,
touchRadius, touchRadius);
CGRect leftSide = CGRectMake(frame.origin.x,frame.origin.y,
touchRadius, frame.size.height);
CGRect rightSide = CGRectMake(frame.origin.x + frame.size.width - touchRadius,
frame.origin.y,
touchRadius, frame.size.height);
CGRect topSide = CGRectMake(frame.origin.x,frame.origin.y,
frame.size.width, touchRadius);
CGRect bottomSide = CGRectMake(frame.origin.x,
frame.origin.y + frame.size.height - touchRadius,
frame.size.width, touchRadius);
if (CGRectContainsPoint(topLeft, touch)) {
currentDragType = DRAG_TOPLEFT;
} else if (CGRectContainsPoint(topRight, touch)) {
currentDragType = DRAG_TOPRIGHT;
} else if (CGRectContainsPoint(bottomLeft, touch)) {
currentDragType = DRAG_BOTTOMLEFT;
} else if (CGRectContainsPoint(bottomRight, touch)) {
currentDragType = DRAG_BOTTOMRIGHT;
} else if (CGRectContainsPoint(topSide, touch)) {
currentDragType = DRAG_TOP;
} else if (CGRectContainsPoint(bottomSide, touch)) {
currentDragType = DRAG_BOTTOM;
} else if (CGRectContainsPoint(leftSide, touch)) {
currentDragType = DRAG_LEFT;
} else if (CGRectContainsPoint(rightSide, touch)) {
currentDragType = DRAG_RIGHT;
} else if (CGRectContainsPoint(frame, touch)) {
currentDragType = DRAG_CENTER;
} else {
currentDragType = DRAG_OFF; // touch point is not in the view frame
}
}
//
// Return the unrotated size of the view
//
- (CGSize) getActualSize:(UIView *)view {
CGSize result;
//CGSize originalSize = view.frame.size;
CGAffineTransform originalTransform = view.transform;
float rotation = atan2f(view.transform.b, view.transform.a);
// reverse rotation of current transform
CGAffineTransform unrotated = CGAffineTransformRotate(view.transform, -rotation);
view.transform = unrotated;
// get the size of the "unrotated" view
result = view.frame.size;
// reset back to what it was
view.transform = originalTransform;
//NSLog(#"Size current = %0.f/%0.f, rotation = %0.2f, unrotated = %0.f/%0.f",
// originalSize.width,originalSize.height,
// rotation,
// result.width,result.height);
return result;
}
//
// Resize or Pan an image on the ViewController View
//
- (IBAction)handleResize:(UIPanGestureRecognizer *)recognizer {
static CGRect initialFrame;
static CGAffineTransform initialTransform;
static Boolean scaleIt = YES;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialFrame = recognizer.view.frame;
initialTransform = recognizer.view.transform;
[self setDragType:recognizer.view.frame withTouch:touch];
scaleIt = YES;
}
if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
scaleIt = NO;
[self getActualSize:recognizer.view];
} else {
// our new view frame - start with the initial one
CGRect newFrame = initialFrame;
// adjust the translation point according to where the user touched the image
float tx = translation.x;
float ty = translation.y;
// resize by dragging a corner or a side
if (currentDragType == DRAG_TOPLEFT) {
tx = -translation.x;
ty = -translation.y;
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
newFrame.size.width -= translation.x;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_TOPRIGHT) {
ty = -translation.y;
newFrame.origin.y += translation.y;
newFrame.size.width += translation.x;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_BOTTOMLEFT) {
tx = -translation.x;
newFrame.origin.x += translation.x;
newFrame.size.width -= translation.x;
newFrame.size.height += translation.y;
} else if (currentDragType == DRAG_BOTTOMRIGHT) {
// origin does not change
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
} else if (currentDragType == DRAG_TOP) {
tx = 0;
newFrame.origin.y += translation.y;
newFrame.size.height -= translation.y;
} else if (currentDragType == DRAG_BOTTOM) {
tx = 0;
newFrame.size.height += translation.y;
} else if (currentDragType == DRAG_LEFT) {
tx = -translation.x;
ty = 0;
newFrame.origin.x += translation.x;
newFrame.size.width -= translation.x;
} else if (currentDragType == DRAG_BOTTOM) {
ty = 0;
newFrame.size.width += translation.x;
} else { //if (currentDragType == DRAG_CENTER) {
newFrame.origin.x += translation.x;
newFrame.origin.y += translation.y;
scaleIt = NO; // normal pan
}
// get the unrotated size of the view
CGSize actualSize = [self getActualSize:recognizer.view];
// make sure we can still touch the image
if (actualSize.width < touchRadius * 2) {
newFrame.size.width += touchRadius * 2;
tx = 0; // stop resizing
}
if (actualSize.height < touchRadius * 2) {
newFrame.size.height += touchRadius * 2;
ty = 0; // stop resizing
}
// pan the image
recognizer.view.transform = CGAffineTransformTranslate(initialTransform, tx, ty);
if (scaleIt) {
// the origin or size changed
recognizer.view.frame = newFrame;
}
}
[self updateInfo:recognizer.view touch:touch change:translation];
}
//
// Pan an image on the ViewController View
//
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialTransform = recognizer.view.transform;
currentDragType = DRAG_ON;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
[self getActualSize:recognizer.view];
}
recognizer.view.transform = CGAffineTransformTranslate(initialTransform, translation.x, translation.y);
[self updateInfo:recognizer.view touch:touch change:translation];
}
//
// Pinch (resize) an image on the ViewController View
//
- (void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGSize initialSize;
static CGAffineTransform initialTransform;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
//get the translation amount in x,y
CGPoint change = CGPointMake(recognizer.view.transform.tx, recognizer.view.transform.ty);
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialSize = recognizer.view.frame.size;
initialTransform = recognizer.view.transform;
currentDragType = DRAG_ON;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
[self getActualSize:recognizer.view];
}
// make sure it stays visible
float scale = recognizer.scale;
float newWidth = initialSize.width * scale;
float newHeight = initialSize.height * scale;
// make sure we can still touch it
if (newWidth > touchRadius * 2 && newHeight > touchRadius * 2) {
// scale the image
recognizer.view.transform = CGAffineTransformScale(initialTransform, scale, scale);
}
[self updateInfo:recognizer.view touch:touch change:change];
}
//
// Rotate an image on the ViewController View
//
- (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer {
static CGFloat initialRotation;
static CGAffineTransform initialTransform;
// where the user has touched down
CGPoint touch = [recognizer locationInView: self.view];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialTransform = recognizer.view.transform;
initialRotation = atan2f(recognizer.view.transform.b, recognizer.view.transform.a);
currentDragType = DRAG_ON;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
currentDragType = DRAG_OFF;
[self getActualSize:recognizer.view];
}
recognizer.view.transform = CGAffineTransformRotate(initialTransform, recognizer.rotation);
[self updateInfo:recognizer.view touch:touch change:CGPointMake(initialRotation, recognizer.rotation)];
}
//
// Prevent simultaneous gestures so my transforms don't get funky
// (may not be necessary ... )
//
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return NO;
}
//
// Spew a message to the user
//
- (void)alert:(NSString *) title :(NSString *)message {
UIAlertController *alert = [UIAlertController
alertControllerWithTitle: title
message: message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okButton = [UIAlertAction
actionWithTitle:#"Ok"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
}];
[alert addAction:okButton];
[self presentViewController:alert animated:YES completion:nil];
}
#end
I'm using this class to create a circular slider but i have a problem,how to not allow when user slide from 1% to 100% and vice versa? Please help me to fix this issue. Thanks in advance.
Here is code:
#interface SLCircularSlider()
#property (nonatomic) CGPoint thumbCenterPoint;
#pragma mark - Init and Setup methods
- (void)setup;
#pragma mark - Thumb management methods
- (BOOL)isPointInThumb:(CGPoint)point;
#pragma mark - Drawing methods
- (CGFloat)sliderRadius;
- (void)drawThumbAtPoint:(CGPoint)sliderButtonCenterPoint inContext:(CGContextRef)context;
- (CGPoint)drawCircularTrack:(float)track atPoint:(CGPoint)point withRadius:(CGFloat)radius inContext:(CGContextRef)context;
- (CGPoint)drawPieTrack:(float)track atPoint:(CGPoint)point withRadius:(CGFloat)radius inContext:(CGContextRef)context;
#end
#pragma mark -
#implementation SLCircularSlider
#synthesize value = _value;
- (void)setValue:(float)value {
if (value != _value) {
if (value > self.maximumValue) { value = self.maximumValue; }
if (value < self.minimumValue) { value = self.minimumValue; }
_value = value;
[self setNeedsDisplay];
if (self.isContinuous) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
}
#synthesize minimumValue = _minimumValue;
- (void)setMinimumValue:(float)minimumValue {
if (minimumValue != _minimumValue) {
_minimumValue = minimumValue;
if (self.maximumValue < self.minimumValue) { self.maximumValue = self.minimumValue; }
if (self.value < self.minimumValue) { self.value = self.minimumValue; }
}
}
#synthesize maximumValue = _maximumValue;
- (void)setMaximumValue:(float)maximumValue {
if (maximumValue != _maximumValue) {
_maximumValue = maximumValue;
if (self.minimumValue > self.maximumValue) { self.minimumValue = self.maximumValue; }
if (self.value > self.maximumValue) { self.value = self.maximumValue; }
}
}
#synthesize minimumTrackTintColor = _minimumTrackTintColor;
- (void)setMinimumTrackTintColor:(UIColor *)minimumTrackTintColor {
if (![minimumTrackTintColor isEqual:_minimumTrackTintColor]) {
_minimumTrackTintColor = minimumTrackTintColor;
[self setNeedsDisplay];
}
}
#synthesize maximumTrackTintColor = _maximumTrackTintColor;
- (void)setMaximumTrackTintColor:(UIColor *)maximumTrackTintColor {
if (![maximumTrackTintColor isEqual:_maximumTrackTintColor]) {
_maximumTrackTintColor = maximumTrackTintColor;
[self setNeedsDisplay];
}
}
#synthesize thumbTintColor = _thumbTintColor;
- (void)setThumbTintColor:(UIColor *)thumbTintColor {
if (![thumbTintColor isEqual:_thumbTintColor]) {
_thumbTintColor = thumbTintColor;
[self setNeedsDisplay];
}
}
#synthesize continuous = _continuous;
#synthesize sliderStyle = _sliderStyle;
- (void)setSliderStyle:(UICircularSliderStyle)sliderStyle {
if (sliderStyle != _sliderStyle) {
_sliderStyle = sliderStyle;
[self setNeedsDisplay];
}
}
#synthesize thumbCenterPoint = _thumbCenterPoint;
/** #name Init and Setup methods */
#pragma mark - Init and Setup methods
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (void)awakeFromNib {
[self setup];
}
- (void)setup {
self.value = 0.0;
self.minimumValue = 0.0;
self.maximumValue = 1.0;
/*self.minimumTrackTintColor = [UIColor blueColor];
self.maximumTrackTintColor = [UIColor whiteColor];
self.thumbTintColor = [UIColor darkGrayColor];*/
self.minimumTrackTintColor = [UIColor clearColor];
self.maximumTrackTintColor = [UIColor clearColor];
self.thumbTintColor = [UIColor clearColor];
self.continuous = YES;
self.thumbCenterPoint = CGPointZero;
/**
* This tapGesture isn't used yet but will allow to jump to a specific location in the circle
*/
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapGestureHappened:)];
[self addGestureRecognizer:tapGestureRecognizer];
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panGestureHappened:)];
panGestureRecognizer.maximumNumberOfTouches = panGestureRecognizer.minimumNumberOfTouches;
[self addGestureRecognizer:panGestureRecognizer];
}
/** #name Drawing methods */
#pragma mark - Drawing methods
#define kLineWidth 5.0
#define kThumbRadius 12.0
- (CGFloat)sliderRadius {
CGFloat radius = MIN(self.bounds.size.width/2, self.bounds.size.height/2);
radius -= MAX(kLineWidth, kThumbRadius);
return radius;
}
- (void)drawThumbAtPoint:(CGPoint)sliderButtonCenterPoint inContext:(CGContextRef)context {
UIGraphicsPushContext(context);
CGContextBeginPath(context);
CGContextMoveToPoint(context, sliderButtonCenterPoint.x, sliderButtonCenterPoint.y);
CGContextAddArc(context, sliderButtonCenterPoint.x, sliderButtonCenterPoint.y, kThumbRadius, 0.0, 2*M_PI, NO);
CGContextFillPath(context);
UIGraphicsPopContext();
}
- (CGPoint)drawCircularTrack:(float)track atPoint:(CGPoint)center withRadius:(CGFloat)radius inContext:(CGContextRef)context {
UIGraphicsPushContext(context);
CGContextBeginPath(context);
float angleFromTrack = translateValueFromSourceIntervalToDestinationInterval(track, self.minimumValue, self.maximumValue, 0, 2*M_PI);
CGFloat startAngle = -M_PI_2;
CGFloat endAngle = startAngle + angleFromTrack;
CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, NO);
CGPoint arcEndPoint = CGContextGetPathCurrentPoint(context);
CGContextStrokePath(context);
UIGraphicsPopContext();
return arcEndPoint;
}
- (CGPoint)drawPieTrack:(float)track atPoint:(CGPoint)center withRadius:(CGFloat)radius inContext:(CGContextRef)context {
UIGraphicsPushContext(context);
float angleFromTrack = translateValueFromSourceIntervalToDestinationInterval(track, self.minimumValue, self.maximumValue, 0, 2*M_PI);
CGFloat startAngle = -M_PI_2;
CGFloat endAngle = startAngle + angleFromTrack;
CGContextMoveToPoint(context, center.x, center.y);
CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, NO);
CGPoint arcEndPoint = CGContextGetPathCurrentPoint(context);
CGContextClosePath(context);
CGContextFillPath(context);
UIGraphicsPopContext();
return arcEndPoint;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint middlePoint;
middlePoint.x = self.bounds.origin.x + self.bounds.size.width/2;
middlePoint.y = self.bounds.origin.y + self.bounds.size.height/2;
CGContextSetLineWidth(context, kLineWidth);
CGFloat radius = [self sliderRadius];
switch (self.sliderStyle) {
case UICircularSliderStylePie:
[self.maximumTrackTintColor setFill];
[self drawPieTrack:self.maximumValue atPoint:middlePoint withRadius:radius inContext:context];
[self.minimumTrackTintColor setStroke];
[self drawCircularTrack:self.maximumValue atPoint:middlePoint withRadius:radius inContext:context];
[self.minimumTrackTintColor setFill];
self.thumbCenterPoint = [self drawPieTrack:self.value atPoint:middlePoint withRadius:radius inContext:context];
break;
case UICircularSliderStyleCircle:
default:
[self.maximumTrackTintColor setStroke];
[self drawCircularTrack:self.maximumValue atPoint:middlePoint withRadius:radius inContext:context];
[self.minimumTrackTintColor setStroke];
self.thumbCenterPoint = [self drawCircularTrack:self.value atPoint:middlePoint withRadius:radius inContext:context];
break;
}
[self.thumbTintColor setFill];
[self drawThumbAtPoint:self.thumbCenterPoint inContext:context];
}
/** #name Thumb management methods */
#pragma mark - Thumb management methods
- (BOOL)isPointInThumb:(CGPoint)point {
CGRect thumbTouchRect = CGRectMake(self.thumbCenterPoint.x - kThumbRadius, self.thumbCenterPoint.y - kThumbRadius, kThumbRadius*2, kThumbRadius*2);
return CGRectContainsPoint(thumbTouchRect, point);
}
/** #name UIGestureRecognizer management methods */
#pragma mark - UIGestureRecognizer management methods
- (void)panGestureHappened:(UIPanGestureRecognizer *)panGestureRecognizer {
CGPoint tapLocation = [panGestureRecognizer locationInView:self];
/* UILabel* percentlbl =(UILabel*) [self.superview viewWithTag:10];
NSLog(#"percentlbl frame %f",percentlbl.frame.origin.y
);
if (CGRectContainsPoint(percentlbl.frame, tapLocation)) {
NSLog(#"Tapped label");
}*/
switch (panGestureRecognizer.state) {
case UIGestureRecognizerStateChanged: {
CGFloat radius = [self sliderRadius];
CGPoint sliderCenter = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
CGPoint sliderStartPoint = CGPointMake(sliderCenter.x, sliderCenter.y - radius);
CGFloat angle = angleBetweenThreePoints(sliderCenter, sliderStartPoint, tapLocation);
if (angle < 0) {
angle = -angle;
}
else {
angle = 2*M_PI - angle;
}
self.value = translateValueFromSourceIntervalToDestinationInterval(angle, 0, 2*M_PI, self.minimumValue, self.maximumValue);
break;
}
case UIGestureRecognizerStateEnded:
if (!self.isContinuous) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
if ([self isPointInThumb:tapLocation]) {
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
}
else {
[self sendActionsForControlEvents:UIControlEventTouchUpOutside];
}
break;
default:
break;
}
}
- (void)tapGestureHappened:(UITapGestureRecognizer *)tapGestureRecognizer {
if (tapGestureRecognizer.state == UIGestureRecognizerStateEnded) {
CGPoint tapLocation = [tapGestureRecognizer locationInView:self];
if ([self isPointInThumb:tapLocation]) {
}
else {
}
}
}
/** #name Touches Methods */
#pragma mark - Touches Methods
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:self];
if ([self isPointInThumb:touchLocation]) {
[self sendActionsForControlEvents:UIControlEventTouchDown];
}
}
#end
/** #name Utility Functions */
#pragma mark - Utility Functions
float translateValueFromSourceIntervalToDestinationInterval(float sourceValue, float sourceIntervalMinimum, float sourceIntervalMaximum, float destinationIntervalMinimum, float destinationIntervalMaximum) {
float a, b, destinationValue;
a = (destinationIntervalMaximum - destinationIntervalMinimum) / (sourceIntervalMaximum - sourceIntervalMinimum);
b = destinationIntervalMaximum - a*sourceIntervalMaximum;
destinationValue = a*sourceValue + b;
return destinationValue;
}
CGFloat angleBetweenThreePoints(CGPoint centerPoint, CGPoint p1, CGPoint p2) {
CGPoint v1 = CGPointMake(p1.x - centerPoint.x, p1.y - centerPoint.y);
CGPoint v2 = CGPointMake(p2.x - centerPoint.x, p2.y - centerPoint.y);
CGFloat angle = atan2f(v2.x*v1.y - v1.x*v2.y, v1.x*v2.x + v1.y*v2.y);
return angle;
}
What you need to do is add a stage in your gesture recognition where you determine if the user is increasing or decreasing the counter. If decreasing, do not respond to decrease changes once the value drops below zero (probable you want to be able to get to zero, not stop at one despite what the question says?). If increasing, don't respond to increase changes once value gets to 100%. You need to ensure of course that while at the maximum you continue to be responsive if the user changes to decrease the value, and likewise to increasing when at zero.
I want to zoom in out a CCNode by pinching and panning the screen. The node has a background which is very large but the portionof it shown on the screen. That node also contains other sprites.
What I have done by now is that first I register UIPinchGestureRecognizer
UIPinchGestureRecognizer * pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinchFrom:)];
[[[CCDirector sharedDirector] view] addGestureRecognizer: pinchRecognizer];
-(void)handlePinchFrom:(UIPinchGestureRecognizer *) pinch
{
if(pinch.state == UIGestureRecognizerStateEnded) {
prevScale = 1;
}
else {
CGFloat dscale = [self scale] - prevScale + pinch.scale;
if(dscale > 0)
{
deltaScale = dscale;
}
CGAffineTransform transform = CGAffineTransformScale(pinch.view.transform, deltaScale, deltaScale);
[pinch.view setTransform: transform];
// [_contentNode setScale:deltaScale];
prevScale = pinch.scale;
}
}
The problem is that it scalw whole UIView not the CCNode. I have also tried to by setting the scale of my _contentNode.
**EDIT
I ave also tried this
- (void)handlePinchGesture:(UIPinchGestureRecognizer*)aPinchGestureRecognizer
{
if (pinch.state == UIGestureRecognizerStateBegan || pinch.state == UIGestureRecognizerStateChanged) {
CGPoint midpoint = [pinch locationInView:[CCDirector sharedDirector].view];
CGSize winSize = [CCDirector sharedDirector].viewSize;
float x = midpoint.x/winSize.width;
float y = midpoint.y/winSize.height;
_contentNode.anchorPoint = CGPointMake(x, y);
float scale = [pinch scale];
_contentNode.scale *= scale;
pinch.scale = 1;
}
}
But it zoom from the bottom left of the screen.
I had the same problem. I use CCScrollView, that contains CCNode that larger than device screen. I want scroll and zoom it, but node shouldnt scroll out of screen, and scale smaller than screen. So, i create my subclass of CCScrollView, where i handle pinch. It has some strange glitches, but it works fine at all.
When pinch began i set anchor point of my node to pinch center on node space. Then i need change position of my node proportional to shift of anchor point, so moving anchor point doesn't change nodes location on view:
- (void)handlePinch:(UIPinchGestureRecognizer*)recognizer
{
if (recognizer.state == UIGestureRecognizerStateEnded) {
_previousScale = self.contentNode.scale;
}
else if (recognizer.state == UIGestureRecognizerStateBegan) {
float X = [recognizer locationInNode:self.contentNode].x / self.contentNode.contentSize.width;
float Y = [recognizer locationInNode:self.contentNode].y / self.contentNode.contentSize.height;
float positionX = self.contentNode.position.x + self.contentNode.boundingBox.size.width * (X - self.contentNode.anchorPoint.x);
float positionY = self.contentNode.position.y + self.contentNode.boundingBox.size.height * (Y - self.contentNode.anchorPoint.y);
self.contentNode.anchorPoint = ccp(X, Y);
self.contentNode.position = ccp(positionX, positionY);
}
else {
CGFloat scale = _previousScale * recognizer.scale;
if (scale >= maxScale) {
self.contentNode.scale = maxScale;
}
else if (scale <= [self minScale]) {
self.contentNode.scale = [self minScale];
}
else {
self.contentNode.scale = scale;
}
}
}
Also i need change CCScrollView min and max scroll, so my node never scroll out of view. Default anchor point is (0,1), so i need shift min and max scroll proportional to the new anchor point.
- (float) maxScrollX
{
if (!self.contentNode) return 0;
float maxScroll = self.contentNode.boundingBox.size.width - self.contentSizeInPoints.width;
if (maxScroll < 0) maxScroll = 0;
return maxScroll - self.contentNode.boundingBox.size.width * self.contentNode.anchorPoint.x;
}
- (float) maxScrollY
{
if (!self.contentNode) return 0;
float maxScroll = self.contentNode.boundingBox.size.height - self.contentSizeInPoints.height;
if (maxScroll < 0) maxScroll = 0;
return maxScroll - self.contentNode.boundingBox.size.height * (1 - self.contentNode.anchorPoint.y);
}
- (float) minScrollX
{
float minScroll = [super minScrollX];
return minScroll - self.contentNode.boundingBox.size.width * self.contentNode.anchorPoint.x;
}
- (float) minScrollY
{
float minScroll = [super minScrollY];
return minScroll - self.contentNode.boundingBox.size.height * (1 - self.contentNode.anchorPoint.y);
}
UIGestureRecognizerStateEnded doesn't have locationInNode: method, so i added it by category. It just return touch location on node space:
#import "UIGestureRecognizer+locationInNode.h"
#implementation UIGestureRecognizer (locationInNode)
- (CGPoint) locationInNode:(CCNode*) node
{
CCDirector* dir = [CCDirector sharedDirector];
CGPoint touchLocation = [self locationInView: [self view]];
touchLocation = [dir convertToGL: touchLocation];
return [node convertToNodeSpace:touchLocation];
}
- (CGPoint) locationInWorld
{
CCDirector* dir = [CCDirector sharedDirector];
CGPoint touchLocation = [self locationInView: [self view]];
return [dir convertToGL: touchLocation];
}
#end
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.
I have 4 elements: greenball, yellowball, orangeball, and redball that fall from the top of the screen
I also have an element, blueball, that follows my touch.
All of these things are working great! :D
However, I want to detect when the greenball intersects with the blueball
my blue ball is placed in the view at viewDidLoad, the 4 balls are placed on the view based on an nstimer that calls onTimer.
All 5 balls are created on the same layer:
[self.view insertSubview:greenImage belowSubview:bottomBar]
I can detect when the redball and greenball collide:
if (CGRectIntersectsRect(greenImage.frame, redImage.frame)) {
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"title" message:#"points" delegate:nil cancelButtonTitle:#"cool" otherButtonTitles:nil];
[message show];
[message release];
}
If I replace "redImage" for "blueball" I don't get the alert. I assume this may be because they aren't declared in the same method.
Any ideas on how to detect this collision?
thanks!
--- EDIT ---
Using the suggestion below I'm doing the following:
greenImage = [[UIImageView alloc] initWithImage:green];
greenImage.frame = CGRectMake(startX,0,43,43);
[self.view insertSubview:greenImage belowSubview:bottomBar];
[UIView beginAnimations:nil context:greenImage];
[UIView setAnimationDuration:5 * speed];
greenImage.frame = CGRectMake(startX, 500.0, 43,43);
CGFloat r1 = greenImage.frame.size.width * .5;
CGFloat r2 = blueBall.frame.size.width * .5;
if(CGPointDistance(greenImage.center, blueBall.center) < (r1 + r2)){
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"notice" message:#"hit!" delegate:nil cancelButtonTitle:#"cool" otherButtonTitles:nil];
[message show];
[message release];
}
below this method I have:
CGFloat CGPointDistance(CGPoint p1, CGPoint p2)
{
return sqrtf((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}
get an error:
conflicting types for 'CGPointDistance'
and a warning:
implicit declaration of function 'CGPointDistance'
To determine if two circles intersect, you simply see if the distance between the centers is less than the sum if their radii. For example...
CGFloat CGPointDistance(CGPoint p1, CGPoint p2)
{
return sqrtf((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}
It looks like your "balls" are views... Assuming the "ball" fills the frame, the radius will be 1/2 the width...
BOOL BallsCollide(UIView *v1, UIView *v2)
{
CGFloat r1 = v1.frame.size.width * .5;
CGFloat r2 = v2.frame.size.width * .5;
return CGPointDistance(v1.center, v2.center) < (r1 + r2);
}
OK -- I have hacked up a quick sample. You can almost drop it into your view controller. Don't use it for production stuff... just as an example of how you can do stuff... You can put this at the top and the rest of your controller below it...
// I do not recommend using this approach for naything non-trivial,
// but it demonstrates the very simple collision detection for circle shapes.
#interface Ball : UIView {
}
#property (nonatomic, strong) UIColor *origColor;
#property (nonatomic, strong) UIColor *color;
+ (id)ballWithRadius:(CGFloat)radius color:(UIColor*)color;
- (BOOL)doesIntersectWith:(Ball*)ball;
#end
#implementation Ball
#synthesize color = _color;
#synthesize origColor = _origColor;
+ (id)ballWithRadius:(CGFloat)radius color:(UIColor*)color
{
CGFloat diameter = radius * 2;
Ball *ball = [[Ball alloc] initWithFrame:CGRectMake(0, 0, diameter, diameter)];
ball.color = ball.origColor = color;
ball.backgroundColor = [UIColor clearColor];
return ball;
}
- (BOOL)doesIntersectWith:(Ball *)ball
{
// Two circles overlap if the sum of their radii
// is less than the distance between the two centers.
CGFloat r1 = self.frame.size.width * .5;
CGFloat r2 = ball.frame.size.width * .5;
CGFloat diffX = self.center.x - ball.center.x;
CGFloat diffY = self.center.y - ball.center.y;
return (diffX*diffX) + (diffY*diffY) < (r1+r2)*(r1+r2);
}
- (BOOL)containsPoint:(CGPoint)point
{
// Contains a point if distance from center is less than radius
CGFloat r = self.frame.size.width *.5;
CGFloat diffX = self.center.x - point.x;
CGFloat diffY = self.center.y - point.y;
return (diffX*diffX) + (diffY*diffY) < r*r;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[self.color setFill];
[self.color setStroke];
CGContextSetLineWidth(context, 1);
CGContextFillEllipseInRect(context, self.bounds);
}
#end
#interface CollideVC() {
NSMutableDictionary *activeTouches;
NSMutableSet *currentCollisions;
}
#end
#implementation CollideVC
- (void)ball:(Ball*)ball touched:(UITouch*)touch
{
[activeTouches setObject:ball forKey:[NSValue valueWithPointer:(__bridge void*)touch]];
}
- (Ball*)getBallTouchedBy:(UITouch*)touch
{
return [activeTouches objectForKey:[NSValue valueWithPointer:(__bridge void*)touch]];
}
- (void)stopTouch:(UITouch*)touch
{
[activeTouches removeObjectForKey:[NSValue valueWithPointer:(__bridge void*)touch]];
}
- (void)ball:(Ball*)ball1 hasCollidedWith:(Ball*)ball2
{
[currentCollisions addObject:ball1];
[currentCollisions addObject:ball2];
ball1.color = ball2.color = [UIColor yellowColor];
[ball1 setNeedsDisplay];
[ball2 setNeedsDisplay];
}
// NOTE: You should delegate handling of collisions...
- (void)checkForCollisions
{
// Should filter those that don't actually need redisplay...
// For simplicity, all that were or are colliding get it...
[currentCollisions enumerateObjectsUsingBlock:^(Ball *ball, BOOL *stop) {
[ball setNeedsDisplay];
ball.color = ball.origColor;
}];
[currentCollisions removeAllObjects];
NSArray *views = self.view.subviews;
for (size_t i = 0; i < views.count; ++i) {
id v = [views objectAtIndex:i];
if (![v isKindOfClass:[Ball class]]) continue;
Ball *ball1 = v;
for (size_t j = i+1; j < views.count; ++j) {
v = [views objectAtIndex:j];
if (![v isKindOfClass:[Ball class]]) continue;
Ball *ball2 = v;
if ([ball1 doesIntersectWith:ball2]) {
[self ball:ball1 hasCollidedWith:ball2];
}
}
}
}
- (void)addBallWithRadius:(CGFloat)radius point:(CGPoint)point color:(UIColor*)color
{
Ball *ball = [Ball ballWithRadius:radius color:color];
ball.center = point;
[self.view addSubview:ball];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
CGPoint touchLocation = [touch locationInView:self.view];
for (Ball *ball in self.view.subviews) {
if ([ball containsPoint:touchLocation]) {
[self ball:ball touched:touch];
}
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
// Use relative distance so center does nt jump to tap location.
CGPoint prev = [touch previousLocationInView:self.view];
CGPoint curr = [touch locationInView:self.view];
CGPoint c = [self getBallTouchedBy:touch].center;
[self getBallTouchedBy:touch].center = CGPointMake(c.x+curr.x-prev.x, c.y+curr.y-prev.y);
}
// NOTE: This does not check the path between the moves, only the new point.
// So a fast movement "past" the other object will not get detected.
// If you want to do comple detection, use an existing engine
[self checkForCollisions];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
[self stopTouch:touch];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self touchesEnded:touches withEvent:event];
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
activeTouches = [NSMutableDictionary dictionary];
currentCollisions = [NSMutableSet set];
[self addBallWithRadius:20 point:CGPointMake(110, 100) color:[UIColor blueColor]];
[self addBallWithRadius:20 point:CGPointMake(200, 200) color:[UIColor redColor]];
[self addBallWithRadius:20 point:CGPointMake(235, 250) color:[UIColor greenColor]];
}
maybe this will help some, just a little modification to the code above.
-(CGFloat)CGPointDistance:(CGPoint)p1 p2:(CGPoint)p2
{
return sqrtf(powf(p1.x - p2.x) + powf(p1.y-p2.y));
}
-(BOOL)DidCollide:(View*)v1 v2:(View*)v2 v1Radius:(CGFloat)v1Radius v2Radius:(CGFloat)v2Radius
{
CGFloat r1 = v1.frame.size.width * v1Radius;
CGFloat r2 = v2.frame.size.width * v2Radius;
return [self PointDistance:v1.center p2:v2.center] < (r1 + r2)
}
and to check for collision your just do
if([self DidCollide:view1 v2:view2 v1Radius:radius v2Radius:radius])
{
//do something
}