views:

1231

answers:

4

I have a scroll view that is the width of the screen but only about 70 pixels high. It contains many 50 x 50 icons (with space around them) that I want the user to be able to choose from. But I always want the scroll view to behave in a paged manner, always stopping with an icon in the exact center.

If the icons were the width of the screen this wouldn't be a problem because the UIScrollView's paging would take care of it. But because my little icons are much less than the content size, it doesn't work.

I've seen this behavior before in an app call AllRecipes. I just don't know how to do it.

Any ideas about how to get paging on a per-icon sized basis to work?

+1  A: 

Take a look at the -scrollView:didEndDragging:willDecelerate: method on UIScrollViewDelegate. Something like:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    int x = scrollView.contentOffset.x;
    int xOff = x % 50;
    if(xOff < 25)
     x -= xOff;
    else
     x += 50 - xOff;

    int halfW = scrollView.contentSize.width / 2; // the width of the whole content view, not just the scroll view
    if(x > halfW)
     x = halfW;

    [scrollView setContentOffset:CGPointMake(x,scrollView.contentOffset.y)];
}

It isn't perfect—last I tried this code I got some ugly behavior (jumping, as I recall) when returning from a rubber-banded scroll. You might be able to avoid that by simply setting the scroll view's bounces property to NO.

Noah Witherspoon
I'll take a look, thanks.
+9  A: 

Try making your scrollview less than the size of the screen (width-wise), but uncheck the "Clip Subviews" checkbox in IB. Then, overlay a transparent, userInteractionEnabled = NO view on top of it (at full width), which overrides hitTest:withEvent: to return your scroll view. That should give you what you're looking for. See this answer for more details.

Ben Gottlieb
I'm not quite sure what you mean, I'll look at the answer you point to.
This is a beautiful solution. I hadn't thought of overriding the hitTest: function and that pretty much cuts out the only downside to this approach. Nicely done.
Ben Gotow
Nice solution! Really helped me out.
DevDevDev
+1  A: 

Since I don't seem to be permitted to comment yet I'll add my comments to Noah's answer here.

I've successfully achieved this by the method that Noah Witherspoon described. I worked around the jumping behavior by simply not calling the setContentOffset: method when the scrollview is past its edges.

         - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
         {      
             // Don't snap when at the edges because that will override the bounce mechanic
             if (self.contentOffset.x < 0 || self.contentOffset.x + self.bounds.size.width > self.contentSize.width)
                 return;

             ...
         }

I also found that I needed implement the -scrollViewWillBeginDecelerating: method in UIScrollViewDelegate to catch all cases.

Jacob Persson
+3  A: 

There is also another solution wich is probably a little bit better than overlaying scroll view with another view and overriding hitTest.

You can subclass UIScrollView and override its pointInside. Scroll view can response for touches outside its frame than. Of course the rest is the same.

@interface PagingScrollView : UIScrollView {

    UIEdgeInsets responseInsets;
}

@property (nonatomic, assign) UIEdgeInsets responseInsets;

@end


@implementation PagingScrollView

@synthesize responseInsets;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint parentLocation = CGPointMake(point.x - self.bounds.origin.x, point.y - self.bounds.origin.y);
    CGRect responseRect = self.frame;
    responseRect.origin.x -= responseInsets.left;
    responseRect.origin.y -= responseInsets.top;
    responseRect.size.width += (responseInsets.left + responseInsets.right);
    responseRect.size.height += (responseInsets.top + responseInsets.bottom);

    return CGRectContainsPoint(responseRect, parentLocation);
}

@end
Split
yes! moreover, if your scrollview's bounds is same with its parent then you can just return yes from the pointInside method. thanks
cocoatoucher
I like this solution, though I have to change parentLocation to "[self convertPoint:point toView:self.superview]" for it to work properly.
amrox