ChatGPT解决这个技术问题 Extra ChatGPT

如何检测 UIScrollView 何时完成滚动

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

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

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

另请参阅,如果您想在以编程方式滚动后检测完成的滚动:stackoverflow.com/questions/2358046/…
较旧的 ObjC 代码,但对我来说效果很好 github.com/dhoerl/ScrollWatcher/blob/master/ScrollWatcher/…

m
mojuba

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,这仍然是这个问题的最佳解决方案。谢谢,像魅力一样工作
你救了我。最佳解决方案。
I
Iulian Onofrei
- (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 也没有用。
W
Wayne

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

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

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

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

A
Aurelien Porte

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

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

分页滚动视图的情况:

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


您的分页滚动视图案例有一个问题:如果您在页面结束位置准确释放滚动(不需要滚动时),则不会调用 scrollViewDidEndDecelerating
t
texmex5

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

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

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

UIScrollViewDelegate documentation


呃,我给了同样的答案。 :)
多哈。有点神秘的名字,但它正是我想要的。应该更好地阅读文档。这是漫长的一天...
这一个班轮解决了我的问题。快给我解决!谢谢你。
C
Community

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

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

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

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


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

X
Xernox

如果有人需要,这是 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)
    ...
}

Z
Zappel

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

这将考虑拖动和减速。


这正是我一直在寻找的。谢谢!
E
Ege Akpinar

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

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

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


z
zzzz

已接受答案的 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
}

k
k06a

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

它基于跟踪运行循环模式变化的思想。并在滚动后至少 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,您可以延迟繁重的操作,直到平滑滚动完成以保持平滑。
繁重的操作,你说......我在全局并发调度队列中执行的那些,可能
b
bob

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

在包含 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

A
Adriana

我有一个点击和拖动动作的案例,我发现拖动正在调用 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
}

l
lexpenz

有一种 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
}

D
Dog

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

J
JsW

在一些早期的 iOS 版本(如 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”注释的两行中使用。


没有“scrollViewDidEndScrolling”(developer.apple.com/documentation/uikit/uiscrollviewdelegate
R
Rahul K Rajan

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

K
Kelvin Tan

使用 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 滚动时等问题。如果没有逻辑,当您滚动出 tableview 时,将调用 didEnd 但它不会执行任何操作。目前在 2020 年使用它。


О
Олег Александрович Пустошкин

RXSwift 版本

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

Z
Zatman

我经历了所有的排列,我发现的最好的代码就是这个。有趣的部分是在“scrollViewWillBeginDragging”中并检测速度是否为零。当用户在动画滚动期间将手指放在屏幕上以制动滚动时,可能会发生这种情况。出于某种原因,这样做不会触发任何其他委托“停止”API。

public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {        
    let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.superview)
    if (velocity.equalTo(CGPoint.zero)){
        scrollViewDidEndScrollingAnimation(scrollView)
    }
}


public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if (decelerate == false){
        scrollViewDidEndScrollingAnimation(scrollView)
    }
}

public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    scrollViewDidEndScrollingAnimation(scrollView)
}

public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
    // do whatever when scrolling stops
}