流畅的下滑关闭页面

26年4月25日 星期六 (已编辑)
1134 字
6 分钟
这篇文章最后修改于 26年4月26日 星期日,部分内容可能已经不适用,如有疑问可联系作者。
AI 摘要

正在生成中...

今天对音乐 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、歌词页这些内部内容的滚动手势,经常和关闭手势冲突。改了之后,关闭手势只绑定在顶部标题区,内部内容继续保留自己的滚动,内外不再打架。

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

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

文章标题:流畅的下滑关闭页面

文章作者:Lanke

文章链接:https://blog.blueke.top/posts/liu-chang-de-xia-hua-guan-bi-ye-mian[复制]

最后修改时间


商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。

1 / 1