媒体预览优化与视频播放器功能增强

December 29, 2025
6 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

涉及功能: 图片预览缓存、视频播放器边距、聊天输入框、长按加速、锁定屏幕、App 重命名


一、问题背景与需求

1.1 我这边用的时候发现的问题

  1. 图片预览缓存问题:滑到下一张图片后,再滑回上一张,需要重新加载,体验不流畅
  2. 视频播放器边距问题:横屏/全屏模式下,顶部按钮太贴近屏幕边缘,难以点击
  3. 聊天输入框抖动:点击录音按钮切换输入状态时,输入框高度变化导致视觉抖动
  4. 视频功能缺失:缺少长按加速播放和锁定屏幕功能

1.2 功能需求

需求优先级复杂度
图片预览缓存优化
视频边距修复
输入框抖动修复
长按加速播放
锁定屏幕

二、问题分析与方案设计

2.1 图片预览缓存问题

问题根因分析

// 原代码 - image_preview_page.dart
isLoading: _loadingStates[_imageFiles[index].id] ?? true,

问题在于 ?? true 的逻辑:当 _loadingStates 中没有该文件的 key 时,默认返回 true,导致即使图片数据已经在 _imageCache 中,也会显示加载动画。

方案对比

方案优点缺点选择
方案A: == true 替代 ?? true简单,null 时返回 false首次加载前会显示空白
方案B: 同时检查缓存和加载状态逻辑完整,覆盖所有情况代码稍复杂

最终方案

// 修复后
final file = _imageFiles[index];
final isLoading = _loadingStates[file.id] == true && !_imageCache.containsKey(file.id);

逻辑解读:

  • _loadingStates[file.id] == true:正在加载中
  • !_imageCache.containsKey(file.id):缓存中还没有数据
  • 两者同时满足才显示 loading,否则直接显示缓存的图片

2.2 视频播放器边距问题

问题分析

从我这边的截图可以看到,横屏模式下右上角的按钮(1.0x 倍速、设置、更多)几乎贴着屏幕边缘,在有刘海屏或曲面屏的设备上更难点击。

media_kit 主题配置分析

MaterialVideoControlsThemeData 提供以下边距相关属性:

  • padding: 整体内边距
  • topButtonBarMargin: 顶部按钮栏外边距
  • bottomButtonBarMargin: 底部按钮栏外边距

解决方案

fullscreen: MaterialVideoControlsThemeData(
  // 原有配置
  padding: const EdgeInsets.symmetric(horizontal: 16),
  // 新增配置
  topButtonBarMargin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
  bottomButtonBarMargin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
),

边距设计考量:

  • 左右各 16px,避免按钮贴边
  • 顶部 16px,与状态栏保持距离
  • 底部 16px,避免与系统手势区冲突

2.3 聊天输入框抖动问题

问题分析

// 原代码 - chat_page.dart
Expanded(
  child: _isVoiceMode
      ? _buildVoiceRecordButton(...)  // 固定高度
      : _buildTextInputArea(...),      // 可变高度 (minLines: 1, maxLines: 4)
),

两个组件的高度计算方式不同:

  • _buildVoiceRecordButton: 使用 SizedBox(height: inputHeight) 固定高度
  • _buildTextInputArea: 使用 IntrinsicHeight + ConstrainedBox(minHeight: inputHeight),高度可随内容增长

切换时如果输入框有多行内容,高度会突变。

方案对比

方案优点缺点选择
方案A: 两者都用固定高度完全无抖动限制输入框功能
方案B: AnimatedSwitcher 淡入淡出平滑过渡,视觉舒适切换有短暂延迟
方案C: AnimatedContainer 高度动画高度平滑变化实现复杂

最终方案

Expanded(
  child: AnimatedSwitcher(
    duration: const Duration(milliseconds: 200),
    transitionBuilder: (child, animation) {
      return FadeTransition(opacity: animation, child: child);
    },
    child: _isVoiceMode
        ? KeyedSubtree(
            key: const ValueKey('voice'),
            child: _buildVoiceRecordButton(...),
          )
        : KeyedSubtree(
            key: const ValueKey('text'),
            child: _buildTextInputArea(...),
          ),
  ),
),

