# 流畅的下滑关闭页面

> 简单分享一下flutter关闭页面的优化思路

发布：2026-04-25 | 分类：计算机 | 标签：Flutter


今天对音乐 App 有进行了一波优化，修了一个交互问题，感觉挺值得记录一下。

关闭播放器界面，我新增了一个手势下滑关闭，本来预期是页面跟着手指往下走。结果手指一松开，页面上会重新上滑到顶部，然后才开始下滑关闭。

## 问题出在哪

旧逻辑是这样的：用户往下滑，页面跟着移动一小段距离，松手的时候判断有没有达到阈值，达到了就关闭，没达到就回弹。

功能上没问题，页面确实能被关掉。但问题是用户下滑的第一帧，页面不是从手指位置开始响应的。

更具体地说，页面在某个状态下被重置了。这个重置动作在视觉上就呈现为"上来一点"。

我在代码里找了半天，发现原来手势开始时，页面先把内部状态归零，然后才去计算用户拖了多远。用户在下滑，结果页面的第一个动作是往上跳一下再开始下滑。这感觉就像你在推一扇门，结果门先往后让了一下再被你推走。

## 旧代码的问题

旧实现大致是这样的：

```dart
GestureDetector(
  onVerticalDragStart: (details) {
    // 记录起始位置
  },
  onVerticalDragUpdate: (details) {
    // 根据增量更新偏移量
    final delta = details.primaryDelta ?? 0;
    _offset += delta;
    setState(() {});
  },
  onVerticalDragEnd: (details) {
    // 判定是否关闭
    if (_offset > threshold) {
      Navigator.pop(context);
    } else {
      _offset = 0;
      setState(() {});
    }
  },
  child: Transform.translate(
    offset: Offset(0, _offset),
    child: pageContent,
  ),
)
```

问题在哪？用手势的增量累加偏移量，而不是用手指的真实位移。当手势仲裁发生时，增量累加的起点和手指真实位置不同步，第一帧就会出现跳变。

## 怎么改的

核心思路变了：不再把下滑手势当成"触发关闭的信号"，而是把它当成"用户直接控制页面位置"。

手势开始的时候，直接从手指按下的位置开始计算，不去重置任何内部状态。手势结束的时候，从当前位置继续下滑关闭，或者从当前位置回弹。不存在"重新开始"这个概念。

手势层用 GestureDetector 监听垂直拖动，更新时直接用手指的真实位移，而不是增量累加。动画层用 AnimationController.unbounded() 表达像素级位移，拖动时直接更新控制器的值，结束时从当前位置继续动画。

具体是这样：

首先定义控制器和必要的变量：

```dart
late final AnimationController _dismissController = AnimationController.unbounded(
  vsync: this,
);

double _startY = 0;
double _currentOffset = 0;
static const double _dismissThreshold = 200.0;
static const Duration _dismissDuration = Duration(milliseconds: 300);
```

手势开始时，记录手指按下的真实位置：

```dart
onVerticalDragStart: (details) {
  _startY = details.globalPosition.dy;
},
```

手势更新时，用当前 globalPosition.dy 减去起始位置得到真实位移，这个值直接赋给控制器：

```dart
onVerticalDragUpdate: (details) {
  _currentOffset = details.globalPosition.dy - _startY;
  _dismissController.value = _currentOffset.clamp(0.0, screenHeight);
},
```

手势结束时，判断是继续下滑还是回弹：

```dart
onVerticalDragEnd: (details) {
  final velocity = details.primaryVelocity ?? 0;
  final current = _dismissController.value;

  if (current > _dismissThreshold || velocity > 500) {
    // 从当前位置继续向下完成关闭动画，动画完再 pop
    _dismissController.animateTo(
      screenHeight,
      duration: _dismissDuration,
      curve: Curves.easeOut,
    ).then((_) => Navigator.pop(context));
  } else {
    // 从当前位置弹回原位
    _dismissController.animateBack(
      0.0,
      duration: _dismissDuration,
      curve: Curves.easeOut,
    );
  }
},
```

页面的位移通过 AnimatedBuilder 绑定到控制器：

```dart
AnimatedBuilder(
  animation: _dismissController,
  builder: (context, child) {
    return Transform.translate(
      offset: Offset(0, _dismissController.value),
      child: child,
    );
  },
  child: pageContent,
)
```

## 改完之后

下滑的第一帧就对上了，页面从手指真实位置开始响应，不再有那个往上跳的动作。

还有一个问题也顺便解决了：之前播放器里的 PageView、歌词页这些内部内容的滚动手势，经常和关闭手势冲突。改了之后，关闭手势只绑定在顶部标题区，内部内容继续保留自己的滚动，内外不再打架。

松手之后的行为也顺了很多。之前是"达到阈值就关闭，没达到就回弹"，两个动作之间有明显的切换感。现在是从当前位置继续动画，用户能看清"自己推下去的页面继续离场"，而不是被某个条件突然截断。

总的来说，优化前后的核心差异很明显：优化前，用户是在“触发”一个关闭指令；优化后，用户更像是在“亲手操作”页面离场，交互感更直观、更自然。
