How to know exactly when a UIScrollView's scrolling has stopped?
In short, I need to know exactly when the scrollview stopped scrolling. By 'stopped scrolling', I mean the moment at which it is no longer moving and not being touched.
I've been working on a horizontal UIScrollView subclass (for iOS 4) with selection tabs in it. One of its requirements is that it stops scrolling below a certain speed to allow user interaction more quickly. It should also snap to the start of a tab. In other words, when the user releases the scrollview and its speed is low, it snaps to a position. I've implemented this and it works, but there's a bug in it.
What I have now:
The scrollview is its own delegate. at every call to scrollViewDidScroll:, it refreshes its speed-related variables:
-(void)refreshCurrentSpeed
{
float currentOffset = self.contentOffset.x;
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
deltaOffset = (currentOffset - prevOffset);
deltaTime = (currentTime - prevTime);
currentSpeed = deltaOffset/deltaTime;
prevOffset = currentOffset;
prevTime = currentTime;
NSLog(@"deltaOffset is now %f, deltaTime is now %f and speed is %f",deltaOffset,deltaTime,currentSpeed);
}
Then proceeds to snap if needed:
-(void)snapIfNeeded
{
if(canStopScrolling && currentSpeed <70.0f && currentSpeed>-70.0f)
{
NSLog(@"Stopping with a speed of %f points per second", currentSpeed);
[self stopMoving];
float scrollDistancePastTabStart = fmodf(self.contentOffset.x, (self.frame.size.width/3));
float scrollSnapX = self.contentOffset.x - scrollDistancePastTabStart;
if(scrollDistancePastTabStart > self.frame.size.width/6)
{
scrollSnapX += self.frame.size.width/3;
}
float maxSnapX = self.contentSize.width-self.frame.size.width;
if(scrollSnapX>maxSnapX)
{
scrollSnapX = maxSnapX;
}
[UIView animateWithDuration:0.3
animations:^{self.contentOffset=CGPointMake(scrollSnapX, self.contentOffset.y);}
completion:^(BOOL finished){[self stopMoving];}
];
}
else
{
NSLog(@"Did not stop with a speed of %f points per second", currentSpeed);
}
}
-(void)stopMoving
{
if(self.dragging)
{
[self setContentOffset:CGPointMake(self.contentOffset.x, self.contentOffset.y) animated:NO];
}
canStopScrolling = NO;
}
Here are the delegate methods:
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
canStopScrolling = NO;
[self refreshCurrentSpeed];
}
-(void)scrollViewDidEnd开发者_开发技巧Dragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
canStopScrolling = YES;
NSLog(@"Did end dragging");
[self snapIfNeeded];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self refreshCurrentSpeed];
[self snapIfNeeded];
}
This works well most of the time, except in two scenarios:
1. When the user scrolls without releasing his/her finger and lets go at a near stationary timing right after moving, it often snaps to its position as it's supposed to, but a lot of times, does not. It usually takes a few attempts to get it to happen. Odd values for time (very low) and/or distance (rather high) appear at the release, causing a high speed value while the scrollView is, in reality, nearly or entirely stationary.
2. When the user taps the scrollview to stop its movement, it seems the scrollview sets the contentOffset
to its previous spot. This teleportation results in a very high speed value. This could be fixed by checking if the previous delta is currentDelta*-1, but I'd prefer a more stable solution.
I've tried using didEndDecelerating
, but when the glitch occurs, it does not get called. This probably confirms that it's stationary already. There seems to be no delegate method that gets called when the scrollview stopped moving completely.
If you'd like to see the glitch yourself, here's some code to fill the scrollview with tabs:
@interface UIScrollView <UIScrollViewDelegate>
{
bool canStopScrolling;
float prevOffset;
float deltaOffset; //remembered for debug purposes
NSTimeInterval prevTime;
NSTimeInterval deltaTime; //remembered for debug purposes
float currentSpeed;
}
-(void)stopMoving;
-(void)snapIfNeeded;
-(void)refreshCurrentSpeed;
@end
@implementation TabScrollView
-(id) init
{
self = [super init];
if(self)
{
self.delegate = self;
self.frame = CGRectMake(0.0f,0.0f,320.0f,40.0f);
self.backgroundColor = [UIColor grayColor];
float tabWidth = self.frame.size.width/3;
self.contentSize = CGSizeMake(100*tabWidth, 40.0f);
for(int i=0; i<100;i++)
{
UIView *view = [[UIView alloc] init];
view.frame = CGRectMake(i*tabWidth,0.0f,tabWidth,40.0f);
view.backgroundColor = [UIColor colorWithWhite:(float)(i%2) alpha:1.0f];
[self addSubview:view];
}
}
return self;
}
@end
A shorter version of this question: how to know when the scrollview stopped scrolling? didEndDecelerating:
does not get called when you release it stationary, didEndDragging:
happens a lot during the scrolling and checking for speed is unreliable due to this odd 'jump' which sets the speed to something random.
I found a solution:
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
I did not notice that last bit before, willDecelerate
. It is false when the scrollView is stationary when ending the touch. Combined with the above-mentioned speed check, I can snap both when it's slow (and not being touched) or when it's stationary.
For anyone not doing any snapping but needs to know when scrolling stopped, didEndDecelerating
will be called at the end of the scroll movement. Combined with a check on willDecelerate
in didEndDragging
, you'll know when the scrolling has stopped.
[Edited Answer] This is what I use - it handles all the edge cases. You need an ivar to keep state, and as shown in the comments, there are other ways to handle this.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
//[super scrollViewWillBeginDragging:scrollView]; // pull to refresh
self.isScrolling = YES;
NSLog(@"+scrollViewWillBeginDragging");
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
//[super scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; // pull to refresh
if(!decelerate) {
self.isScrolling = NO;
}
NSLog(@"%@scrollViewDidEndDragging", self.isScrolling ? @"" : @"-");
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
self.isScrolling = NO;
NSLog(@"-scrollViewDidEndDecelerating");
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
self.isScrolling = NO;
NSLog(@"-scrollViewDidScrollToTop");
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
self.isScrolling = NO;
NSLog(@"-scrollViewDidEndScrollingAnimation");
}
I created a really simple project uses the above code, so that when a person interacts with a scrollView (including a WebView), it inhibits process intensive work until the user stops interacting with the scrollView AND the scrollview has stopped scrolling. It's like 50 lines of code: ScrollWatcher
Here's how to combine scrollViewDidEndDecelerating
and scrollViewDidEndDragging:willDecelerate
to perform some operation when scrolling has finished:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self stoppedScrolling];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[self stoppedScrolling];
}
}
- (void)stoppedScrolling
{
// done, do whatever
}
Delegate methods mentioned in this post answers did not help me. I found another answer which detect final scroll end in scrollViewDidScroll:
Link is here
I answered this question on my blog which outlines how to do it without problems.
It involves 'intercepting the delegate' to do a few things, then to pass the delegate messages to where they were intended. This is required because there are a few scenarios where the delegate fires if it moved programmatically, or not, or both, and so anyway, best to just look at this link below:
How to know when a UIScrollView is scrolling
It's a little tricky, but I've put up a recipe there and have explained the parts in detail.
It is important to understand, that the when UIScrollView is stopping to move it triggers two different functions of the UIScrollViewDelegate depending on how strong the user has pushed.
- stopDragging is triggered, when the user stops his drag gesture on the screen. When this was very slowly no more movement will happen after that.
- stopDecelerating is triggered, when the user has pushed the scroll view in a way, so that it is still moving after the user touches up his finger and then comes to an end. This function is not always triggered, it is not triggered, when there is no movement after the user touches up his finger. Then only the function above "stopDragging" is triggered.
So in conclusion it is necessary to combine the two functions like @WayneBurkett already pointed out in his answer.
In Swift 5.X:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
myFunction()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate == false {
myFunction()
}
}
private func myFunction() {
// scrollView did stop moving -> do what ever
// ...
}
Here's my Swift solution which works in all cases known to me. Note: fab is a button that I am showing and hiding.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
hideFAB()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
showFAB()
} else {
hideFAB()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
showFAB()
}
func showFAB() {
fab.isHidden = false
}
func hideFAB() {
fab.isHidden = true
}
精彩评论