关键点:

  • AnimatedSwitcher 提供切换动画
  • KeyedSubtree + ValueKey 确保正确识别组件切换
  • FadeTransition 使用淡入淡出效果,视觉上更平滑

三、视频播放器功能增强

3.1 长按加速播放

功能设计

项目设计
触发方式长按视频区域
加速倍率2.0x(可配置)
恢复时机松开手指
视觉反馈顶部显示 “2.0x 加速中” 提示
锁定兼容锁定状态下禁用

实现方案

// 状态变量
bool _isLongPressing = false;
double _speedBeforeLongPress = 1.0;
static const double _longPressSpeed = 2.0;
 
// 长按开始
void _onLongPressStart() {
  if (_isLocked) return;  // 锁定时不响应
  _speedBeforeLongPress = _currentSpeed;  // 保存当前速度
  _player.setRate(_longPressSpeed);       // 切换到加速
  setState(() => _isLongPressing = true);
}
 
// 长按结束
void _onLongPressEnd() {
  if (!_isLongPressing) return;
  _player.setRate(_speedBeforeLongPress);  // 恢复原速度
  setState(() => _isLongPressing = false);
}

手势集成

GestureDetector(
  onLongPressStart: (_) => _onLongPressStart(),
  onLongPressEnd: (_) => _onLongPressEnd(),
  onLongPressCancel: _onLongPressEnd,  // 取消时也要恢复
  child: MaterialVideoControlsTheme(...),
)

加速提示 UI

if (_isLongPressing)
  Positioned(
    top: 80,
    left: 0,
    right: 0,
    child: Center(
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        decoration: BoxDecoration(
          color: Colors.black87,
          borderRadius: BorderRadius.circular(20),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(TablerIcons.player_play, color: Colors.white, size: 18),
            SizedBox(width: 6),
            Text('${_longPressSpeed}x 加速中', style: TextStyle(color: Colors.white)),
          ],
        ),
      ),
    ),
  ),

3.2 锁定屏幕

功能设计

项目设计
入口位置竖屏:顶部控制栏左侧;横屏:全屏按钮旁边
锁定状态图标变橙色,底部显示”已锁定”
禁用手势双击快进、滑动进度、亮度、音量
隐藏控件底部进度条、其他按钮(只保留锁定按钮)
解锁方式点击锁图标

状态管理

bool _isLocked = false;
 
void _toggleLock() {
  setState(() => _isLocked = !_isLocked);
  if (_isLocked) {
    showAppToast(context, '已锁定,点击锁图标解锁');
  }
}

手势禁用

MaterialVideoControlsThemeData(
  // 锁定时禁用所有手势
  seekOnDoubleTap: !_isLocked,
  seekGesture: !_isLocked,
  volumeGesture: !_isLocked,
  brightnessGesture: !_isLocked,
  // 锁定时隐藏底部控件
  bottomButtonBar: _isLocked ? [] : const [...],
)

锁定按钮

MaterialCustomButton(
  onPressed: _toggleLock,
  icon: Icon(
    _isLocked ? TablerIcons.lock : TablerIcons.lock_open,
    color: _isLocked ? Colors.orange : Colors.white,
  ),
),

锁定状态提示

if (_isLocked)
  Positioned(
    bottom: 80,
    left: 0,
    right: 0,
    child: Center(
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: Colors.black54,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(TablerIcons.lock, color: Colors.orange, size: 16),
            SizedBox(width: 4),
            Text('已锁定', style: TextStyle(color: Colors.white70, fontSize: 12)),
          ],
        ),
      ),
    ),
  ),

四、App 重命名

4.1 修改位置

平台文件修改内容
Androidandroid/app/src/main/AndroidManifest.xmlandroid:label
Windowswindows/runner/main.cppwindow.Create() 参数

4.2 具体修改

Android:

<!-- 修改前 -->
<application android:label="E2E Pan" ...>
 
<!-- 修改后 -->
<application android:label="AgentIO" ...>

Windows:

