涉及文件:
client/lib/ui/image_preview_page.dartclient/lib/ui/video_player_page.dart
一、问题概述
本次修复解决了三个问题:
- 视频比例菜单横屏溢出 - 横屏模式下底部菜单超出屏幕边界
- 长按加速弹窗动画不符合预期 - 我更希望弹窗立即出现,松手立即消失,无过渡动画
- 图片捏合放缩时误滑到相邻图片 - 两指捏合操作时会意外触发 PageView 左右滑动
二、视频比例菜单横屏溢出
2.1 问题分析
_showAspectMenu() 使用 showModalBottomSheet 显示画面比例选项。在横屏模式下,屏幕高度变小,Column 内容超出可视区域。
2.2 解决方案
与之前修复字幕菜单和音轨菜单溢出的方案一致:
showModalBottomSheet(
context: context,
backgroundColor: Colors.grey[900],
isScrollControlled: true, // 允许自定义高度
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7, // 最大高度 70%
),
builder: (ctx) => SafeArea(
child: SingleChildScrollView( // 内容可滚动
child: Column(
mainAxisSize: MainAxisSize.min,
children: [/* ... */],
),
),
),
);关键点:
isScrollControlled: true允许 BottomSheet 超出默认高度限制constraints.maxHeight限制最大高度为屏幕的 70%SingleChildScrollView包裹内容,确保超出时可滚动
三、长按加速弹窗动画移除
3.1 我这边用的时候发现
我这边用的时候发现长按加速弹窗不需要弹出动画,应该:
- 长按时:立即显示
- 松手时:立即消失
3.2 原实现
原代码使用 TweenAnimationBuilder 实现缩放动画:
// 旧代码
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.8, end: 1.0),
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
builder: (context, scale, child) {
return Transform.scale(
scale: scale,
child: Container(/* ... */),
);
},
)3.3 新实现
直接返回 Container,无任何动画:
// 新代码
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF2196F3),
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(
color: Color(0x662196F3),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: Row(/* ... */),
)设计决策:
- 长按加速是实时操作反馈,动画会带来延迟感
- 参考视频 App(如 B站、YouTube)的长按加速,都是立即显示
四、图片捏合手势冲突(核心问题)
4.1 问题描述
在图片预览页面,当我两指捏合放大图片时,经常会意外滑动到上一张或下一张图片。这是因为:
InteractiveViewer处理捏合缩放PageView处理左右滑动- 两个手势同时竞争,导致操作时经常触发错误的行为
4.2 之前的尝试(失败)
之前我们尝试通过缩放状态回调来控制:
// 旧方案:基于缩放状态
bool _isZoomed = false;
// 在 _ImageViewerItem 中添加回调
onZoomChanged: (zoomed) {
if (_isZoomed != zoomed) {
setState(() => _isZoomed = zoomed);
}
}
// PageView 根据缩放状态切换 physics
physics: _isZoomed
? const NeverScrollableScrollPhysics()
: const PageScrollPhysics(),问题:
- 回调是在
onInteractionEnd时触发的 - 当开始捏合时,图片可能还没放大,但手指已经在移动
- 这时 PageView 仍然可以滑动,导致误触
4.3 调研成熟项目方案
通过搜索,找到了 Medium 上的一篇文章:How to Resolve Gesture Conflicts in Flutter with Scroll and Pinch-to-Zoom
核心思路:检测触点数量,而非缩放状态
- 当触点数量 >= 2 时(多指操作),禁用 PageView 滑动
- 当触点数量 < 2 时(单指或无触摸),启用 PageView 滑动
4.4 最终实现
使用 Listener widget 检测触点数量:
class _ImagePreviewPageState extends State<ImagePreviewPage> {
// 触点数量检测(多指操作时禁用 PageView 滑动)
int _pointerCount = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
body: Listener(
// 检测触点数量:多指操作时禁用 PageView 滑动
onPointerDown: (_) => setState(() => _pointerCount++),
onPointerUp: (_) => setState(() => _pointerCount--),
onPointerCancel: (_) => setState(() => _pointerCount--),
child: Stack(
children: [
PageView.builder(
// 多指操作时禁用 PageView 滑动,避免捏合时意外切换图片
physics: _pointerCount >= 2
? const NeverScrollableScrollPhysics()
: const PageScrollPhysics(),
itemBuilder: (context, index) {
return _ImageViewerItem(/* ... */);
},
),
],
),
),
);
}
}4.5 方案对比
| 方案 | 触发时机 | 可靠性 | 使用体验 |
|---|---|---|---|
| 缩放状态回调 | 手势结束后 | 低 | 差,捏合初期仍会误滑 |
| 触点数量检测 | 手指触碰瞬间 | 高 | 好,立即阻止滑动 |
4.6 为什么 Listener 比 GestureDetector 更适合
Listener是低级别的触摸事件监听,不参与手势竞争GestureDetector会与InteractiveViewer产生手势冲突Listener可以在不干扰其他手势的情况下获取触点信息
4.7 代码清理
移除了不再需要的代码:
_ImageViewerItem中的onZoomChanged参数_onInteractionStart方法(原用于检测缩放开始)_onInteractionEnd中的onZoomChanged调用
保留了 _onInteractionEnd 用于同步 _zoomLevel(双击缩放级别切换需要)。
五、技术要点总结
5.1 Flutter 手势处理层级
Listener (最底层,原始触摸事件) ↓GestureDetector (手势识别) ↓InteractiveViewer / PageView (高级交互组件)当需要检测触点数量而不参与手势竞争时,使用 Listener。
5.2 BottomSheet 横屏适配模式
showModalBottomSheet(
isScrollControlled: true,
constraints: BoxConstraints(maxHeight: height * 0.7),
builder: (ctx) => SafeArea(
child: SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, children: []),
),
),
);5.3 动画的取舍
- 装饰性动画:页面切换、列表滚动 → 可以有动画
- 实时反馈:长按加速、拖动进度 → 不应有动画延迟
六、相关文件变更
image_preview_page.dart
- 添加
_pointerCount状态变量 - 使用
Listener包裹 body 检测触点 - PageView 的
physics根据_pointerCount动态切换 - 移除
_ImageViewerItem.onZoomChanged参数和相关逻辑
video_player_page.dart
_showAspectMenu()添加滚动支持_buildSpeedIndicator()移除TweenAnimationBuilder
七、后续优化方向
-
惯性滑动处理:当前方案在手指抬起瞬间就允许滑动,如果快速松手,可能还有惯性滑动。可以考虑添加短暂延迟。
-
缩放状态保持:当前切换图片后缩放状态会重置。如果需要保持,可以在
_ImageViewerItem中保存变换矩阵。 -
边界回弹:图片缩放后拖动到边界时,可以添加弹性回弹效果。