Dragging a view within a view, constraining it within - iphone

I have a pretty simple bit of code for dragging and drawing.
Three views: The root view is basically the window (with a few interface controls).
Within it, I have a "canvas" view. Within the canvas view, I have a third view (a small graphic of a character), which the user is meant to "drag" around within the canvas. The dragging works fine.
The problem is that I can't seem to constrain my "character" within the bounds of the canvas. Ideally, when he runs into the edge of the canvas, he should stop (not spill over into the root view)
The canvas view clips subviews; so he does disappear once he goes over the edge - but I don't want him to disappear, I want him to not be able to go any further.
I also call CGRectIsWithinRect before my animation, which in principle should do the trick. The problem is that the frame of the character isn't updated while the animation is occurring - so as long as he's already moving, he'll keep on moving right of the side of the page.
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UIView * charView = [[canvasView floatingView] retain];
CGPoint pt = [[touches anyObject] locationInView:canvasView];
if (CGRectContainsPoint([canvasView bounds], pt)) {
CGRect charFrame = charView.frame;
CGRect containerFrame = canvasView.frame;
BOOL inTheYard= CGRectContainsRect(containerFrame, charFrame);
if (inTheYard == NO) {
//if we're at the edge, don't go any further
return;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:.2];
CGFloat x = floatingView.center.x + (pt.x - startPoint.x);
CGFloat y = floatingView.center.y + (pt.y - startPoint.y);
CGPoint middle = CGPointMake(x,y);
floatingView.center = middle;
[UIView commitAnimations];
[floatingView release];
}
}
How can I keep him from moving past the edges of his superview?

Instead of checking if the touch event is inside the canvas you can clamp it to be inside. Check the x and y points for the touch event separately and if they are outside the canvas move the character up to the wall on that side of the canvas.
CGFloat w = canvasFrame.frame.size.width;
CGFloat h = canvasFrame.frame.size.height;
if (pt.x < 0) pt.x = 0;
if (pt.x > w) pt.x = w;
if (pt.y < 0) pt.y = 0;
if (pt.y > h) pt.y = h;
This will allow you to keep dragging the character along the wall of the canvas even when the touch event is outside (if you remove the inTheYard check).

Related

Restrict image movement to grid

I'm trying to make the boxes move only in horizontal and vertical lines following the touchesMoved event.
I could easily check on every touch event if the center of the box is in the center of the current grid block and then allow a direction change. But that will require the touches to be very precise, and could also lead to bugs because of speedy fingers etc. and then miss the touch event.
What I essential would like to do is make the box move correctly in the grid, even though your touch path is not "perfect". See image below, blue lines indicates box movement, green line is the path of the touch. The box needs to follow the grid closest to the touch path, so it looks like your moving it.
How can i accomplish this?
Edit: Forgot to mention that the movement of the box should be smooth, not jump from grid to grid.
Video
Bonus info: I will also need to flicks boxes left/right up/down with touch momentum, and of course have some sort of collision detection, so boxes can't move through other boxes on the grid. Am I better off with a 2d physics engine?
Smooth animation based on hacker2007 answer:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint location = [aTouch locationInView:gridContainerView];
for (UIView *subView in gridContainerView.subviews) {
if (conditionForBeingAGridBlock && CGRectContainsPoint(subview.frame, touchPoint))
{
[UIView beginAnimations:#"Dragging A DraggableView" context:nil];
self.frame = CGRectMake(0, location.y, subview.frame.size.width, subview.frame.size.height);
[UIView commitAnimations];
}
}
}
This will move the view you are dragging under your finger, you can add checks yourself to make sure that it doesn't go where it can't. But this will get your a smooth dragging animation.
Seems the problem is what you're setting the frame to in your anitamion:
Currently every time you move the square a little bit you get a new rect here:
CGRect rect = [self.grid getCellRect:self.grid.gridView x:cell.x y:cell.y];
Then in your animation you do this:
self.frame = rect;
Then you animate the box to that rect. So if you move your finger really quickly then you will get that 'skip' effect. If you change it to this:
self.frame = CGRectMake(location.x - self.bounds.size.width / 2, location.y - self.bounds.size.height / 2, self.bounds.size.width, self.bounds.size.height);
try this approach assuming your grid blocks are located in gridContainerView
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint touchPoint = [touch locationInView: gridContainerView];
for (UIView *subView in gridContainerView.subviews) {
if (conditionForBeingAGridBlock &&
CGRectContainsPoint(subview.frame, touchPoint)) {
box.frame = subview.frame;
}
}
}
}