// 修改前
if (!window.Create(L"e2eepan_client", origin, size)) {
 
// 修改后
if (!window.Create(L"AgentIO", origin, size)) {

4.3 备选名字方案

在讨论过程中,提供了以下备选方案供参考:

名字含义适用场景
VaultSync保险库 + 同步强调安全存储
CipherNest密码 + 巢穴突出加密,有归属感
Enclave安全飞地简洁,安全领域常用
PrivoxPrivate + Box现代感
LockrLock + er简短
Arcanum拉丁语”秘密”高端神秘

五、文件修改清单

文件修改类型内容
image_preview_page.dart修改修复缓存判断逻辑
video_player_page.dart重写添加边距、长按加速、锁定屏幕
chat_page.dart修改AnimatedSwitcher 平滑切换
AndroidManifest.xml修改App 名称改为 AgentIO
main.cpp (Windows)修改窗口标题改为 AgentIO

六、关键代码变更

6.1 image_preview_page.dart

// 修改前
itemBuilder: (context, index) {
  return _ImageViewerItem(
    file: _imageFiles[index],
    imageData: _imageCache[_imageFiles[index].id],
    isLoading: _loadingStates[_imageFiles[index].id] ?? true,  // 问题所在
    ...
  );
},
 
// 修改后
itemBuilder: (context, index) {
  final file = _imageFiles[index];
  // 只有在明确加载中时才显示 loading,否则检查缓存
  final isLoading = _loadingStates[file.id] == true && !_imageCache.containsKey(file.id);
  return _ImageViewerItem(
    file: file,
    imageData: _imageCache[file.id],
    isLoading: isLoading,
    ...
  );
},

6.2 video_player_page.dart 新增状态

// 长按加速相关
bool _isLongPressing = false;
double _speedBeforeLongPress = 1.0;
static const double _longPressSpeed = 2.0;
 
// 锁定屏幕相关
bool _isLocked = false;

6.3 video_player_page.dart 主体结构

return SafeArea(
  child: Stack(
    children: [
      // 视频播放器主体(包含 GestureDetector 和 MaterialVideoControlsTheme)
      Center(
        child: GestureDetector(
          onLongPressStart: (_) => _onLongPressStart(),
          onLongPressEnd: (_) => _onLongPressEnd(),
          onLongPressCancel: _onLongPressEnd,
          child: MaterialVideoControlsTheme(...),
        ),
      ),
      // 长按加速提示
      if (_isLongPressing) Positioned(...),
      // 锁定状态提示
      if (_isLocked) Positioned(...),
    ],
  ),
);

七、测试要点

7.1 图片预览

  • 滑动切换图片后,滑回已查看的图片应立即显示(无加载动画)
  • 首次加载图片时应显示加载动画
  • 缓存应在整个预览会话中保持

7.2 视频播放器

  • 横屏模式按钮与屏幕边缘有足够间距
  • 长按视频区域开始加速,显示加速提示
  • 松开后恢复原速度
  • 锁定后所有手势失效
  • 锁定状态图标变色,底部显示提示
  • 点击锁图标可解锁

7.3 聊天输入框

  • 切换录音/文字模式时无明显抖动
  • 切换有平滑的淡入淡出效果

八、经验总结

  1. 状态判断要完整?? true 这种默认值设计容易导致边界情况问题,应该明确考虑所有状态组合

  2. media_kit 主题系统强大:通过 MaterialVideoControlsThemeData 可以精细控制各种边距、手势开关

  3. AnimatedSwitcher 解决切换抖动:配合 KeyedSubtree 可以优雅地处理组件切换动画

  4. 功能与锁定的协调:新增功能时要考虑与锁定状态的兼容性,锁定时应禁用相关功能

  5. 多平台命名一致性:修改 App 名称需要同时修改 Android、iOS、Windows、macOS、Linux 等平台的配置文件


九、后续优化方向

  1. 长按加速倍率可配置:允许我在设置中选择 2x 或 3x
  2. iOS 平台 App 名称:需要修改 ios/Runner/Info.plist 中的 CFBundleDisplayName
  3. 图片缓存 LRU 策略:当缓存图片过多时,自动清理最久未使用的图片
  4. 锁定状态持久化:记住锁定偏好(可选)