绚丽电影APP

Flutter for OpenHarmony 实战:视频播放器深度定制与弹幕系统实现

前言

在 Splendid Movie 项目中,播放详情页 (Player Screen) 无疑是用户停留时间最长、交互最密集的页面。除了基础的视频播放,我们需要加入现代视频应用标配的“灵魂功能”——弹幕系统,并优化播放控制体验。

本文将拆解如何在 OpenHarmony 上利用 Flutter 实现一个轻量级、高性能的弹幕引擎,并处理视频播放状态中的各种边界情况(如重播时的状态回跳)。

在这里插入图片描述
在这里插入图片描述

一、 引入视频播放器插件

在这里插入图片描述

在这里插入图片描述

在 OpenHarmony 平台上使用 Flutter 播放视频,我们需要依赖社区提供的适配版本。请在 pubspec.yaml 中添加如下配置:

dependencies:
  flutter:
    sdk: flutter
  
  # 视频播放器核心包
  video_player: ^2.8.2
  
  # OpenHarmony 平台适配实现
  video_player_ohos:
    git:
      url: https://gitee.com/openharmony-sig/flutter_packages.git
      path: packages/video_player/video_player_ohos

📌 注意:由于鸿蒙生态仍在快速迭代,建议优先使用 Gitee 上的 openharmony-sig 仓库源码依赖,以获取最新的功能支持和 Bug 修复。

二、 播放器架构设计:层叠布局的胜利

一个成熟的播放器界面,本质上是一个复杂的 Stack 布局。我们需要将不同的功能层按顺序叠加,确保交互互不干扰。

2.1 UI 分层结构

Stack
├── Layer 1: Video Surface (底层视频画面)
├── Layer 2: Black Overlay (暂停/缓冲时的暗色遮罩)
├── Layer 3: Danmaku Layer (弹幕层,位于视频之上,控件之下)
└── Layer 4: Controls Overlay (顶层控制面板:进度条、按钮)

这种分层设计的最大好处是:弹幕不会挡住控制按钮,但会遮挡视频内容,符合用户习惯

2.2 核心代码实现框架

// lib/screens/player_screen.dart片段

Widget _buildVideoPlayer() {
  return AspectRatio(
    aspectRatio: 16 / 9,
    child: Stack(
      children: [
        // 1. 视频渲染层 (VideoPlayer 插件)
        Positioned.fill(
          child: _isInitialized 
              ? VideoPlayer(_videoController)
              : Image.network(widget.movie.posterUrl), // 占位图
        ),

        // 2. 弹幕层 (仅在播放时运行)
        if (_isPlaying)
          Positioned.fill(
             child: _buildDanmakuLayer(), // 下文详解
          ),

        // 3. 控制层 (点击显示/隐藏)
        AnimatedOpacity(
          opacity: _showControls ? 1.0 : 0.0,
          duration: const Duration(milliseconds: 300),
          child: _buildControls(),
        ),
      ],
    ),
  );
}

三、 纯 Flutter 手写轻量级弹幕引擎

在这里插入图片描述

市面上有成熟的弹幕库,但往往过于繁重。针对 Splendid Movie,我们需要一个极致轻量(<50行核心代码)的实现。

3.1 弹幕模型设计

class DanmakuItem {
  String text;
  double top;      // 垂直轨道位置
  double position; // 水平位置 (left offset)
  Color color;     // 弹幕颜色

  DanmakuItem({
    required this.text, 
    required this.top, 
    required this.position, 
    required this.color
  });
}

3.2 驱动引擎:AnimationController

我们不需要复杂的物理引擎,只需要一个不断重复的 AnimationController 来驱动每一帧的位置更新。

// 在 State 中初始化
late AnimationController _danmakuController;
final List<DanmakuItem> _activeDanmakus = [];


void initState() {
  // 定义一个 10秒 周期的控制器,不仅用于定时,更作为心跳驱动
  _danmakuController = AnimationController(
    vsync: this, 
    duration: const Duration(seconds: 10)
  )..repeat();

  _danmakuController.addListener(() {
    // 1. 生成逻辑:每一帧有 2% 的概率生成新弹幕
    if (Random().nextInt(100) < 2) {
      _addDanmaku();
    }
    
    // 2. 移动逻辑:更新所有存活弹幕的位置
    setState(() {
      for (var item in _activeDanmakus) {
        item.position -= 1.5; // 🚀 移动速度
      }
      // 3. 回收逻辑:移除跑出屏幕左侧的弹幕
      _activeDanmakus.removeWhere((item) => item.position < -100);
    });
  });
}

