【问题标题】:How to detect when a UIScrollView has finished scrolling如何检测 UIScrollView 何时完成滚动
【发布时间】:2010-11-02 20:35:36
【问题描述】:

UIScrollViewDelegate 有两个委托方法scrollViewDidScroll:scrollViewDidEndScrollingAnimation:,但它们都不会告诉你滚动何时完成。 scrollViewDidScroll 只通知您滚动视图确实滚动了,而不是它已经完成滚动。

另一种方法scrollViewDidEndScrollingAnimation 似乎仅在您以编程方式移动滚动视图时才会触发,而不是在用户滚动时触发。

有谁知道检测滚动视图何时完成滚动的方案?

【问题讨论】:

标签: ios iphone uiscrollview


【解决方案1】:

RXSwift 版本

commentsTableView.rx.didEndScrollingAnimation
    .bind(
        onNext: { _ in
            debugPrint(":DEBUG:", "didEndScrollingAnimation")
        }
    )
    .disposed(by: disposeBag)

【讨论】:

    【解决方案2】:
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        scrollingFinished(scrollView: scrollView)
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if decelerate {
            //didEndDecelerating will be called for sure
            return
        }
        scrollingFinished(scrollView: scrollView)        
    }
    
    func scrollingFinished(scrollView: UIScrollView) {
       // Your code
    }
    

    【讨论】:

      【解决方案3】:

      在一些早期的 iO​​S 版本(如 iOS 9、10)上,如果滚动视图突然被触摸停止,scrollViewDidEndDecelerating 将不会被触发。

      但在当前版本(iOS 13)中,scrollViewDidEndDecelerating 肯定会被触发(据我所知)。

      因此,如果您的应用也针对早期版本,您可能需要一种解决方法,例如 Ashley Smart 提到的解决方法,或者您可以使用以下解决方法。

      
          func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
              if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating { // 1
                  scrollViewDidEndScrolling(scrollView)
              }
          }
      
          func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
              if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating { // 2
                  scrollViewDidEndScrolling(scrollView)
              }
          }
      
          func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
              // Do something here
          }
      

      说明

      UIScrollView 会通过三种方式停止:
      - 快速滚动并自行停止
      - 通过手指触摸快速滚动和停止(如紧急制动)
      - 慢慢滚动并停止

      第一个可以通过scrollViewDidEndDecelerating和其他类似的方法检测到,而其他两个不能。

      幸运的是,UIScrollView 有三种状态可以用来识别它们,分别用在“//1”和“//2”注释的两行中。

      【讨论】:

      【解决方案4】:

      使用 Ashley Smart 逻辑并正在转换为 Swift 4.0 及更高版本。

          func scrollViewDidScroll(_ scrollView: UIScrollView) {
               NSObject.cancelPreviousPerformRequests(withTarget: self)
              perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation(_:)), with: scrollView, afterDelay: 0.3)
          }
      
          func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
              NSObject.cancelPreviousPerformRequests(withTarget: self)
          }
      

      上面的逻辑解决了用户滚动离开表格视图等问题。如果没有逻辑,当您滚动出 tableview 时,didEnd 将被调用,但它不会执行任何操作。目前在 2020 年使用它。

      【讨论】:

        【解决方案5】:
        - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
            [self stoppedScrolling];
        }
        
        - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
            if (!decelerate) {
                [self stoppedScrolling];
            }
        }
        
        - (void)stoppedScrolling {
            // ...
        }
        

        【讨论】:

        • 它看起来只有在减速时才有效。如果您缓慢平移,然后释放,它不会调用 didEndDecelerating。
        • 虽然是官方API。它实际上并不总是像我们期望的那样工作。 @Ashley Smart 给出了更实用的解决方案。
        • 完美运行,解释有道理
        • 这仅适用于由于拖动交互而导致的滚动。如果您的滚动是由于其他原因引起的(例如键盘打开或键盘关闭),您似乎必须通过 hack 检测事件,并且 scrollViewDidEndScrollingAnimation 也没有用。
        【解决方案6】:

        如果你喜欢 Rx,你可以像这样扩展 UIScrollView:

        import RxSwift
        import RxCocoa
        
        extension Reactive where Base: UIScrollView {
            public var didEndScrolling: ControlEvent<Void> {
                let source = Observable
                    .merge([base.rx.didEndDragging.map { !$0 },
                            base.rx.didEndDecelerating.mapTo(true)])
                    .filter { $0 }
                    .mapTo(())
                return ControlEvent(events: source)
            }
        }
        

        这将允许您这样做:

        scrollView.rx.didEndScrolling.subscribe(onNext: {
            // Do what needs to be done here
        })
        

        这将考虑拖动和减速。

        【讨论】:

        • 这正是我想要的。谢谢!
        【解决方案7】:

        另一种方法是使用scrollViewWillEndDragging:withVelocity:targetContentOffset,每当用户抬起手指并包含滚动停止的目标内容偏移量时都会调用它。在scrollViewDidScroll: 中使用此内容偏移量可以正确识别滚动视图何时停止滚动。

        private var targetY: CGFloat?
        public func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                                              withVelocity velocity: CGPoint,
                                              targetContentOffset: UnsafeMutablePointer<CGPoint>) {
               targetY = targetContentOffset.pointee.y
        }
        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if (scrollView.contentOffset.y == targetY) {
                print("finished scrolling")
            }
        

        【讨论】:

          【解决方案8】:

          320 implementations 好多了 - 这里有一个补丁可以让滚动的开始/结束保持一致。

          -(void)scrollViewDidScroll:(UIScrollView *)sender 
          {   
          [NSObject cancelPreviousPerformRequestsWithTarget:self];
              //ensure that the end of scroll is fired.
              [self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:sender afterDelay:0.3]; 
          
          ...
          }
          
          -(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
          {
              [NSObject cancelPreviousPerformRequestsWithTarget:self];
          ...
          }
          

          【讨论】:

          • 对于我的滚动问题,SO 唯一有用的答案。谢谢!
          • 应该是 [self performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:sender afterDelay:0.3]
          • 2015年,这仍然是最好的解决方案。
          • 我真的不敢相信我在 2016 年 2 月,iOS 9.3,这仍然是这个问题的最佳解决方案。谢谢,像魅力一样工作
          • 你救了我。最佳解决方案。
          【解决方案9】:

          刚刚开发的解决方案可检测何时在应用范围内完成滚动: https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

          它基于跟踪runloop模式变化的想法。并至少在滚动后 0.2 秒后执行块。

          这是iOS10+跟踪runloop模式变化的核心思想:

          - (void)tick {
              [[NSRunLoop mainRunLoop] performInModes:@[ UITrackingRunLoopMode ] block:^{
                  [self tock];
              }];
          }
          
          - (void)tock {
              self.runLoopModeWasUITrackingAgain = YES;
              [[NSRunLoop mainRunLoop] performInModes:@[ NSDefaultRunLoopMode ] block:^{
                  [self tick];
              }];
          }
          

          以及针对 iOS2+ 等低部署目标的解决方案:

          - (void)tick {
              [[NSRunLoop mainRunLoop] performSelector:@selector(tock) target:self argument:nil order:0 modes:@[ UITrackingRunLoopMode ]];
          }
          
          - (void)tock {
              self.runLoopModeWasUITrackingAgain = YES;
              [[NSRunLoop mainRunLoop] performSelector:@selector(tick) target:self argument:nil order:0 modes:@[ NSDefaultRunLoopMode ]];
          }
          

          【讨论】:

          • @IsaacCarolWeisberg 您可以延迟繁重的操作,直到平滑滚动完成以保持平滑。
          • 繁重的操作,你说......我在全局并发调度队列中执行的那些,可能
          【解决方案10】:

          有一个UIScrollViewDelegate 的方法可以用来检测(或者更好的说是“预测”)滚动是否真正完成:

          func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
          

          UIScrollViewDelegate 中的一个,可用于检测(或者更好地说是“预测”)何时真正完成滚动。

          在我的例子中,我将它与水平滚动一起使用,如下所示(在 Swift 3 中):

          func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
              perform(#selector(self.actionOnFinishedScrolling), with: nil, afterDelay: Double(velocity.x))
          }
          func actionOnFinishedScrolling() {
              print("scrolling is finished")
              // do what you need
          }
          

          【讨论】:

            【解决方案11】:

            如果有人需要,这里是 Ashley Smart 的 Swift 答案

            func scrollViewDidScroll(_ scrollView: UIScrollView) {
                    NSObject.cancelPreviousPerformRequests(withTarget: self)
                    perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
                ...
            }
            
            func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
                    NSObject.cancelPreviousPerformRequests(withTarget: self)
                ...
            }
            

            【讨论】:

              【解决方案12】:

              UIScrollview 有一个委托方法

              - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
              

              在委托方法中添加以下代码行

              - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
              {
              CGSize scrollview_content=scrollView.contentSize;
              CGPoint scrollview_offset=scrollView.contentOffset;
              CGFloat size=scrollview_content.width;
              CGFloat x=scrollview_offset.x;
              if ((size-self.view.frame.size.width)==x) {
                  //You have reached last page
              }
              }
              

              【讨论】:

                【解决方案13】:

                已接受答案的 Swift 版本:

                func scrollViewDidScroll(scrollView: UIScrollView) {
                     // example code
                }
                func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
                        // example code
                }
                func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView!, atScale scale: CGFloat) {
                      // example code
                }
                

                【讨论】:

                  【解决方案14】:

                  对于所有与拖动交互相关的滚动,这就足够了:

                  - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
                      _isScrolling = NO;
                  }
                  
                  - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
                      if (!decelerate) {
                          _isScrolling = NO;
                      }
                  }
                  

                  现在,如果您的滚动是由于程序化的 setContentOffset/scrollRectVisible 引起的(animated = YES 或者您显然知道滚动何时结束):

                   - (void)scrollViewDidEndScrollingAnimation {
                       _isScrolling = NO;
                  }
                  

                  如果您的滚动是由其他原因引起的(例如键盘打开或键盘关闭),您似乎必须通过 hack 检测事件,因为 scrollViewDidEndScrollingAnimation 也没有用。

                  PAGINATED滚动视图的例子:

                  因为,我猜,Apple 应用了加速曲线,每次拖动都会调用 scrollViewDidEndDecelerating,因此在这种情况下无需使用 scrollViewDidEndDragging

                  【讨论】:

                  • 您的分页滚动视图案例有一个问题:如果您在页面结束位置完全释放滚动(不需要滚动时),scrollViewDidEndDecelerating 将不会被调用
                  【解决方案15】:

                  我有一个点击和拖动动作的案例,我发现拖动是调用scrollViewDidEndDecelerating

                  并且使用代码手动更改偏移量 ([_scrollView setContentOffset:contentOffset animated:YES];) 正在调用 scrollViewDidEndScrollingAnimation。

                  //This delegate method is called when the dragging scrolling happens, but no when the     tapping
                  - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
                  {
                      //do whatever you want to happen when the scroll is done
                  }
                  
                  //This delegate method is called when the tapping scrolling happens, but no when the  dragging
                  -(void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
                  {
                       //do whatever you want to happen when the scroll is done
                  }
                  

                  【讨论】:

                    【解决方案16】:

                    这已在其他一些答案中进行了描述,但这里(在代码中)如何结合 scrollViewDidEndDeceleratingscrollViewDidEndDragging:willDecelerate 在滚动完成时执行一些操作:

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

                    【讨论】:

                      【解决方案17】:

                      回顾一下(和新手)。这不是那么痛苦。只需添加协议,然后添加您需要检测的功能。

                      在包含 UIScrollView 的视图(类)中,添加协议,然后将此处的任何功能添加到您的视图(类)中。

                      // --------------------------------
                      // In the "h" file:
                      // --------------------------------
                      @interface myViewClass : UIViewController  <UIScrollViewDelegate> // <-- Adding the protocol here
                      
                      // Scroll view
                      @property (nonatomic, retain) UIScrollView *myScrollView;
                      @property (nonatomic, assign) BOOL isScrolling;
                      
                      // Protocol functions
                      - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
                      - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
                      - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView;
                      
                      
                      // --------------------------------
                      // In the "m" file:
                      // --------------------------------
                      @implementation BlockerViewController
                      
                      - (void)viewDidLoad {
                          CGRect scrollRect = self.view.frame; // Same size as this view
                          self.myScrollView = [[UIScrollView alloc] initWithFrame:scrollRect];
                          self.myScrollView.delegate = self;
                          self.myScrollView.contentSize = CGSizeMake(scrollRect.size.width, scrollRect.size.height);
                          self.myScrollView.contentInset = UIEdgeInsetsMake(0.0,22.0,0.0,22.0);
                          // Allow dragging button to display outside the boundaries
                          self.myScrollView.clipsToBounds = NO;
                          // Prevent buttons from activating scroller:
                          self.myScrollView.canCancelContentTouches = NO;
                          self.myScrollView.delaysContentTouches = NO;
                          [self.myScrollView setBackgroundColor:[UIColor darkGrayColor]];
                          [self.view addSubview:self.myScrollView];
                      
                          // Add stuff to scrollview
                          UIImage *myImage = [UIImage imageNamed:@"foo.png"];
                          [self.myScrollView addSubview:myImage];
                      }
                      
                      // Protocol functions
                      - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
                          NSLog(@"start drag");
                          _isScrolling = YES;
                      }
                      
                      - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
                          NSLog(@"end decel");
                          _isScrolling = NO;
                      }
                      
                      - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
                          NSLog(@"end dragging");
                          if (!decelerate) {
                             _isScrolling = NO;
                          }
                      }
                      
                      // All of the available functions are here:
                      // https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollViewDelegate_Protocol/Reference/UIScrollViewDelegate.html
                      

                      【讨论】:

                        【解决方案18】:

                        我刚刚发现了这个问题,这和我问的差不多: How to know exactly when a UIScrollView's scrolling has stopped?

                        虽然 didEndDecelerating 在滚动时有效,但固定释放时的平移不会注册。

                        我最终找到了解决方案。 didEndDragging 有一个参数 WillDecelerate,在静止释放情况下为 false。

                        通过在 DidEndDragging 中检查 !decelerate 并结合 didEndDecelerating,您可以得到滚动结束的两种情况。

                        【讨论】:

                          【解决方案19】:

                          我已经尝试过 Ashley Smart 的回答,它就像一个魅力。这是另一个想法,仅使用 scrollViewDidScroll

                          -(void)scrollViewDidScroll:(UIScrollView *)sender 
                          {   
                              if(self.scrollView_Result.contentOffset.x == self.scrollView_Result.frame.size.width)       {
                              // You have reached page 1
                              }
                          }
                          

                          我只有两页,所以它对我有用。但是,如果您有多个页面,则可能会出现问题(您可以检查当前偏移量是否是宽度的倍数,但是您将不知道用户是停在第二页还是正在前往第三页或更多)

                          【讨论】:

                            【解决方案20】:

                            我认为 scrollViewDidEndDecelerating 是您想要的。它的 UIScrollViewDelegates 可选方法:

                            - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
                            

                            告诉代理滚动视图已经结束了滚动运动的减速。

                            UIScrollViewDelegate documentation

                            【讨论】:

                            • 呃,我给出了同样的答案。 :)
                            • 多哈。有点神秘的名字,但它正是我想要的。应该更好地阅读文档。这是漫长的一天......
                            • 这一班轮解决了我的问题。快给我解决!谢谢。
                            猜你喜欢
                            • 2015-08-15
                            • 2019-02-09
                            • 2018-11-22
                            • 1970-01-01
                            • 2021-03-11
                            • 1970-01-01
                            • 2011-04-17
                            • 1970-01-01
                            • 2018-04-09
                            相关资源
                            最近更新 更多