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
相关产品推荐
相关产品推荐

