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

实现类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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 10:27:17