如何检测UIScrollView何时完成滚动


145

UIScrollViewDelegate有两个委托方法scrollViewDidScroll:scrollViewDidEndScrollingAnimation:但是在滚动完成时,这两个都不告诉您。scrollViewDidScroll仅通知您滚动视图已滚动,而不是已完成滚动。

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

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


另请参见,如果要在以编程方式滚动后要检测完成的滚动,请执行以下操作:stackoverflow.com/questions/2358046/…–
Trevor

Answers:


157
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self stoppedScrolling];
}

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

- (void)stoppedScrolling {
    // ...
}

13
看起来这只有在减速时才起作用。如果平移缓慢,然后松开,则不会调用didEndDecelerating。
肖恩·克拉克·赫斯

4
虽然这是官方的API。实际上,它并不总是像我们期望的那样工作。@Ashley Smart提供了更实用的解决方案。
Di Wu

完美的作品,而且解释很合理
Steven Elliott

这仅适用于由于拖动交互而导致的滚动。如果您的滚动是由于其他原因(例如键盘打开或键盘关闭)引起的,则似乎您必须通过hack检测到该事件,并且scrollViewDidEndScrollingAnimation也不起作用。
Aurelien Porte

176

320点的实现是好多了-这里是一个补丁来获得滚动一致的开始/结束。

-(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上关于滚动问题的唯一有用答案。谢谢!
Di Wu

4
应该是[self performSelector:@selector(scrollViewDidEndScrollingAnimation :) withObject:sender afterDelay:0.3]
Brainware

1
在2015年,这仍然是最好的解决方案。
卢卡斯2015年

2
老实说,我无法相信我使用的是iOS 9.3,即2016年2月,这仍然是解决此问题的最佳方法。谢谢,像个魅力一样工作
gmogames

1
你救了我。最佳解决方案。
Baran Emre

21

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

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

告诉代表滚动视图已结束,从而降低了滚动运动。

UIScrollViewDelegate文档


嗯,我给了同样的答案。:)
Suvesh Pratapa

h 有点神秘地命名,但是它正是我想要的。应该更好地阅读文档。这是漫长的一天...
Michael Gaylord

这个班轮解决了我的问题。为我快速解决!谢谢。
Dody Rachmat Wicaksono

20

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

- (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也没有用。

一个的情况下分页滚动视图:

因为,我猜想,苹果会应用一条加速曲线,scrollViewDidEndDecelerating每次拖动都会被调用,因此scrollViewDidEndDragging在这种情况下无需使用。


您的分页滚动视图案例有一个问题:如果您完全在页面结束位置释放滚动(不需要滚动),scrollViewDidEndDecelerating则不会被调用
Shadow Of

19

在其他一些答案中对此进行了描述,但是这里(在代码中)介绍了滚动完成后如何组合scrollViewDidEndDeceleratingscrollViewDidEndDragging:willDecelerate执行一些操作:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self stoppedScrolling];
}

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

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

7

我只是发现了这个问题,与我问的问题几乎相同: 如何确切知道UIScrollView的滚动何时停止?

尽管didEndDecelerating在滚动时有效,但是没有注册带有固定释放的平移。

我最终找到了解决方案。didEndDragging具有参数WillDecelerate,在固定释放情况下为false。

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


5

如果您喜欢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
})

这将同时考虑到拖动和减速。


这正是我想要的。谢谢!
nomnom

4
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollingFinished(scrollView: scrollView)
}

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if decelerate {
        //didEndDecelerating will be called for sure
        return
    }
    else {
        scrollingFinished(scrollView: scrollView)
    }
}

func scrollingFinished(scrollView: UIScrollView) {
   // Your code
}

3

我已经尝试过Ashley Smart的答案,它的工作原理很吸引人。这是另一个想法,仅使用scrollViewDidScroll

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

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


3

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
}

3

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

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)
    ...
}

2

刚刚开发的解决方案可以检测整个应用程序何时滚动完成:https//gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

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

这是跟踪iOS 10+的运行循环模式更改的核心思想:

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

大声笑应用范围的滚动检测-例如,如果该应用的任何UIScrollVIew当前正在滚动……似乎有点无用……
艾萨克·卡罗尔·韦斯伯格

@IsaacCarolWeisberg,您可以将繁重的操作延迟到平滑滚动完成以保持平滑为止。
6

沉重的操作,你说...的那些,我在全局并发调度队列执行,可能
艾萨克·卡罗尔·韦斯伯格

1

回顾(以及新手)。并不是那么痛苦。只需添加协议,然后添加检测所需的功能。

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

// --------------------------------
// 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

1

我遇到了点击和拖动动作的情况,发现拖动正在调用scrollViewDidEndDecelerating

并使用代码([_scrollView setContentOffset:contentOffset animation: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
}

1

一种替代方法是使用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")
    }

0

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

0

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
}

0

使用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)
    }

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


0

在某些较早的iOS版本(如iOS 9、10)上,scrollViewDidEndDecelerating如果通过触摸突然停止scrollView ,则不会触发。

但在当前版本(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”注释的两行中使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.