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.
How do I get paging on a per-icon sized basis to work?
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.
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. Then scroll view can respond for touches outside its frame. Of course the rest is the same.
I see a lot of solutions, but they are very complex. A much easier way to have small pages but still keep all area scrollable, is to make the scroll smaller and move the
scrollView.panGestureRecognizer
to your parent view. These are the steps:Reduce your scrollView size
Make sure your scroll view is paginated and does not clip subview
In code, move the scrollview pan gesture to the parent container view that is full width:
The accepted answer is very good, but it will only work for the
UIScrollView
class, and none of its descendants. For instance if you have lots of views and convert to aUICollectionView
, you will not be able to use this method, because the collection view will remove views that it thinks are "not visible" (so even though they aren't clipped, they will disappear).The comment about that mentions
scrollViewWillEndDragging:withVelocity:targetContentOffset:
is, in my opinion, the correct answer.What you can do is, inside this delegate method you calculate the current page/index. Then you decide whether the velocity and target offset merit a "next page" movement. You can get pretty close to the
pagingEnabled
behavior.note: I'm usually a RubyMotion dev these days, so someone please proof this Obj-C code for correctness. Sorry for the mix of camelCase and snake_case, I copy&pasted much of this code.
kPageWidth is the width you want your page to be. kPageOffset is if you don't want the cells to be left aligned (i.e. if you want them to be center aligned, set this to half the width of your cell). Otherwise, it should be zero.
This will also only allow scrolling one page at a time.
Take a look at the
-scrollView:didEndDragging:willDecelerate:
method onUIScrollViewDelegate
. Something like: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 toNO
.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.I also found that I needed implement the
-scrollViewWillBeginDecelerating:
method inUIScrollViewDelegate
to catch all cases.I tried out the solution above that overlayed a transparent view with pointInside:withEvent: overridden. This worked pretty well for me, but broke down for certain cases - see my comment. I ended up just implementing the paging myself with a combination of scrollViewDidScroll to track the current page index and scrollViewWillEndDragging:withVelocity:targetContentOffset and scrollViewDidEndDragging:willDecelerate to snap to the appropriate page. Note, the will-end method is only available iOS5+, but is pretty sweet for targeting a particular offset if the velocity != 0. Specifically, you can tell the caller where you want the scroll view to land with animation if there's velocity in a particular direction.
When creating the scrollview, make sure you set this:
Then add your subviews to the scroller at an offset equal to their index * height of the scroller. This is for a vertical scroller:
If you run it now the views are spaced out, and with paging enabled they scroll on one at a time.
So then put this in your viewDidScroll method:
The frames of the subviews are still spaced out, we're just moving them together via a transform as the user scrolls.
Also, you have access to the variable p above, which you can use for other things, like alpha or transforms within the subviews. When p == 1, that page is fully being shown, or rather it tends towards 1.
Try use the contentInset property of the scrollView:
It may take some playing around with your values to achieve desired paging.
I have found this to work more cleanly compared to alternatives posted. Problem with using
scrollViewWillEndDragging:
delegate method is the acceleration for slow flicks is not natural.This is the only real solution to the problem.
Here is my answer. In my example, a
collectionView
which has a section header is the scrollView that we want to make it has customisPagingEnabled
effect, and cell's height is a constant value.Refined Swift version of the
UICollectionView
solution:Old thread, but worth mentioning my take on this:
This way you can a) use the entire width of the scrollview to pan / swipe and b) be able to interact with the elements that are out of the scrollview's original bounds
For the UICollectionView issue (which for me was a UITableViewCell of a collection of horizontally scrolling cards with "tickers" of the upcoming / prior card), I just had to give up on using Apple's native paging. Damien's github solution worked awesomely for me. You can tweak the tickler size by upping the header width and dynamically sizing it to zero when at the first index so you don't end up with a large blank margin