void _addDanmaku() {
  _activeDanmakus.add(DanmakuItem(
    text: "高能预警!", // 实际项目中可从弹幕池随机取
    top: 20.0 + Random().nextInt(150), // 随机轨道高度
    position: MediaQuery.of(context).size.width, // 从屏幕最右侧生成
    color: Colors.white.withOpacity(0.8),
  ));
}

2.3 渲染层:ClipRect + Stack

Widget _buildDanmakuLayer() {
  return ClipRect( // 关键!裁掉跑出屏幕外的弹幕
    child: Stack(
      children: _activeDanmakus.map((d) => Positioned(
        left: d.position,
        top: d.top,
        child: Text(d.text,
          style: TextStyle(
            color: d.color, 
            shadows: [Shadow(blurRadius: 2, color: Colors.black)] // 描边增加可读性
          )
        ),
      )).toList(),
    ),
  );
}

三、 交互优化:重播“无敌”护盾

在这里插入图片描述

在开发过程中,我们发现一个体验痛点:当视频播放结束 (_isEnded = true) 用户点击“重播”时,视频状态会瞬间跳回“结束”,导致重播失败。这是因为原生播放器的状态更新有延迟。

3.1 问题复现

  1. 用户点击重播 -> 代码调用 seekTo(0)play()
  2. VideoPlayer 这里需要一点时间。
  3. 在此期间,原生层下一帧回调发来:position: total_duration
  4. Flutter 逻辑误判为“又播放完了”,再次暂停视频。

3.2 解决方案:时间窗护盾

我们在重播动作触发时,开启一个 1.5 秒的“无敌时间”,在此期间无视所有的播放结束信号。

DateTime? _activeReplayTime;

void _togglePlay() {
  if (_isEnded) {
    // 🛡️ 开启 1.5秒 面移护盾
    _activeReplayTime = DateTime.now(); 
    
    setState(() {
      _isEnded = false;
      _showControls = true;
    });

    // 并发执行,不等待 future,让 UI 立即响应
    _videoController.seekTo(Duration.zero);
    _videoController.play();
  } else {
    // 普通暂停/播放逻辑...
  }
}

// 在监听器中
_videoController.addListener(() {
  // ...
  if (total > 0 && current >= total && !_isEnded) {
    // 🛡️ 检查护盾是否生效
    if (_activeReplayTime != null && 
        DateTime.now().difference(_activeReplayTime!) < const Duration(milliseconds: 1500)) {
       return; // 依然在护盾保护期,无视结束信号
    }
    
    setState(() => _isEnded = true);
  }
});

这个简单的逻辑完美解决了跨平台播放器常见的状态回跳问题,体验极度丝滑。


四、 OpenHarmony 适配细节

在鸿蒙设备上播放视频时,要注意网络权限和编解码支持。

  1. 网络权限:确保 module.json5 中已声明 ohos.permission.INTERNET
  2. HTTPS 支持:鸿蒙默认策略较严,建议视频源统一使用 HTTPS 协议,且证书合法。
  3. 全屏切换:目前 Flutter for OpenHarmony 的全屏切换尚需配合原生 Ability 调用,建议暂时使用伪全屏(隐藏 UI)方案替代。

五、 总结

这一篇我们深入了 PlayerScreen 的核心:

  • 构建了稳健的四层播放器架构
  • 手写了弹幕引擎,无需引入重型依赖包。
  • 利用时间戳护盾解决了异步状态管理的 Bug。

下一篇,我们将离开播放器,转向业务层。【业务篇】全栈业务页构建 — 从电影商城到个人中心,我们将探讨网格布局、沉浸式个人页头以及如何复用我们封装好的组件。


📦 完整代码已上传至 AtomGitsplendid_movie

🌐 欢迎加入开源鸿蒙跨平台社区开源鸿蒙跨平台开发者社区

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