如何准确知道UIScrollView的滚动何时停止?

29
简而言之,我需要知道滚动视图停止滚动的确切时间。所谓“停止滚动”,是指它不再移动且没有被触摸的那一刻。
我一直在开发一个水平UIScrollView子类(适用于iOS 4),其中包含选择标签。其中一个要求是,它会在低于某个速度的情况下停止滚动,以便更快地进行用户交互。它还应该捕捉到标签的开始位置。换句话说,当用户释放滚动视图并且其速度很慢时,它会捕捉到一个位置。我已经实现了这一点,并且它可以工作,但是有一个错误。
我的做法:
滚动视图是它自己的代理。在每次调用scrollViewDidScroll:时,它会刷新与速度相关的变量:
-(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);
}

然后如有需要,继续执行“捕获”操作:

-(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;
}

这里是委托方法:

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    canStopScrolling = NO;
    [self refreshCurrentSpeed];
}

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    canStopScrolling = YES;
    NSLog(@"Did end dragging");
    [self snapIfNeeded];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [self refreshCurrentSpeed];
    [self snapIfNeeded];
}
这通常运作良好,但在以下两个场景中会出现问题: 1. 当用户滚动时不松开手指,并在移动后几乎马上松开手指时,它通常会像预期的那样回到位置,但很多时候却没有。在释放时出现奇怪的时间值(非常低)和/或距离值(相当高),导致scrollView的实际静止或完全静止时速度值非常高。 2. 当用户点击scrollview停止其移动时,scrollview似乎会将contentOffset设置为其之前的位置。这种瞬间移动会导致非常高的速度值。可以通过检查先前的增量是否为currentDelta * -1来解决此问题,但我更喜欢更稳定的解决方案。
我尝试使用didEndDecelerating,但在出现故障时,它不会被调用。这可能证实它已经静止了。似乎没有委托方法在滚动视图完全停止移动时被调用。
如果您想自己看到故障,请使用以下代码填充scrollview。
@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

这个问题的简短版本是:如何知道滚动视图停止滚动?当您静止释放时,didEndDecelerating:不会被调用,didEndDragging:在滚动过程中经常发生,并且由于这种奇怪的“跳跃”,检查速度是不可靠的,它会将速度设置为随机值。


你能否将 didEndDragging 与触摸事件结合起来,这样当用户触摸视图时,你设置一个标志并在他们移开手指时重置。然后只要标志被设置,就不会运行 didEndDragging: 下的相关逻辑。 - FreeAsInBeer
谢谢您的回复。然而,我不确定您提到的标志应该是关于什么的。在我看来,您的意思是当滚动视图被释放一次时,标志会显示出来,但考虑到用户在滚动时多次释放滚动视图,或者只在静止点上没有抬起手指时,我无法想象这有什么用处。 - Aberrant
7个回答

36

我找到了一个解决方案:

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

我之前没有注意到最后那一部分,willDecelerate。当scrollView在结束触摸时保持静止时,它的值为false。结合上述速度检查,我既可以在速度较慢时(且没有被触摸)进行捕捉,也可以在静止时进行捕捉。

对于不进行任何捕捉但需要知道滚动停止时间的人,didEndDecelerating将在滚动结束时调用。结合在didEndDragging中对willDecelerate的检查,您将知道滚动何时停止。


当scrollView的pagingEnabled为Yes时,这是否也有效?这是关于我的问题的帖子。http://stackoverflow.com/questions/9337996/objective-c-uiscrollview-pagingenabled-when-next-page-enters-start-the-anima - SeongHo
@SeongHo 我非常确定它可以。但是我目前无法检查,而且很长一段时间内都不行。 - Aberrant

22

[编辑后的答案] 这是我使用的方法——它可以处理所有边缘情况。你需要一个实例变量来保持状态,并且如评论所示,还有其他处理此问题的方法。

- (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");
}

我使用上述代码创建了一个非常简单的项目,当用户与scrollView(包括WebView)交互时,它会抑制处理密集型工作,直到用户停止与scrollView交互以及scrollView停止滚动。这只需要50行代码:ScrollWatcher


2
这位先生,这是我见过的最干净的解决问题的方法。谢谢。 - thomax
针对您的编辑,您可能会喜欢这个链接:https://github.com/lmirosevic/GBToolbox/commit/87be97a5eff702ef9ef0361fcb0539f36eb4984f。您只需调用 ExecuteAfterScrolling(^{ NSLog(@"heavy work"); });,无需烦恼于布尔状态和条件检查的代码混乱。 - lmirosevic
@lms,你能解释一下为什么它能工作吗?看起来这个方法只是在NSTimer中运行。 - xi.lin
@xi.lin 我发布的代码中没有NSTImer。它将各种委托方法组合成一个连贯的组,以确保在用户通过任何触摸与滚动视图进行交互或滚动视图在快速滑动后滚动时,一定会指示此情况发生。当它指示scrollView未滚动时,它不会移动也不会被触摸。赶快获取链接项目并自行查看。 - David H
@DavidH 感谢您的解决方案~ 但我正在询问Ims关于GBToolbox的方法,似乎使用了NSTimer。 - xi.lin

21
以下是如何结合scrollViewDidEndDeceleratingscrollViewDidEndDragging:willDecelerate来在滚动结束时执行某些操作的方法:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView 
                  willDecelerate:(BOOL)decelerate
{
    if (!decelerate) {
        [self stoppedScrolling];
    }
}

- (void)stoppedScrolling
{
    // done, do whatever
}

2

这篇文章提到的委托方法并没有帮助我解决问题。我找到了另一种方法,在 scrollViewDidScroll: 中检测最终滚动结束。链接在这里


1

我在我的博客上回答了这个问题,概述了如何无错误地完成它。

它涉及“截取代理”来执行一些操作,然后将代理消息传递到它们预期的位置。这是必需的,因为有几种情况下,如果它在编程中移动或不移动,则代理会触发,所以最好只查看下面的链接:

如何知道UIScrollView正在滚动

这有点棘手,但我已经在那里放了一个配方,并详细解释了各个部分。


1
重要的是要理解,当UIScrollView停止移动时,它会触发UIScrollViewDelegate的两个不同函数,具体取决于用户的推力大小。
1. 当用户在屏幕上停止拖动手势时,将触发stopDragging。如果这很慢,之后就不会再有移动了。
2. 当用户以一种使滚动视图在他松开手指后仍然移动的方式推动时,将触发stopDecelerating。当用户松开手指后没有移动时,不会始终触发此功能。那么只有上面的“stopDragging”功能被触发。
因此,总结起来,需要像@WayneBurkett在他的回答中指出的那样,结合这两个功能
在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
    // ...
}

0
这是我的Swift解决方案,适用于我所知道的所有情况。注意:fab是一个我正在显示和隐藏的按钮。
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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接