Loop UIScrollView but continue decelerating

2019-02-10 15:25发布

问题:

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.

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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.