繁体   English   中英

如何检测 UIScrollView 何时完成滚动

[英]How to detect when a UIScrollView has finished scrolling

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

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

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

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];
...
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self stoppedScrolling];
}

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

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

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

- (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在这种情况下。

这已在其他一些答案中进行了描述,但这里(在代码中)如何组合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 是你想要的。 它的 UIScrollViewDelegates 可选方法:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

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

UIScrollViewDelegate 文档

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

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

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

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

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

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

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

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

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

这是跟踪运行循环模式更改 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 ]];
}

我已经尝试过 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 页或更多的)

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

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

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

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

在一些早期的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”注释的两行中。

我需要一个回调,当所有缩放和滚动手势结束并且所有动画(如减速和缩放反弹)完成时触发。

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

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

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

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

我经历了所有的排列,我发现的最好的代码就是这个。 有趣的部分是在“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
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM