limiting the panning bounds of UIPanGestureRecognizer - iphone

I have a view that I am currently hiding at the bottom of the screen. Now I want to be able to move the view by scrolling it vertically, through the y-axis. But I don't want it to go up further than full height of the view (i.e: I don't want to see a white space at the bottom). I've written this code:
- (IBAction)panHighlightReadingVC:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:self.view];
CGPoint newCenter = CGPointMake(self.view.bounds.size.width / 2,
roundf(recognizer.view.center.y + translation.y));
CGFloat velocityY = [recognizer velocityInView:self.view].y;
if ((recognizer.view.frameY > self.view.frameHeight - recognizer.view.frameHeight || velocityY > 0)) {
recognizer.view.center = newCenter;
[recognizer setTranslation:CGPointZero inView:self.view];
}
}
This sort of work, if I scroll it slowly. If I scroll it very fast, then there is a chance that the frameY of the view is less than the `superView.frameHeight - recognizer.view.frameHeight. How do I fix this?

When you are calculating the newCenter, limit it to the bounds that you actually want it to fit in. You probably don't need that last if statement if you are limiting the newCenter to be within your limits.
Also, do yourself a favor and only use center when it actually makes things simpler. In your case I think you should use frame. Then you don't have to worry about dividing by 2.
Try something like this:
- (IBAction)panHighlightReadingVC:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:self.view];
CGFloat newY = MAX(recognizer.view.frame.origin.y + translation.y, self.view.frameHeight - recognizer.view.frameHeight);
CGRect newFrame = CGRectMake(recognizer.view.frame.origin.x, newY, recognizer.view.frame.size.width, recognizer.view.frame.size.height);
recognizer.view.frame = newFrame;
[recognizer setTranslation:CGPointZero inView:self.view];
}
Also, velocity doesn't seem to apply in your situation, it only complicates things.

Related

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/

How can I animate the road in iPhone Xcode?

I am going to animate the road, simply calling 1 image again and again from top to bottom. Can anyone suggest how this is possible?
You'll want to look at a UIScrollView. Put a UIImageView in it, then use the following code:
-(void)scroll{
CGPoint currentPoint = scrollView.contentOffset;
CGPoint scrollPoint = CGPointMake(currentPoint.x, currentPoint.y + 1);
[scrollView setContentOffset:scrollPoint animated:NO];
if(scrollPoint.y == scrollView.contentSize.height+5)//If we've reached the bottom, reset slightly past top, to give ticker look
{
[scrollView setContentOffset:CGPointMake (0,-125)];
}
}
Run that code with a looping NSTimer and you'll be set :)

Set center point of UIView after CGAffineTransformMakeRotation

I have a UIView that I want the user to be able to drag around the screen.
-(void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint pt = [[touches anyObject] locationInView:self];
CGPoint center = self.center;
CGFloat adjustmentX = pt.x - startLocation.x;
CGFloat adjustmentY = pt.y - startLocation.y;
center.x += adjustmentX;
center.y += adjustmentY;
[self setCenter:center];
}
works perfectly until I apply a transform to the view as follows:
self.transform = CGAffineTransformMakeRotation(....);
However, once the transform has been applied, dragging the UIView results in the view being moved in a exponential spiral effect.
I'm assuming I have to make an correction for the rotation in the touchesMoved method, but so far all attempts have eluded me.
You need to translate your transform to your new coordinates because a transform other than the identity is assigned to your view and moving its center alters the transform's results.
Try this instead of altering your view's center
self.transform = CGAffineTransformTranslate(self.transform, adjustmentX, adjustmentY);

Dragging a view within a view, constraining it within

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).

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 :)