【问题标题】:How to know exactly when a UIScrollView's scrolling has stopped?如何准确知道 ScrollView 滚动何时停止?
【发布时间】:2011-12-02 07:04:03
【问题描述】:

简而言之,我需要确切地知道滚动视图何时停止滚动。我所说的“停止滚动”是指它不再移动且不再被触摸的那一刻。

我一直在研究一个带有选择选项卡的水平 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.当用户在不松开手指的情况下滚动并在移动后立即以接近静止的时间松开时,它通常会按其应有的位置对齐,但很多时候不会。通常需要几次尝试才能实现。时间(非常低)和/或距离(相当高)的奇数值出现在发布时,导致高速值,而实际上滚动视图几乎或完全静止。 2. 当用户点击滚动视图停止其移动时,滚动视图似乎将contentOffset 设置为之前的位置。这种瞬移导致非常高的速度值。这可以通过检查之前的增量是否为 currentDelta*-1 来解决,但我更喜欢更稳定的解决方案。

我尝试过使用didEndDecelerating,但是当故障发生时,它不会被调用。这可能证实它已经是静止的。当滚动视图完全停止移动时,似乎没有调用委托方法。

如果您想亲自查看故障,这里有一些代码可以用标签填充滚动视图:

@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: 下运行相关逻辑。
  • 感谢您的回复。但是,我不确定您提到的标志应该是什么。在我看来,您的意思是当滚动视图被释放一次时,标志会显示,但考虑到用户在滚动时多次释放滚动视图,或者仅在静止点而没有抬起他/她的手指。

标签: objective-c ios uiscrollview


【解决方案1】:

这是我的 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
}

【讨论】:

    【解决方案2】:

    理解这一点很重要,当 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
        // ...
    }
    

    【讨论】:

      【解决方案3】:

      我找到了解决办法:

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

      我之前没有注意到最后一点,willDecelerate。结束触摸时滚动视图静止时为假。结合上面提到的速度检查,我可以在它很慢(并且没有被触摸)或静止时捕捉它。

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

      【讨论】:

      • 当 scrollView 的 pagingEnabled 为 Yes 时这是否也有效?这是我关于我的问题的帖子。 stackoverflow.com/questions/9337996/…
      • @SeongHo 我很确定确实如此。不过,我目前无法检查它,而且很长一段时间内都不会。
      【解决方案4】:

      我在我的博客上回答了这个问题,其中概述了如何毫无问题地做到这一点。

      它涉及“拦截委托”以做一些事情,然后将委托消息传递到它们的预期位置。这是必需的,因为在某些情况下,如果委托以编程方式移动或不以编程方式移动,或两者兼而有之,则委托会触发,所以无论如何,最好只查看下面的此链接:

      How to know when a UIScrollView is scrolling

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

      【讨论】:

        【解决方案5】:

        下面是如何结合scrollViewDidEndDeceleratingscrollViewDidEndDragging:willDecelerate在滚动完成时执行一些操作:

        - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
        {
            [self stoppedScrolling];
        }
        
        - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView 
                          willDecelerate:(BOOL)decelerate
        {
            if (!decelerate) {
                [self stoppedScrolling];
            }
        }
        
        - (void)stoppedScrolling
        {
            // done, do whatever
        }
        

        【讨论】:

          【解决方案6】:

          [编辑的答案] 这就是我使用的 - 它处理所有边缘情况。您需要一个 ivar 来保持状态,如 cmets 所示,还有其他方法可以处理此问题。

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

          我使用上面的代码创建了一个非常简单的项目,因此当一个人与滚动视图(包括 WebView)交互时,它会抑制进程密集型工作,直到用户停止与滚动视图交互并且滚动视图停止滚动。好像50行代码:ScrollWatcher

          【讨论】:

          • 这,好先生,是我所见过的手头问题的最干净的解决方案。谢谢。
          • 响应您的编辑,您可能会喜欢这样:github.com/lmirosevic/GBToolbox/commit/… 您只需调用ExecuteAfterScrolling(^{ NSLog(@"heavy work"); });,无需处理布尔状态并使用条件检查乱扔代码。
          • @lms 你能解释一下它为什么有效吗?似乎该方法只是在 NSTimer 中运行
          • @xi.lin 我发布的代码中没有 NSTImer。它的作用是将各种委托方法组合成一个有凝聚力的组,当用户通过任何类型的触摸与滚动视图交互时,或者滚动视图在轻弹后滚动时,它肯定会指示。当它指示 scrollView 没有滚动时,它没有移动并且没有被触摸。抓住链接的项目,亲自看看。
          • @DavidH 感谢您的解决方案〜但我正在向 Ims 询问 GBToolbox 的方式,它似乎使用 NSTimer。
          【解决方案7】:

          这篇文章答案中提到的委托方法对我没有帮助。我在scrollViewDidScroll: 中找到了另一个检测最终滚动结束的答案,链接是here

          【讨论】:

            猜你喜欢
            • 2015-12-07
            • 1970-01-01
            • 2012-01-01
            • 2011-06-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多