How to make an UICollectionView with infinite paging?

I have a UICollectionView with 6 pages, and paging enabled, and a UIPageControl. What I want is, when I came to the last page, if I drag to right, UICollectionView reloads from first page seamlessly.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
// The key is repositioning without animation
if (collectionView.contentOffset.x == 0) {
// user is scrolling to the left from image 1 to image 10.
// reposition offset to show image 10 that is on the right in the scroll view
[collectionView scrollRectToVisible:CGRectMake(collectionView.frame.size.width*(pageControl.currentPage-1),0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
else if (collectionView.contentOffset.x == 1600) {
// user is scrolling to the right from image 10 to image 1.
// reposition offset to show image 1 that is on the left in the scroll view
[collectionView scrollRectToVisible:CGRectMake(0,0,collectionView.frame.size.width,collectionView.frame.size.height) animated:NO];
}
pageControlUsed = NO;
}
It doesn't work like I want. What can I do?
Here's what I ended up with for my UICollectionView (horizontal scrolling like the UIPickerView):
#implementation UIInfiniteCollectionView
- (void) recenterIfNecessary {
CGPoint currentOffset = [self contentOffset];
CGFloat contentWidth = [self contentSize].width;
// don't just snap to center, since this might be done in the middle of a drag and not aligned. Make sure we account for that offset
CGFloat offset = kCenterOffset - currentOffset.x;
int delta = -round(offset / kCellSize);
CGFloat shift = (offset + delta * kCellSize);
offset += shift;
CGFloat distanceFromCenter = fabs(offset);
// don't always recenter, just if we get too far from the center. Eliza recommends a quarter of the content width
if (distanceFromCenter > (contentWidth / 4.0)) {
self.contentOffset = CGPointMake(kCenterOffset, currentOffset.y);
// move subviews back to make it appear to stay still
for (UIView *subview in self.subviews) {
CGPoint center = subview.center;
center.x += offset;
subview.center = center;
}
// add the offset to the index (unless offset is 0, in which case we'll assume this is the first launch and not a mid-scroll)
if (currentOffset.x > 0) {
int delta = -round(offset / kCellSize);
// MODEL UPDATE GOES HERE
}
}
}
- (void) layoutSubviews { // called at every frame of scrolling
[super layoutSubviews];
[self recenterIfNecessary];
}
#end
Hope this helps someone.
I've been using the Street Scroller sample to create an infinite scroller for images. That works fine until I wanted to set pagingEnabled = YES; Tried tweaking around the recenterIfNecessary code and finally realized that it's the contentOffset.x that has to match the frame of the subview that i want visible when paging stops. This really isn't going to work in recenterIfNecessary since you have no way of knowing it will get called from layoutSubviews. If you do get it adjusted right, the subview may pop out from under your finger. I do the adjustment in scrollViewDidEndDecelerating. So far I haven't had problems with scrolling fast. It will work and simulate paging even when pagingEnabled is NO, but it looks more natural with YES.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[super scrollViewDidEndDecelerating:scrollView];
CGPoint currentOffset = [self contentOffset];
// find the subview that is the closest to the currentOffset.
CGFloat closestOriginX = 999999;
UIView *closestView = nil;
for (UIView *v in self.visibleImageViews) {
CGPoint origin = [self.imageContainerView convertPoint:v.frame.origin toView:self];
CGFloat distanceToCurrentOffset = fabs(currentOffset.x - origin.x);
if (distanceToCurrentOffset <= closestOriginX) {
closestView = v;
closestOriginX = distanceToCurrentOffset;
}
}
// found the closest view, now find the correct offset
CGPoint origin = [self.imageContainerView convertPoint:closestView.frame.origin toView:self];
CGPoint center = [self.imageContainerView convertPoint:closestView.center toView:self];
CGFloat offsetX = currentOffset.x - origin.x;
// adjust the centers of the subviews
[UIView animateWithDuration:0.1 animations:^{
for (UIView *v in self.visibleImageViews) {
v.center = [self convertPoint:CGPointMake(v.center.x+offsetX, center.y) toView:self.imageContainerView];
}
}];
}
I have not used UICollectionView for infinite scrolling, but when doing it with a UIScrollView you first adjust your content offset (instead of using scrollRectToVisible) to the location you want. Then, you loop through each subview in your scroller and adjust their coordinates either to the right or left based on the direction the user was scrolling. Finally, if either end is beyond the bounds you want them to be, move them to the far other end. Their is a very good WWDC video from apple about how to do infinite scrolling you can find here: http://developer.apple.com/videos/wwdc/2012/

Test for a pinch on UIImageView (not pinch and zoom - just test for pinch)

This isn't another one of "those" questions, don't worry.
Basically I want to test for a pinch and then run an animation. Here is the code I currently have:
// test for a pinch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSSet *allTouches = [event allTouches];
if([allTouches count] == 2){
//there are two touches...
UITouch *firstTouch = [[allTouches allObjects] objectAtIndex:0];
UITouch *secondTouch = [[allTouches allObjects] objectAtIndex:1];
CGPoint firstPoint = [firstTouch locationInView:self.superview];
CGPoint secondPoint = [secondTouch locationInView:self.superview];
int X1 = firstPoint.x;
int Y1 = firstPoint.y;
int X2 = secondPoint.x;
int Y2 = secondPoint.y;
// lets work out the distance:
// sqrt( (x2-x1)^2 + (y2-y1)^2 );
preDistance = sqrtf( (float)pow((X2-X1),2) + (float)pow((Y2-Y1),2) );
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSSet *allTouches = [event allTouches];
if([allTouches count] == 2){
//there are two touches...
UITouch *firstTouch = [[allTouches allObjects] objectAtIndex:0];
UITouch *secondTouch = [[allTouches allObjects] objectAtIndex:1];
CGPoint firstPoint = [firstTouch locationInView:self.superview];
CGPoint secondPoint = [secondTouch locationInView:self.superview];
int X1 = firstPoint.x;
int Y1 = firstPoint.y;
int X2 = secondPoint.x;
int Y2 = secondPoint.y;
// lets work out the distance:
// sqrt( (x2-x1)^2 + (y2-y1)^2 );
postDistance = sqrtf( (float)pow((X2-X1),2) + (float)pow((Y2-Y1),2) );
// lets test now
// if the post distance is LESS than the pre distance then
// there has been a pinch INWARDS
if(preDistance > postDistance){
NSLog(#"Pinch INWARDS");
// so we need to move it back to its small frame
if(CGRectEqualToRect(self.frame, largeFrame) && !CGRectIsEmpty(self.smallFrame)){
// we can go inwards
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
self.frame = smallFrame;
[UIView commitAnimations];
}
}
else {
NSLog(#"Pinch OUTWARDS");
// so we need to move it back to its small frame
if(!CGRectEqualToRect(self.frame, largeFrame)){
// we can go outwards
// set org frame
smallFrame = self.frame; // so we can go back later
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
self.frame = largeFrame;
[UIView commitAnimations];
}
}
}
}
All it does is measure the distance between the two touches if there is a multi touch, then it measures the distance again when the touches end. If the difference between the two is positive then it was an outwards pinch, and if it was negative it was an inwards pinch.
Depending on the direction of the pinch the image view will scale bigger or smaller to and from set CGRects (this is why I don't want the "normal" pinch and zoom functionality). I just want it to be a gesture to scale an image up and down again.
However, this isn't working very well... it doesn't always pick up pinches. I don't know if this is because the view its in is also detecting touches (only single pinches), but still...
Anyway, I was wondering if anyone has a better way of detecting pinches? I've currently subclassed the UIImageView to create my ImageController class which has these touch methods in them. All I need to know is a) they are touching this image and b) which direction they've pinched in.
Thanks
Check out UIPinchGestureRecognizer in the documentation. I haven't used it, but seems like it's just what you need.
Anyway, I was wondering if anyone has a better way of detecting pinches?
If you're targeting iOS 3.2 and upwards you can use gesture recognizers - there's no good reason not to. If you need to target pre 3.2, then your above approach (or varients therein, using touchesBegin, etc) is the only way, so it's just tweaking the algorithm.
If you can, I'd strongly recommend using UIPinchGestureRecognizer. It will make your life considerably easier, as it handles all the details. If you want to detect both the scale and direction of the pinch you can combine UIPinchGestureRecognizer with a UIRotationGestureRecognizer - the former will just give you the scale of a pinch, not the relative angle.
If you can live without 3.1.x devices I would recommend that you use the UIPinchGestureRecognizer. It's very simple to use, but the problem is that you're only going to be able to run it on iOS devices running 3.2 and upwards.
For some reason you are using locationInView to identify the position of the touches. Have you tried identifying the touches location in the current view (the actual image view)? Calling superview means that you will get the position of the touches in the view that contains your image view.

How to move a UIImageView after applying CGAffineTransformRotate to it?

I'm building an iPhone app. I have a UIView that contains a set of UIImageView subclass objects. The user can drag and rotate the image views via touch. I'm having trouble moving the image view after it has been rotated.
To rotate an image view, I apply a transform rotation, which works fine. It looks like this:
CGAffineTransform trans = self.transform;
self.transform = CGAffineTransformRotate(trans, delta);
The problem comes later when the user tries to move the element via touch. In touchesBegan:WithEvent:, I save the start point in a class variable, startLocation:
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
// Retrieve the touch point
CGPoint pt = [[touches anyObject] locationInView:self];
startLocation = pt;
}
In touchesMoved:withEvent:, I had the following code, which works well enough if there is no rotation transform on the image view:
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint pt = [[touches anyObject] locationInView:self];
CGFloat dx = pt.x - startLocation.x;
CGFloat dy = pt.y - startLocation.y;
CGPoint newCenter = CGPointMake(self.center.x + dx, self.center.y + dy);
self.center = newCenter;
}
But if there is a rotation transform on the image view, then the image view thrashes about the screen on each touchesMoved event and soon disappears. In the debugger, I observed that the value of pt became monstrous. It occurred to me that I needed to transform that point, which I did, like so:
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint pt = [[touches anyObject] locationInView:self];
if (!CGAffineTransformIsIdentity(self.transform)) {
pt = CGPointApplyAffineTransform(pt, self.transform);
}
CGFloat dx = pt.x - startLocation.x;
CGFloat dy = pt.y - startLocation.y;
CGPoint newCenter = CGPointMake(self.center.x + dx, self.center.y + dy);
}
This worked much better. I can drag the image about the screen now. But the very first movement causes the image to jolt once in one direction or another, depending on the angle of rotation in the transform and the dimensions of the image view.
How can I move the image view without having the initial jolt?
Why is it that I do not need to transform startLocation (the point I capture when touches began)?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)_event {
CGPoint pt = [[touches anyObject] locationInView:self];
startLocation = pt;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)_event {
CGPoint pt = [[touches anyObject] previousLocationInView:self];
CGFloat dx = pt.x - startLocation.x;
CGFloat dy = pt.y - startLocation.y;
CGPoint newCenter = CGPointMake(self.center.x + dx, self.center.y + dy);
self.center = newCenter;
}
- (void)setRotation:(float)rotation {
self.transform = CGAffineTransformRotate(self.transform, degreesToRadians(rotation));
}
It seems you need to convert the coordinates from the main not-rotated object to the rotated-view system.
Take a look at the UIView method "convertPoint:toView:":
convertPoint:toView:
Converts a point from the receiver’s coordinate system to that of the specified view.
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view
Parameters
point
A point specified in the local coordinate system (bounds) of the receiver.
view
The view into whose coordinate system point is to be converted. If view is nil, this method instead converts to window base coordinates. Otherwise, both view and the receiver must belong to the same UIWindow object.
Return Value
The point converted to the coordinate system of view.
Update:
In response to the comments:
You have to get the "real" coordinates of the finger touch (in the non-rotated system), then when the finger moves you always have new coordinates in the main non-rotated view: and they are the point that you have to convert in the rotated view parent of the view that you are moving.
If A is a mainView 320x480 pixel
and B is a subView 320x480 pixel centered in A,
and C is a subView in B at position 170,240 (+10,+0 of the screen center)
and you rotate B of 90 degrees clockwise
then C is still in 170,240 in B
But you see it in 160,250 on the screen,
and if now the user want to move it +20 to the right the user moves the finger +20 in the screen coordinates, not in the B view coordinates,
so the user would like to see it in 180,250 of the screen,
that means you need to convert this point in the B coordinates system...
So it's a bit easier, you just need to get the screen coordinates of the finger when the user moves it and just convert it in rotated-view coordinates (B)...

