如何检测 UIScrollView 何时完成滚动

UIScrollViewDepate 有两个委托方法 scrollViewDidScroll:scrollViewDidEndScrollingAnimation:,但是它们都没有告诉您什么时候滚动完成。scrollViewDidScroll只是通知您滚动视图没有滚动,而不是它已经完成了滚动。

另一个方法 scrollViewDidEndScrollingAnimation似乎只有在以编程方式移动滚动视图时才会触发,而在用户滚动时则不会触发。

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

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


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


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

我想 scrollViewDidEndDecelerating 是你想要的。它的 UIScrollViewGenerates 可选方法:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

告诉委托滚动视图已结束减速滚动运动。

委托文档

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

我已经尝试了 Ashley Smart 的答案,效果非常好。这里有另一个想法,只使用 scrollViewDidScroll

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

我只有两页纸,所以对我很有用。然而,如果你有一个以上的页面,它可能是有问题的(你可以检查当前的偏移量是宽度的倍数,然后你不会知道如果用户停止在第2页或他的方式第3或更多)

我只是找到了这个问题,和我问的差不多: 如何确切地知道当 UIScrollView 的滚动已经停止?

尽管 EndDecelerating 在滚动时起作用,但使用固定版本的平移并不起作用。

我最终找到了一个解决方案。 did EndDraging 有一个参数 WillDecelerate,在静态发布的情况下是错误的。

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

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

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

如果你的滚动是由于其他原因(比如键盘打开或者键盘关闭) ,看起来你必须通过黑客技术来检测这个事件,因为 scrollViewDidEndScrollingAnimation也没什么用。

分页滚动视图的情况:

因为,我猜想,苹果应用一个加速曲线,scrollViewDidEndDecelerating被调用为每个拖动,所以没有必要使用 scrollViewDidEndDragging在这种情况下。

概述(对新手而言)。没那么痛苦。只需添加协议,然后添加检测所需的函数。

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

这已经在其他一些答案中描述过了,但是下面(在代码中)介绍了当滚动完成时如何组合 scrollViewDidEndDeceleratingscrollViewDidEndDragging:willDecelerate来执行某些操作:

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


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


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

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

快速答案:

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
}

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

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

刚刚开发了一个解决方案,用于检测什么时候完成了应用程序范围的滚动: Https://gist.github.com/k06a/731654e3168277fb1fd0e64abc7d899e

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

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

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

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

另一种方法是使用 scrollViewWillEndDragging:withVelocity:targetContentOffset,每当用户抬起手指并包含滚动将停止的目标内容偏移量时,就会调用 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")
}
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
}

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

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

使用 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年使用。

在一些早期的 iOS 版本(如 iOS9,10)中,如果 scrollView 突然被触摸停止,scrollViewDidEndDecelerating就不会被触发。

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

因此,如果你的应用程序也针对早期版本,你可能需要一个变通方案,如阿什利聪明提到的,或者你可以以下一个。


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”注释的两行中使用。

RXSwift 版本

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

我仔细检查了所有的排列,我找到的最好的代码是这个。有趣的部分在“ scrollViewWillBeginDraging”中,检测速度是否为零。当用户的手指在动画滚动过程中放在屏幕上,从而对滚动进行制动时,就会出现这种情况。由于某种原因,这样做不会触发任何其他委托“停止”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
}

我需要一个回调,火灾时,所有缩放和滚动手势已结束 还有所有动画,如减速和缩放反弹已完成。

UIScrollView有一些我们可以检查的标志,比如 isTrackingisDecelerating。我们还有各种 scrollViewDidEnd...委托回调。但是,这些标志和回调的更新顺序不一致,并且覆盖所有边缘情况看起来非常困难。

我现在要做的是将滚动视图标志的检查推迟到下一个运行循环。这可以确保所有标志都已更新。

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
dispatchScrollViewInteractionUpdate()
}


func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
dispatchScrollViewInteractionUpdate()
}


func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
dispatchScrollViewInteractionUpdate()
}


func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
dispatchScrollViewInteractionUpdate()
}


private func dispatchScrollViewInteractionUpdate() {
DispatchQueue.main.async {
self.updateScrollViewInteraction()
}
}


private func updateScrollViewInteraction() {
if !scrollView.isZooming
&& !scrollView.isDragging
&& !scrollView.isDecelerating
&& !scrollView.isZoomBouncing
&& !scrollView.isTracking {
print("All animations and interactions have ended.")
}
}