How can I set the end dragging velocity of a UICollectionView with custom flow layout? - cocoa-touch

I'm working on a UICollectionView with a custom flow layout subclass which, among other things, does some custom "paging". Everything's fine but for the fact that depending on how I drag, when I release and after - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity gets called, the collection view (or some part of the UICollectionViewFlowLayout which I did not yet know I need to override) is controlling the velocity with which the animation of an item snapping happens.
That is, if I slightly offset an item from the center of the
collection view qnd release, it snaps back to its position pretty
quickly (desired).
But If I drag the item, say, half way past the collection view's
frame and/or change swiping directions while still dragging and then
release, the "snap" animation takes too long (not desired: I'd like the velocity to adjust so that the end drag animation takes the same amount of time always, regardless of distance).
I tried modifying the decelerationRate of the collection view but it doesn't seem to do anything. And I'm thinking of writing my own animation block in one of the collection view delegate methods, but I'm wondering if there is a different way (perhaps from within the flow layout subclass?).

Well, actually setting self.collectionView.decelerationRate = 0.; seems to work for now. It at least does not decelerate the scrolling and so it looks like constant velocity which is not exactly what I wanted but feels almost right.

Related

UITableView w/ paging & momentum

OBSERVATIONS
I've discovered some "weird" things about how UITableViews / UIScrollViews scroll in Objective-C for iOS after you've lifted your finger:
A scroll view's "velocity" (as in scrollViewWillEndDragging:withVelocity:targetContentOffset:) is not based in time but rather based in "render" or "frame refresh" or the like; i.e., a "velocity" of 3.0 means that the scroll view will scroll 3.0 points before the next refresh, at which point it will scroll yet another 3.0 points if the "velocity" is still 3.0. (in contrast to an absolute time-based velocity)
"Refreshes" occur approx. 950 times per second, but this varies and is not dependable. (again, in contrast to an absolute time-based refresh rate)
Because UIScrollViews decelerate exponentially rather than linearly, decelerationRate is a geometric constant. This means that on every "refresh" of the scroll view, its "velocity" is set to its previous velocity times the "deceleration rate".
At some point, the scroll view's velocity is close enough to zero that it stops scrolling. I am not sure what this threshold is yet.
DESIRED BEHAVIOR / PROBLEM
I've derived an equation that can theoretically be solved to adjust the deceleration rate so that the scroll view can scroll naturally with momentum and settle close enough to the top of a table view cell to mimic "paging." (I also have an animation added to scrollViewDidEndDragging:willDecelerate: to adjust the content offset minimally. Adjusting content offset w/o adjust decelerationRate looks weird; the scroll view either jumps ahead or lurches backwards suddenly.)
This equation is: Ad^n + Bd + C = 0 where d = decelerationRate. Ideally I'd like something in the form of d(n, A, B, C), but my math PhD friend tells me that's not deterministically possible. He pointed me towards the following page: http://en.wikipedia.org/wiki/Root-finding_algorithm
I've also found that I can't animate a scroll view to a predetermined content offset using animation style UIViewAnimationCurveEaseOut because setContentOffset:animated: doesn't take an options: parameter. Otherwise I would "fake" scrolling to my precalculated content offset.
(I found this on SO but it didn't work for me: UIScrollview setContentOffset with non linear animation ?)
I'd like to use a table view because all of my elements in my scroll view will be cells essentially, and table views are best for displaying a list of cells. However, based on my difficulty getting my desired behavior, I might just make a paging scrollview, which will probably be easier. I can then just limit the number of "cells" visible in my custom "table view" or else load them post-hoc as necessary.
What are your ideas / thoughts / suggestions?
So far I have not found existing examples of what I would like.
ILLUSTRATION
OR
So this answer was obtained via a snarky but brilliant user on the #iphonedev IRC channel!
From within scrollViewWillEndDragging:withVelocity:targetContentOffset: you can just re-set the targetContentOffset to whatever you desire via *targetContentOffset = newTargetOffset;! Incredibly simple and elegant.
My mistake was trying to call [scrollview setContentOffset:newTargetOffset animated:YES] from within scrollViewWillEndDragging:withVelocity:targetContentOffset:, which didn't work at all, and therefore I set out to "solve" my problem in a more convoluted way that also didn't work at all.

When using UIScrollView, how do I make the viewed ends to be complete, not partial

How do I make the UIScroll view show complete views, not partial views?
(Note) I don't want it jumping to a complete view. It needs to move naturally or at least not immediate... needs to be smooth.
thanks
If your views are all of a constant size and you just want left/right or up/down scrolling, set pagingEnabled on the scroll view to YES. Supposing you wanted your scroll view to be 320x480 but to show the sides of the next and previous pages (so, e.g., each thing inside the view was 280 points wide), you'd size the scroll view to be 280x480 but set masksToBounds to NO.
If you have a more complicated scheme, install a scroll view delegate and act on scrollViewDidScroll:, paying attention to contentOffset. Probably you want to implement logic like:
add an observer on tracking; when it transitions to NO from 'YES' enable your logic inside scrollViewDidScroll: In there:
if a forced scroll is pending, cancel it
calculate where you'd force the scrolling to from the current position
schedule a scroll to there (which you'll effect via setContentOffset:animated:) for half a second from now
You can use a non-repeating NSTimer for the scheduling aspect. The logic you've essentially implemented is that if the user stops adjusting the view, wait for the natural inertia to end (which you'll detect by the 0.5 second gap since last movement), then transition smoothly to the nearest aligned position.
Check out Apple's documentation here first:
https://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/UIScrollView_pg/Introduction/Introduction.html
Then the ScrollViewSuite sample:
https://developer.apple.com/library/ios/#samplecode/ScrollViewSuite/Introduction/Intro.html#//apple_ref/doc/uid/DTS40008904
I think you are referring to Paging techniques.

Best approach for drawing a graph of almost infinite size in a UIScrollView on iPad

I'm currently working on an app which needs to draw s.th. like a network graph. Unfortunately this graph can become very big with thousands of movable objects.
I tried to put a giant UIView inside a UIScrollView but soon noticed this won't work because of memory limitations.
So I tried another approach: currently I have a UIView which has exactly the size of the visible part of the UIScrollView. The scrollview is set to not handle the scrolling (only the pinching). Instead I handle the scrolling in the UIView. Everytime a user scrolls, all graphic objects (those graphic objects are currently just subclasses of NSObject, which contain custom drawing code) are moved, so it seems like the view is scrolling. In the drawRect I only draw the graphics that are currently visible.
Also I constantly add and remove sublayers if they are moved out/in the visible frame
This works very smooth even with thousands of objects.
Unfortunately this approach has some drawbacks:
I can't zoom out to see all the objects in the graph, instead the user can only see a part of it
I don't get the inertial scrolling the UIScrollView offers
Other approaches I tried, like the CATiledLayer, don't work either because all the objects in the graph are draggable by the user and it looks really ugly if I use a CATiledLayer...
Swapping out the UIView with other UIViews while the user scrolls may help with the inertial scrolling, but it makes everything more complicated and zooming out completely still won't work :-(
Do you know of any best practices to draw graphs that can be very big?
//edit: I ended up with a uiscrollview which has a subview which has a cascrolllayer which has many sublayers. While zooming in and out the frame of the uiscrollviews subview is constantly changed to the uiscrollviews bounds by view.frame = scrollview.bounds. While dragging the scrollview the cascrolllayer is always forced to scroll to the current offset of the scrollview.
I needed to subclass the uiscrollview and hack around in order to make the zooming work nicely, but it's working well now. This approach works very well and allows very big graphs with lots of draggable elements.
//edit: see my other answer below, the approach above didn't work out as well as I initially thought, especially the zooming part
CATiledLayer is definitely what you should use here—there’s not really another solution that’ll let you use Quartz/UIKit drawing on a huge zoomable canvas. For anything that needs to be interactive (dragged or animated or whatever), you can disable its display in the main tiled layer and overlay another view or layer on top of it that just contains the object being interacted with.

iPhone UIScrollView, Slow down the scrolling

How do I add some extra drag to the UIScrollView physics. It scrolls just a little too fast for what I am doing. (I don't want to disable altogether, I still like the rubber band effect when you get to the end or beginning of the view.) Is there any way to slow it down?
Actually, after reading the Documentation I discovered the property for UIScrollView that slows down the scrolling, so Apple does make this readily available for anyone else looking for this:
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
Even though it says fast, it is speeding up deceleration which in effect slows the scrolling down. This was exactly what I needed. And no worry of patents. :)
Actually you can make block animation with ScrollToRowAtIndexPath:... with animated set to NO. This way block animation will animate underlaying variables manually using selected time.
As UICollectionView is subclass of UIScrollView You can use scroll view delegate method.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
targetContentOffset->x = (targetContentOffset->x - scrollView.contentOffset.x) / 6 + scrollView.contentOffset.x;
targetContentOffset->y = (targetContentOffset->y - scrollView.contentOffset.y) / 6 + scrollView.contentOffset.y;
}
This is simply not possible right now. Maybe Apple will one day extend the API to allow it, but until that day, you're stuck with the default speed.
The only other option would be your own scrolling implementation. This isn't trivial if you want all the nice polish such as the rubber band effect and the way the scrolling will lock horizontally or vertically. Not to mention flicking to scroll.
Also, Apple has a patent for some of those scrolling behaviours, so writing your own might even be dangerous. Source: "Apple's touch-screen patent".
A computing device, comprising: a touch screen display; one or more processors; memory; and one or more programs, wherein the one or more programs are stored in the memory and configured to be executed by the one or more processors, the one or more programs including: instructions for detecting one or more finger contacts with the touch screen display; instructions for applying one or more heuristics to the one or more finger contacts to determine a command for the device; and instructions for processing the command; wherein the one or more heuristics comprise: a vertical screen scrolling heuristic for determining that the one or more finger contacts correspond to a one-dimensional vertical screen scrolling command rather than a two-dimensional screen translation command based on an angle of initial movement of a finger contact with respect to the touch screen display; a two-dimensional screen translation heuristic for determining that the one or more finger contacts correspond to the two-dimensional screen translation command rather than the one-dimensional vertical screen scrolling command based on the angle of initial movement of the finger contact with respect to the touch screen display; and a next item heuristic for determining that the one or more finger contacts correspond to a command to transition from displaying a respective item in a set of items to displaying a next item in the set of items.
But of course, I am not a lawyer.

How can I scroll a UIView of indefinite size within a UIScrollView

I'm trying to draw a graph that is indefinitely large horizontally, and the same height as the screen. I've added a UIScrollView, and a subclass of a UIView within it, which implements the -drawRect: method. In the simulator, everything works fine, but on the device, it can't seem to draw the graph after it reaches a certain size.
I'm already caching pretty much everything I can, and basically only calling CGContextAddLineToPoint in the -drawRect: section. I'm only drawing what's visible on the screen. I have a delegate to the UIScrollView which listens for -scrollViewDidScroll: which then tells the graph to redraw itself ([graphView setNeedsDisplay]).
I found one method that tells me to override the +layerClass method and return [CATiledLayer class]. This does allow the graph to actually draw on the device, but it functions very poorly. It's incredibly slow to actually draw, and the fade in that occurs is undesirable.
Any suggestions?
Well, here's my answer: I basically did something similar to how the UITableView works with cells: I have an NSMutableSet of GraphView objects which store unused graphs. When a section of the scroll view becomes visible, I take a graph view from that set (or make a new one if the set is empty). It already had a scrollX property to determine which part of it was supposed to draw. I set the scrollX property to the correct value and, instead of using the screen width, I gave it an arbitrary width to draw. When it goes out of the scroll view, it is removed from the UIScrollView and added to the set.
I wonder though if I really even need to remove them when they go outof the view? It may be prudent to try leaving them in and remove the ones not on screen only if I get a low memory warning? This might get rid of the pause whenever it needs to redraw a section of graph that hasn't changed.
My saving grace here was that my GraphView already was set up to draw only a portion of the graph. All I needed to do then was just make more than one of them.
I think this is a limitation of the iPhone graphics hardware. Through experimentation, I have seen that the iPhone will refuse to draw a frame that is bigger than 2000 pixels in either height or width. It probably has something to do with limited size for frame buffers in hardware.
Watch the 2011 WWDC session video entitled "Session 104 - Advanced Scroll View Techniques".
Thanks, that's helpful. One question -- what did you use for the contentSize of the UIScrollView? Does UIScrollView tolerate large content sizes (over 2000 px) as long as you're not creating buffers to fill the entire space in your content view? Or are you keeping the UIScrollView a constant size (say, 2 screen widths) and resting the UIScrollView contentOffset property each time you draw (using scrollX instead of the contentOffset to store your position)?
I think I answered my own question (the latter seems like a better alternative), heh, but I'll go ahead and post this in case other people need clarification.