Loop UIScrollView but continue decelerating - iphone

I've set up an infinite scroll view and when it reaches 0 content offset I set it to max content offset and vice versa.
i.e.
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
This works but it stops the UIScrollView decelerating.
Is there a way to do this but keep the UIScrollView decelerating?
I tried this...
float declerationRate = scrollView.decelerationRate;
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width, 0) animated:NO];
scrollView.decelerationRate = declerationRate;
but it didn't work.
EDIT
I just realised that decelerationRate is the setting to determine how slow/fast the deceleration is.
What I need is to get the current velocity from the scrollView.

Right I had to tweak the idea a bit.
Turns out trying to set the velocity of a UIScrollView is difficult... very difficult.
So anyway, I kind of tweaked it.
This is actually a mini project after answering someone else's SO question and thought I'd try to solve it myself.
I want to create a spinner app that I can swipe to spin an arrow around so it spins and decelerates to a point.
What I did was set up a UIImageView with the arrow pointing up.
Then covering the UIImageView is a UIScrollView.
Then in the code...
#interface MyViewController () <UIScrollViewDelegate>
#property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
#property (nonatomic, weak) IBOutlet UIImageView *arrowView;
#end
#implementation MyViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//make the content size really big so that the targetOffset of the deceleration will never be met.
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * 100, self.scrollView.frame.size.height);
//set the contentOffset of the scroll view to a point in the center of the contentSize.
[self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width * 50, 0) animated:NO];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)rotateImageView
{
//Calculate the percentage of one "frame" that is the current offset.
// each percentage of a "frame" equates to a percentage of 2 PI Rads to rotate
float minOffset = self.scrollView.frame.size.width * 50;
float maxOffset = self.scrollView.frame.size.width * 51;
float offsetDiff = maxOffset - minOffset;
float currentOffset = self.scrollView.contentOffset.x - minOffset;
float percentage = currentOffset / offsetDiff;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI * 2 * percentage);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//the scrollView moved so update the rotation of the image
[self rotateImageView];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
//the scrollview stopped moving.
//set the content offset back to be in the middle
//but make sure to keep the percentage (used above) the same
//this ensures the arrow is pointing in the same direction as it ended decelerating
float diffOffset = scrollView.contentOffset.x;
while (diffOffset >= scrollView.frame.size.width) {
diffOffset -= scrollView.frame.size.width;
}
[scrollView setContentOffset:CGPointMake(scrollView.frame.size.width * 50 + diffOffset, 0) animated:NO];
}
#end
This gives the desired effect of a spinner like on Wheel of Fortune that will spin endlessly in either direction.
There is a flaw though. If the user keeps spinning and spinning without letting it stop it will only go 50 spins in either direction before coming to a stop.

As I said in the comment, the way you're doing is producing an expected result, one way to do what you want is to set the content offset to the top but then by using the content size height and deceleration value, you could animate the content offset again, check out this answer: https://stackoverflow.com/a/6086521/662605
You will have to play around with some math before it feel right but I think this is a reasonable workaround.
The lower the deceleration, the longer the animation (time) and the more it will animate (distance). Let me know what you think.
As you've said, the deceleration is probably not the only thing you need. So you could try KVO on the contentOffset to calculate the mean velocity over half a second perhaps to get an idea of speed.

There are probably a few different ways to do this. Ideally, you won't have to do any type of calculation to simulate the remaining deceleration physics or mess around at all with UIScrollView's internals. It's error-prone, and it's not likely to perfectly match the UIScrollView physics everyone's used to.
Instead, for cases where your scroll view is merely a driver for a gesture (i.e., you don't actually need to display anything in it), I think it's best to just have a massively wide scroll view. Set its initial content offset to the center of its content size. In scrollViewDidScroll(_:), calculate a percentage of the traversed screen width:
let pageWidth = scrollView.frame.width
let percentage = (scrollView.contentOffset.x % pageWidth) / pageWidth
This will basically loop from 0.0 to 1.0 (moving right) or from 1.0 to 0.0 (moving left) over and over. You can forward those normalized values to some other function that can respond to it, perhaps to drive an animation. Just structure whatever code responds to this such that it appears seamless when jumping from 0.0 to 1.0 or from 1.0 to 0.0.
Of course, if you need whatever you're looping to occur faster or slower than the normal scroll view speed, just use a smaller or larger fraction of the screen width. I just picked that arbitrarily.
If you're worried about hitting the edges of the scrollable content, then when the scroll view comes to a complete rest1, reset its content offset to the same initial center value, plus whatever remainder of the screen width (or whatever you're using) the scroll view was at when it stopped scrolling.
Given the resetting approach above, even for scroll views where you're hosting visible content, you can effectively achieve an infinitely scrolling view as long as you update whatever view frames/model values to take into account the reset scroll offset.
1 To properly capture this, implement scrollViewDidEndDecelerating(_:) and scrollViewDidEndDragging(_:willDecelerate:), only calling your "complete rest" function in the latter case when willDecelerate is false. Always call it in the former case.

I found that if you call
[scrollView setContentOffset:CGPointMake(0,0) animated:NO];
at point(0,0), it will trigger bounces action for UIScrollView, then the deceleration will stop. The same situation takes place when you call setContentOffset to bound of the content.
So you can call setContentOffset to point(10,10) or somewhere else to keep the deceleration easily.

Related

Restrict UIScrollView srollable area

I'm working with a UIScrollView to display a large document, I want to restrict the area that the user can view somehow. I have almost achieved the desired result with the following code:
[childView setFrame:CGRectMake(offsetX, offsetY, contentWidth, contentHeight)];
[scrollView setContentSize:CGSizeMake(contentWidth, contentHeight)];
With offsetX and offsetY being negative numbers to move the child view outside of the scrollview area. This works perfectly at zoom level 1.0 but not at any other zoom levels. I have implemented - (void)setZoomScale:(float)zoomScale like this:
- (void)setZoomScale:(float)zoomScale {
[super setZoomScale:zoomScale];
[childView setFrame:CGRectMake(offsetX * zoomScale, offsetY * zoomScale, contentWidth, contentHeight)];
}
But this doesn't work, the offset gradually gets further out the more the view is zoomed. What is the best way to achieve this?
Thanks,
J
Obviously you could fix the minimum and maximum zoom scale to 1.
However, it works for me if I set the contentSize in scrollViewDidZoom:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[scrollView setContentSize:CGSizeMake(1024*scrollView.zoomScale, 1024*scrollView.zoomScale)];
}
btw, make sure you turn bounces off to check accurately:
scrollView.bounces = NO;

How to zoom when scroll UIScrollView

I add few labels in UIScrollView and I want when I scroll, the the middle label font size can become bigger. And the previous middle label font size shrinks to smaller. And the change happens gradually. Like below. Label 1 move to left shrink smaller and label 2 move to middle becomes bigger. All labels in a UIScroll view.
I tried some, like when scroll I tried zoom scroll page, seems complex than I thought...
Anyone has good idea? Thanks!
Pretty simple really. Just stick a UIScrollViewDelegate to the scrollview and do something like this..
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
for (int i = 0; i < [scrollView.subviews count]; i++) {
UIView *v = [scrollView.subviews objectAtIndex:i];
float position = v.center.x - scrollView.contentOffset.x;
float offset = 2.0 - (abs(scrollView.center.x - position) * 1.0) / scrollView.center.x;
v.transform = CGAffineTransformIdentity;
v.transform = CGAffineTransformScale(v.transform, offset, offset);
}
}
But, if you aren't impressed by the affine transform, you could still scale the rect using the offset factor and set the UILabel to adjustsFontSizeToFitWidth... and you are done!
Just make sure there is enough space! Else it could get out of hand very easily.
Assumptions :
There are no other views in the scrollview apart from these labels.
Its required to work just horizontally.
It could be done with CoreAnimation. You have to keep the index of the main label (that one in the center), and after scrolling is done or when scrolling starts (use some proper for you method in UIScrollViewDelegate) and simply shrink side labels by animation.
Just make the size of the font bigger (with an animation block) when it is the middle one, and smaller when it is not.
You can add a category to UILabel with -(BOOL)isMiddle and set it to true/false.