Pull Menu in CocoaTouch

I am attempting to make a UIView/UIControl that people can drag up and reveal a text box, and then drag down to hide it. However, I have yet to find a method to make this "fluid" - it always seems to stop at random places and doesn't allow any more movement. At the moment I am using a UIView for the top part of the view and here is the current code:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
if ([touch view] == topMenuView) {
CGPoint location = [touch locationInView:self.superview];
CGPoint locationInsideBox = [touch locationInView:self];
CGPoint newLocation = location;
newLocation.x = self.center.x;
newLocation.y = newLocation.y + (self.frame.size.height - locationInsideBox.y) / 2;
if ((self.superview.frame.size.height - newLocation.y) < (self.frame.size.height / 2) && (self.superview.frame.size.height - newLocation.y) > -32)
{
self.center = newLocation;
}
return;
}
}
Any help would be much appreciated!
I would use a pan gesture recognizer. The following code will simply move the view up and down along with the user's finger. If you want to limit how far up it moves, have it snap to place or have momentum you'll need to add to it.
UIView * view; // The view you're moving
CGRect originalFrame; // The frame of the view when the touch began
- (void) pan:(UIPanGestureRecognizer *)pan {
switch (pan.state) {
case UIGestureRecognizerStateBegan: {
originalFrame = view.frame;
} break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded: {
CGPoint translation = [pan translationInView:view];
CGRect frame = originalFrame;
frame.origin.y += translation.y;
view.frame = frame;
} break;
}
}
Removing this line may well help you:
newLocation.y = newLocation.y + (self.frame.size.height - locationInsideBox.y) / 2;
What you're trying to accomplish is really nothing more than a scrollable view, so I would recommend using a UIScrollView.
Put your UIView in a UIScrollView with transparent background, and place the UIScrollView on top of your textbox. Set the correct contentSize and you're good to go.
use a uiview animation block to update the frame of the sliding view corresponding with the touch point recieved. set the animation blocks duration to something really short like .01 or lower.
I'd recommend splitting the issue in two:
Implement the view which has the text box at the bottom - You will just have to implement your own custom view/view controller.
Add your view as a subview of a UIScrollView.
This is a good tutorial which demonstrates proper initialization of UIScrollView and embedding content in it.
Custom views/controllers are a bit of a broader subject :)