实现类Google Maps的TableView抽屉:展开后延续拖拽速度滚动
实现抽屉拖拽速度向TableView滚动速度的转换方案
这个需求我在做类似的底部抽屉组件时正好碰到过,核心是要在手势结束的节点精准捕捉拖拽速度,再无缝传递给TableView模拟自然滚动。下面是我验证过的可行实现思路:
1. 先做好基础状态与数据记录
首先在你的抽屉容器类里定义状态枚举和关键属性,用来跟踪抽屉状态和拖拽速度:
// 抽屉状态枚举 enum DrawerState { case collapsed, halfExpanded, fullyExpanded } // 当前抽屉状态,初始设为半展开 var currentDrawerState: DrawerState = .halfExpanded // 记录手势结束前的最后拖拽速度 var lastDragVelocity: CGPoint = .zero
2. 优化手势处理逻辑
在你的平移手势处理方法里,分阶段处理手势状态,重点在手势结束时判断是否触发全展开,并完成速度传递:
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: self.view) // 实时更新拖拽速度 lastDragVelocity = gesture.velocity(in: self.view) switch gesture.state { case .began: // 手势开始时临时禁用TableView滚动,避免手势冲突 tableView.isScrollEnabled = false case .changed: // 这里保留你原有的抽屉拖拽位移计算逻辑(调整frame或约束) // ... 你的现有代码 ... case .ended, .cancelled: // 判断是否满足从半展开切换到全展开的条件 if currentDrawerState == .halfExpanded && shouldSwitchToFullyExpanded(translation: translation) { // 先完成抽屉全展开的动画 animateDrawer(to: .fullyExpanded) { [weak self] finished in guard let self = self, finished else { return } // 动画完成后传递速度给TableView self.transferVelocityToTableView() // 重新开启TableView滚动 self.tableView.isScrollEnabled = true } } else { // 其他状态切换逻辑(比如回到半展开) animateDrawer(to: .halfExpanded) tableView.isScrollEnabled = true } default: break } }
3. 定义状态切换的判断规则
根据拖拽位移和速度,判断是否要切换到全展开状态:
private func shouldSwitchToFullyExpanded(translation: CGPoint) -> Bool { // 假设抽屉向上拖拽展开(y轴负方向),可根据你的坐标系调整 let upwardVelocityThreshold: CGFloat = -300 // 速度超过这个值直接触发 let upwardTranslationThreshold: CGFloat = -50 // 拖拽位移超过这个值也触发 return translation.y < upwardTranslationThreshold || lastDragVelocity.y < upwardVelocityThreshold }
4. 核心:将拖拽速度转换为TableView滚动速度
这一步是关键,要把抽屉的拖拽速度转换成TableView的滚动速度,模拟自然滚动的衰减效果:
private func transferVelocityToTableView() { // 坐标系转换:抽屉的y轴拖拽速度对应TableView的垂直滚动速度,调整方向和阻尼 // 0.8是阻尼系数,可根据手感调整 let scrollVelocity = CGPoint(x: 0, y: lastDragVelocity.y * 0.8) // 计算目标偏移量,模拟滚动衰减 let targetOffset = tableView.contentOffset.applying(CGAffineTransform(translationX: 0, y: scrollVelocity.y * 0.2)) // 用动画模拟自然滚动的衰减过程 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: abs(scrollVelocity.y)/1000, options: .curveEaseOut) { self.tableView.setContentOffset(targetOffset, animated: false) } // 保持TableView的自然减速特性 tableView.decelerationRate = .normal }
5. 辅助:抽屉状态切换动画
如果用Auto Layout实现抽屉位置,动画逻辑大概是这样:
private func animateDrawer(to state: DrawerState, completion: ((Bool) -> Void)? = nil) { switch state { case .fullyExpanded: drawerTopConstraint.constant = 0 // 全展开时顶部贴父视图顶部 case .halfExpanded: drawerTopConstraint.constant = self.view.bounds.height * 0.5 // 半展开高度为屏幕一半 default: break } UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) { self.view.layoutIfNeeded() } completion: { finished in completion?(finished) } currentDrawerState = state }
6. 手势冲突的最后保障
重写手势识别器的代理方法,确保抽屉手势优先于TableView滚动手势:
override func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { // 当抽屉未全展开时,让TableView的滚动手势失效 if otherGestureRecognizer is UIPanGestureRecognizer && currentDrawerState != .fullyExpanded { return true } return false }
内容的提问来源于stack exchange,提问作者Eden
相关产品推荐
相关产品推荐