How can I stop UITable from overriding the content offset I set when it is decelerating?

I have a table that I'm doing some special loading for. The user starts scrolled to the bottom. When the user scrolls near the top, I detect this through scroll view delegate methods, and I quickly load some additional content, and populate more of the top part of the list. I want this to look seamless, like an "infinite scroll" upward. To do this, I have to set the content offset, so that the user doesn't see the table "jump" upward. When I scroll slowly, this works perfectly. When I scroll quickly, so that the table is decelerating, the content offset I set is ignored. Here is the code I'm using:
CGFloat oldHeight = self.tableView.contentSize.height;
CGFloat oldOffset = self.tableView.contentOffset.y;
self.tableContentsArray = newTableContentsArray;
[self.tableView reloadData];
CGFloat newHeight = self.tableView.contentSize.height;
CGFloat newOffset = oldOffset + (newHeight - oldHeight);
self.tableView.contentOffset = CGPointMake(0, newOffset);
So if I scroll up quickly with a table 100px high and hit the top while decelerating, I load more data, get a new table height of, say, 250px. I set the offset to 150. However, since it's still decelerating, the Apple code leaves the offset set to 150 for .1 seconds or something, then goes to the next calculated offset for deceleration, and sets the offset to 0, which makes it look to the user like they just skipped 150px of content, and are now at the new top of the list.
Now I'd LOVE to keep the acceleration from the list, so that it keeps going up for a while, slows down, and ends up somewhere around 120px offset, just like you would expect. Question is, how?
If I use [self.tableView setContentOffset: CGPointMake(0, newOffset) animated: NO]; it stops the content offset from being ignored, but stops the list dead.
We had an interesting situation like this at work a few months back. We wanted to use the UITableViewController because of it's caching, loading, and animations, but we wanted it to scroll horizontally (in your case it would be scroll upward). The solution? Rotate the table, then rotate the cells the other direction.
In your case, the code would look like this:
#define degreesToRadian(x) (M_PI * (x) / 180.0) in the header
- (void) viewDidLoad {
[super viewDidLoad];
self.table.transform = CGAffineTransformMakeRotation(degreesToRadian(-180));
...
}
Then rotate the cell, so it appears in the right orientation for the user
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
cell.transform = CGAffineTransformMakeRotation(degreesToRadian(180));
}
Now you can postpend your cells like any normal table, and they'll be added on top instead of the bottom.
I am afraid you are trying to do things too complicated without really understanding them.
Do you really have an infitite table or a very long table? I think it would be possible to just tell the table it has 1000000 cells. Each cell is loaded when you need it. And that's basically what you want to do.

Scrolling speed during UITableView re-ordering mode (not performance related)

