You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS如何让ScrollView减速时继续滑动至顶部而非中途停止?

解决iOS ScrollView减速到指定位置后继续滑到0的问题

要实现ScrollView在减速过程中原本要停在Y=20时,继续以当前减速度滑到Y=0,直接用setContentOffset:animated:肯定不行——它的动画曲线是固定的,没法匹配ScrollView原生的减速手感。我们需要接管ScrollView的减速过程,用物理模拟的方式手动计算每帧的偏移量,具体步骤如下:

1. 监测ScrollView的状态和偏移量

通过UIScrollViewDelegate的两个方法覆盖两种场景:拖拽结束后系统计算的停止点为Y=20,以及已处于减速状态、偏移量接近Y=20的情况。

场景1:拖拽结束时拦截目标停止点

实现scrollViewWillEndDragging:withVelocity:targetContentOffset:方法,这里能直接拿到系统计算的最终停止位置:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    // 用小阈值避免浮点精度问题
    if (ABS(targetContentOffset->y - 20) < 1.0) {
        // 先阻止系统默认的停止行为,固定当前偏移量
        [scrollView setContentOffset:scrollView.contentOffset animated:NO];
        // 启动自定义减速动画到Y=0
        [self startCustomDecelerationForScrollView:scrollView withInitialVelocity:velocity.y initialOffset:scrollView.contentOffset.y];
    }
}

场景2:减速过程中监测偏移量

实现scrollViewDidScroll:方法,同时记录上一帧的偏移量和时间来计算当前滑动速度:

@property (nonatomic, assign) CGFloat lastOffsetY;
@property (nonatomic, assign) NSTimeInterval lastTime;

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (!scrollView.decelerating) return;
    
    // 计算当前滑动速度
    NSTimeInterval currentTime = CACurrentMediaTime();
    CGFloat deltaOffset = scrollView.contentOffset.y - self.lastOffsetY;
    NSTimeInterval deltaTime = currentTime - self.lastTime;
    CGFloat currentVelocity = deltaOffset / deltaTime;
    
    self.lastOffsetY = scrollView.contentOffset.y;
    self.lastTime = currentTime;
    
    // 当偏移量接近Y=20时启动自定义动画
    if (ABS(scrollView.contentOffset.y - 20) < 1.0) {
        [scrollView setContentOffset:scrollView.contentOffset animated:NO];
        [self startCustomDecelerationForScrollView:scrollView withInitialVelocity:currentVelocity initialOffset:scrollView.contentOffset.y];
        // 重置记录值避免干扰后续逻辑
        self.lastOffsetY = 0;
        self.lastTime = 0;
    }
}

2. 用CADisplayLink模拟自然减速动画

我们用CADisplayLink逐帧计算偏移量,完美模拟ScrollView的原生减速规律(基于系统的decelerationRate,速度每帧按指数衰减):

- (void)startCustomDecelerationForScrollView:(UIScrollView *)scrollView withInitialVelocity:(CGFloat)initialVelocity initialOffset:(CGFloat)initialOffset {
    __block CGFloat currentVelocity = initialVelocity;
    __block CGFloat currentOffset = initialOffset;
    
    CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateScrollOffset:)];
    // 用userInfo传递需要的数据
    displayLink.userInfo = @{
        @"scrollView": scrollView,
        @"velocity": @(currentVelocity),
        @"offset": @(currentOffset),
        @"displayLink": displayLink
    };
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)updateScrollOffset:(CADisplayLink *)displayLink {
    UIScrollView *scrollView = displayLink.userInfo[@"scrollView"];
    CGFloat velocity = [displayLink.userInfo[@"velocity"] floatValue];
    CGFloat currentOffset = [displayLink.userInfo[@"offset"] floatValue];
    CADisplayLink *link = displayLink.userInfo[@"displayLink"];
    
    CGFloat deltaTime = displayLink.duration;
    // 模拟原生ScrollView的减速:每帧速度乘以系统的减速率
    CGFloat decelerationRate = scrollView.decelerationRate;
    velocity *= decelerationRate;
    
    // 更新当前偏移量
    currentOffset += velocity * deltaTime;
    
    // 到达Y=0时停止动画
    if (currentOffset <= 0) {
        currentOffset = 0;
        [link invalidate];
        scrollView.contentOffset = CGPointMake(0, currentOffset);
    } else {
        scrollView.contentOffset = CGPointMake(0, currentOffset);
        // 更新userInfo中的数据供下一帧使用
        NSMutableDictionary *updatedInfo = [displayLink.userInfo mutableCopy];
        updatedInfo[@"velocity"] = @(velocity);
        updatedInfo[@"offset"] = @(currentOffset);
        displayLink.userInfo = updatedInfo;
    }
}

关键说明

  • 为什么不用UIView动画?:原生UIView动画的timingCurve无法完美模拟ScrollView的指数减速规律,用CADisplayLink逐帧计算能保证动画手感和原生减速完全一致。
  • 减速率的使用:直接复用ScrollView的decelerationRate(默认是UIScrollViewDecelerationRateNormal或Fast),确保减速节奏和系统统一。
  • 阈值处理:判断偏移量时使用小阈值(比如1.0),避免因浮点精度问题导致逻辑触发异常。

内容的提问来源于stack exchange,提问作者art-of-dreams

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:25:31