Is there a way to increase the speed that you can drag a cell up/down during a table's movable row mode of a UITableView? For instance, there seems to be a standard speed that the table will allow you to drag the cell when you are moving it around and the scroll speed seems to increase if you hold it near the top/bottom edge of the device screen. For a given cell height and a whole bunch of cells, this might take a while to re-order them all if I have to use their standard slow scrolling.
So basically, I would like to increase the speed that it scrolls up/down when you are dragging the cell so it won't take as long to get to where you want to drop the cell in place.
I understand that you can speed up the moving process by decreasing cell height to place more cells on the device screen, but I'd rather do this only if I can't increasing scrolling speed.
Any suggestions or ideas from past experiences with this? Any advice is greatly appreciated!
Here you go. A proof of concept of a speed enhancer of the scroll when you move a cell, SDK 3.1. It may not pass Apple's requirements since overriding _beginReorderingForCell and _endReorderingForCell looks a little off-spec. There are other ways to determine if a cell starts or ends reordering (e.g. subclassing UITableViewCell and finding some measure) but this is the easiest I think.
The approach is quite simple: for every movement of Y pixels down, we move 2*Y pixels down, only when reordering.
The problem is that the currently dragged cell is a subview of the table view, so it shifts with the table view if we move it. If we are to correct for that within this setContentOffset, it has no effect since the position of the cell will be set based on values calculated apart from the current contentOffset. Therefore we correct an instant later using performSelector.
I left the debugging lines in there for convenience. All you need to do is to use FastUITableView instead of UITableView (esp. in you NIB)
You may of course want to add some timing things, so that the speed only goes up after 1 second or so. That will be trivial.
#interface FastUITableView : UITableView
{
UITableViewCell *draggingCell;
CGFloat lastY;
}
#end
#implementation FastUITableView
-(void)_beginReorderingForCell:(UITableViewCell*)cell
{
printf("begin reordering for cell %x\n",cell);
draggingCell = cell;
lastY = -1.0f;
[super _beginReorderingForCell:cell];
}
-(void)_endReorderingForCell:(UITableViewCell*)cell wasCancelled:(BOOL)cancelled animated:(BOOL)animated
{
printf("end reordering for cell %x\n",cell);
draggingCell = nil;
[super _endReorderingForCell:cell wasCancelled:cancelled animated:animated];
}
-(void)setContentOffset:(CGPoint)pt
{
if ( !draggingCell )
{
[super setContentOffset:pt];
return;
}
if ( lastY < 0 ) lastY = pt.y;
CGPoint fast = pt;
float diff = pt.y - lastY;
//diff *= 0.5; /// <<--- control speed here
fast.y = pt.y + diff;
if ( fast.y > self.contentSize.height - self.superview.frame.size.height )
{
CGFloat corr = fast.y - self.contentSize.height + self.superview.frame.size.height;
printf("Correction: %f\n",corr);
fast.y -= corr;
diff -= corr;
} else if ( fast.y < 0.0f ) {
CGFloat corr = -fast.y;
printf("Correction: %f\n",corr);
diff += corr;
fast.y += corr;
}
[self performSelector:#selector(moveCell:) withObject:[NSNumber numberWithFloat:diff] afterDelay:0.01];
lastY = fast.y;
// printf("setting content offset: %f -> %f of max %f\n",pt.y, fast.y, self.contentSize.height);
[super setContentOffset:fast];
}
-(void)moveCell:(NSNumber*)diff
{
CGRect frame = draggingCell.frame;
frame.origin.y += [diff floatValue];
// printf("shifting cell %x with %f\n",draggingCell,[diff floatValue]);
draggingCell.frame = frame;
}
If the list is going to get that long, you may want to consider other reordering mechanisms. For example, my Netflix queue includes a number in each call specifying the order of the queue, 1 for the movie about to ship, 2 for the next and so on to 187 or so for the most recent additions to the queue. I can drag an entry to reorder like on the iPhone, but I can also change the order numbers in the cells to reorder the entries, which is much easer than having to drag #187 to the 10th spot in the queue. There is also a button in each entry to put that entry on the very top.
In your app, you can add extra controls in edit view to assist in reordering such as the order number or "up/down 10" buttons.
If there are common reasons your users would want to order entries, you can have a button that handles that, such as "move up to next nearest blue entry."
From my experience, tableView scroll speed depends on distance between dragged cell and content origin/end points.
So to increase scroll speed, you don't need to subclass UITableView. Instead, you can set contentInset property of a tableView.
For example, call this in viewDidLoad:
tableView.contentInset = UIEdgeInsetsMake(64, 0, 64, 0)
Value 64 assumes that there are statusBar and navBar above tableView, top/bottom constraint between tableView and superView is set to 0 and edgesForExtendedLayout is set to .all.
That tableView goes under navBar and statusBar but because of contentInset it looks like it's attached to navBar.

UIScrollView. Any thoughts on implementing "infinite" scroll/zoom?

So, UITableView supports essentially "infinite" scrolling. There' may be a limit but that sucker can scroll for a looonnnggg time. I would like to mimic this behavior with a UIScrollView but there are two fundamental impediments:
1) scrollView.contentSize is fixed at creation time.
2) zooming can blow any lazy-loading scheme all to hell since it can cause infinte data explosion.
Have others out there pondered this idea? Yah, I know, we are essentially talking about re-creating Google Maps here. Any insights would be much appreciated.
Cheers,
Doug
I've just finished implementing the infitine scroll for me.
In my Implementation I have UITableViewCell with a scrollView and Navigationbuttons. The scrollView contains x views all with the same width. views are alined horizontally and paging is enabled.
scrollView.clipsToBounds = YES;
scrollView.scrollEnabled = YES;
scrollView.pagingEnabled = YES;
scrollView.showsHorizontalScrollIndicator = NO;
My codelogic is like the following:
In my initialization function I
create all the views (for the scrollview) and
put them into an array and
add them to the scrollView
Then I call a function that calculates in a loop the positions for each view (each time you detect a scroll this function will need to be called too). It always takes the first element of the array and sets the frame to (0,0,...,...), the second with (i*width,0,....,....) and so on. The function beeing called looks like this:
- (void)updateOffsetsOfViews{
int xpos = 0;
for (int i=0; i<[views count]; i++) {
UIImageView *_view = [views objectAtIndex:i];
CGRect aFrame = _view.frame;
aFrame.origin.x = xpos;
aFrame.origin.y = 0.0;
_view.frame = aFrame;
xpos += viewWidth;
}
float center = 0;
if(fmod([views count],2) == 1){
center = viewWidth * ([views count]-1)/2;
}else {
center = viewWidth * [views count]/2;
}
[scrollView setContentOffset:CGPointMake(center, 0)];
lastOffset = center;
}
Then (still in the initialization process) I add an observer
[scrollView addObserver:self forKeyPath:#"contentOffset" options:0 context:nil];
so each time something in the scrollView changes I get the (observeValueForKeyPath)-function called, which looks like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
UIImageView *_viewFirst = (UIImageView *)[views objectAtIndex:0];
if ( fmod([scrollView contentOffset].x,viewWidth) == 0.0) {
if ([scrollView contentOffset].x > lastOffset) {
[views removeObjectAtIndex:0];
[views addObject:_viewFirst];
[self updateOffsetsOfViews];
}else if ([scrollView contentOffset].x < lastOffset) {
UIImageView *_viewLast = (UIImageView *)[views lastObject];
[views removeLastObject];
[views insertObject:_viewLast atIndex:0];
[self updateOffsetsOfViews];
}
}
}
And in dealloc or viewDidUnload (depends on how you implement it) don't forget to remove the observer.
[scrollView removeObserver:self forKeyPath:#"contentOffset"];
Hope this helps, you might notice some overhead, but in my implementation I also support like scrolling 5 pages (well... unlimited) at once and autoanimated scrolling etc. so you might see something that could be thrown away.
While it's impossible to have a truly infinite UIScrollView, there are some simple tricks you can use to emulate that behavior.
Handling the fixed contentSize: have some fixed-size view handled by your scroll view, and at launch or instantiation, set the content offset so that you're seeing the middle of the handled view. Then just watch the content offset (using KVO or some other method), and if you near any edge, update the content of the view with a new set of content (offset appropriately) and reset the scroll view's contentOffset property to be back in the middle.
Handling zooming: do something similar, only this time watch the zoom factor on the scroll view. Whenever it gets to a certain point, do some manipulation to whatever data you're presenting so that it appears zoomed, then reset the zoom factor to 1.0. For example, if you're scrolling an image and it gets zoomed to appear twice as large, programmatically apply some kind of transform to make the image twice as large, then reset the scroll view's zoom factor to 1.0. The image will still appear zoomed in, but the scroll view will be able to continue zooming in further as necessary. (Google Maps takes this one step further where it lazy-loads more detailed views as the user zooms - you may or may not choose to implement this.)
The StreetScroller sample project from Apple demonstrates how to perform infinite scrolling in a UIScrollView.
Bear in mind that when the scroll is animated, contentOffset changes many times, not just page by page, but with each step in the animation.
Perhaps setting contentSize to some gigantic value and then moving a limited number of underlying views around to track the view position as in the Tiling sample will do the trick.
To mitigate the possibility of eventually reaching an edge and having to abruptly recenter the view (which cancels any scrolling currently in motion), the view can be recentered when it is stationary, from time to time.
Anyway, that's what I'm about to try.