flutter鸿蒙:实现类似B站或抖音的弹幕功能

1. 前言

需要借助插件实现,目前推荐几款插件都是纯Dart语言开发的,虽然没有单独适配鸿蒙,但是可以在鸿蒙设备上使用,缺点是,会有轻微掉帧的情况。

本人使用设备:华为mate60 Pro

2. 插件仓库链接

  • flutter_barrage:

https://github.com/danielwii/flutter_barrage

  • flutter_easy_barrage:

https://github.com/Avengong/flutter_easy_barrage

  • flutter_barrage_craft:

https://github.com/taxze6/flutter_barrage_craft

  • canvas_danmaku:

https://github.com/Predidit/canvas_danmaku

3. 核心代码

  1. flutter_barrage:

    library flutter_barrage;

    import 'dart:async';
    import 'dart:collection';
    import 'dart:math';

    import 'package:flutter/material.dart';
    import 'package:quiver/collection.dart';

    const TAG = 'FlutterBarrage';

    class BarrageWall extends StatefulWidget {
    final BarrageWallController controller;

    /// the bullet widget
    final Widget child;

    /// time in seconds of bullet show in screen
    final int speed;

    /// used to adjust speed for each channel
    final int speedCorrectionInMilliseconds;

    final double width;
    final double height;

    /// will not send bullets to the area is safe from bottom, default is 0
    /// used to not cover the subtitles
    final int safeBottomHeight;

    /// [disable] by default, set to true will overwrite other bullets
    final bool massiveMode;

    /// used to make barrage tidy
    final double maxBulletHeight;

    /// enable debug mode, will display a debug panel with information
    final bool debug;
    final bool selfCreatedController;

    BarrageWall({
    List<Bullet>? bullets,
    BarrageWallController? controller,
    ValueNotifier<BarrageValue>? timelineNotifier,
    this.speed = 5,
    this.child = const SizedBox(),
    required this.width,
    required this.height,
    this.massiveMode = false,
    this.maxBulletHeight = 16,
    this.debug = false,
    this.safeBottomHeight = 0,
    this.speedCorrectionInMilliseconds = 3000,
    }) : controller = controller ??
    BarrageWallController.withBarrages(bullets,
    timelineNotifier: timelineNotifier),
    selfCreatedController = controller == null {
    if (controller != null) {
    this.controller.value = controller.value.size == 0
    ? BarrageWallValue.fromList(bullets ?? [])
    : controller.value;
    this.controller.timelineNotifier =
    controller.timelineNotifier ?? timelineNotifier;
    }
    }

    @override
    State<StatefulWidget> createState() => _BarrageState();
    }

    /// It's a class that holds the position of a bullet
    class BulletPos {
    int id;
    int channel;
    double position; // from right to left
    double width;
    bool released = false;
    int lifetime;
    Widget widget;

    BulletPos(
    {required this.id,
    required this.channel,
    required this.position,
    required this.width,
    required this.widget})
    : lifetime = DateTime.now().millisecondsSinceEpoch;

    updateWith({required double position, double width = 0}) {
    this.position = position;
    this.width = width > 0 ? width : this.width;
    this.lifetime = DateTime.now().millisecondsSinceEpoch;
    // debugPrint("[TAG] update to this");
    }

    bool get hasExtraSpace {
    return position > width + 8;
    }

    @override
    String toString() {
    return 'BulletPos{id: id, channel: channel, position: position, width: width, released: released, widget: widget}';
    }
    }

    class _BarrageState extends State<BarrageWall> with TickerProviderStateMixin {
    late BarrageWallController _controller;
    Random _random = new Random();
    // int _processed = 0;
    // double? _width;
    // double? _height;
    double? _lastHeight; // 上一次计算通道个数的的高度记录
    late Timer _cleaner;

    double? _maxBulletHeight;
    int? _totalChannels;
    int? _channelMask;
    // Map<dynamic, BulletPos> _lastBullets = {};
    List<int> _speedCorrectionForChannels = [];

    int _calcSafeHeight(double height) {
    if (height.isInfinite) {
    final toHeight = context.size!.height;
    debugPrint("[TAG] height is infinite, set it to toHeight");
    return toHeight.toInt();
    } else {
    final safeBottomHeight =
    _controller.safeBottomHeight ?? widget.safeBottomHeight;
    final toHeight = height - safeBottomHeight;
    debugPrint(
    '[TAG] safe bottom height: safeBottomHeight, set safe height to toHeight'); if (toHeight < 0) { throw Exception( 'safe bottom height is too large, it should be less than height');
    }
    return toHeight.toInt();
    }
    }

    /// null means no available channels exists
    int? _nextChannel() {
    final _randomSeed = _totalChannels! - 1;

    复制代码
     if (_controller.usedChannel ^ _channelMask! == 0) {
       return null;
     }
    
     var times = 1;
     var channel = _randomSeed == 0 ? 0 : _random.nextInt(_randomSeed);
     var channelCode = 1 << channel;
    
     while (_controller.usedChannel & channelCode != 0 &&
         _controller.usedChannel ^ _channelMask! != 0) {
       times++;
       channel = channel >= _totalChannels! ? 0 : channel + 1;
       channelCode = 1 << channel;
    
       /// return random channel if no channels available and massive mode is enabled
       if (times > _totalChannels!) {
         if (widget.massiveMode == true) {
           return _random.nextInt(_randomSeed);
         }
         return null;
       }
     }
     // _controller.usedChannel |= (1 << channel);
     _controller.updateChannel((usedChannel) => usedChannel |= (1 << channel));
     return channel;

    }

    _releaseChannels() {
    // final now = DateTime.now().millisecondsSinceEpoch;
    for (int i = 0; i < _controller.lastBullets.length; i++) {
    final channel = _controller.lastBullets.keys.elementAt(i);
    var isNotReleased = !_controller.lastBullets[channel]!.released;
    var liveTooLong =
    false; // now - _controller.lastBullets[channel].lifetime > widget.speed * 2 * 1000 + widget.speedCorrectionInMilliseconds;
    if (liveTooLong ||
    (isNotReleased && _controller.lastBullets[channel]!.hasExtraSpace)) {
    _controller.lastBullets[channel]!.released = true;
    // _controller.usedChannel &= _channelMask! ^ 1 << channel;
    _controller.updateChannel(
    (usedChannel) => usedChannel &= _channelMask! ^ 1 << channel);
    }
    }
    }

    void _handleBullets(
    BuildContext context, {
    required List<Bullet> bullets,
    required double width,
    double? end,
    }) {
    // cannot get the width of widget when not rendered, make a twice longer width for now
    end ??= width * 2;

    复制代码
     _releaseChannels();
     if (widget.debug)
       debugPrint(
           '[$TAG] handle bullets: ${bullets.length} - ${_controller.usedChannel.toRadixString(2)}');
     bullets.forEach((Bullet bullet) {
       AnimationController animationController;
    
       final nextChannel = _nextChannel();
       if (nextChannel != null) {}
    
       /// discard bullets do not have available channel and massive mode is not enabled too
       if (nextChannel == null) {
         return;
       }
    
       final showTimeInMilliseconds =
           widget.speed * 2 * 1000 - _speedCorrectionForChannels[nextChannel];
       animationController = AnimationController(
           duration: Duration(milliseconds: showTimeInMilliseconds),
           vsync: this);
       Animation<double> animation = new Tween<double>(begin: 0, end: end)
           .animate(animationController..forward());
    
       final channelHeightPos = nextChannel * _maxBulletHeight!;
    
       /// make bullets not showed up in same time
       final fixedWidth = width + _random.nextInt(20).toDouble();
       final bulletWidget = AnimatedBuilder(
         animation: animation,
         child: bullet.child,
         builder: (BuildContext context, Widget? child) {
           if (animation.isCompleted) {
             _controller.lastBullets[nextChannel]
                 ?.updateWith(position: double.infinity);
             return const SizedBox();
           }
    
           double widgetWidth = 0.0;
    
           /// get current widget width
           if (child != null) {
             final renderBox = context.findRenderObject() as RenderBox?;
             if (renderBox?.hasSize ?? false) {
               widgetWidth = renderBox!.size.width;
    
               /// 通过计算出的 widget width 在判断弹幕完全移出了可视区域
               if (widgetWidth > 0 &&
                   animation.value > (fixedWidth + widgetWidth)) {
                 _controller.lastBullets[nextChannel]
                     ?.updateWith(position: double.infinity);
                 return const SizedBox();
               }
             }
           }

    // debugPrint(
    // '[TAG] {_controller.lastBullets[nextChannel]?.id} == {context.hashCode} child pos: {animation.value}'); // 【通道不为空】或者【通道的最后元素】之后出现了可以新增的元素 if (!_controller.lastBullets.containsKey(nextChannel) || (_controller.lastBullets.containsKey(nextChannel) && _controller.lastBullets[nextChannel]!.position > animation.value)) { _controller.lastBullets[nextChannel] = BulletPos( id: context.hashCode, channel: nextChannel, position: animation.value, width: widgetWidth, widget: child!); // debugPrint("[TAG] add {_controller.lastBullets[nextChannel]} - {context.hashCode}");
    } else if (_controller.lastBullets[nextChannel]!.id ==
    context.hashCode) {
    // 当前元素是最后元素,更新相关信息
    _controller.lastBullets[nextChannel]
    ?.updateWith(position: animation.value, width: widgetWidth);
    } // 其他情况直接更新页面元素

    复制代码
           final widthPos = fixedWidth - animation.value;
           return Transform.translate(
             offset: Offset(widthPos, channelHeightPos.toDouble()),
             child: child,
           );
         },
       );
       _controller.widgets.putIfAbsent(animationController, () => bulletWidget);
     });

    }

    @override
    void didUpdateWidget(BarrageWall oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller) {
    _controller = widget.controller;
    }
    }

    void handleBullets() {
    if (_controller.isEnabled && _controller.value.waitingList.isNotEmpty) {
    final recallNeeded = _lastHeight != widget.height || _channelMask == null;

    复制代码
       if (_totalChannels == null || recallNeeded) {
         _lastHeight = widget.height;
         _maxBulletHeight = widget.maxBulletHeight;
         _totalChannels = _calcSafeHeight(widget.height) ~/ _maxBulletHeight!;
         debugPrint("[$TAG] total channels: ${_totalChannels! + 1}");
         _channelMask = (2 << _totalChannels!) - 1;
    
         for (var i = 0; i <= _totalChannels!; i++) {
           final nextSpeed = widget.speedCorrectionInMilliseconds > 0
               ? _random.nextInt(widget.speedCorrectionInMilliseconds)
               : 0;
           _speedCorrectionForChannels.add(nextSpeed);
         }
       }
    
       _handleBullets(
         context,
         bullets: _controller.value.waitingList,
         width: widget.width,
       );
       // _processed += _controller.value.waitingList.length;
       setState(() {});
     }

    }

    @override
    void initState() {
    _controller = widget.controller;
    _controller.initialize();

    复制代码
     _controller.addListener(handleBullets);
     _controller.enabledNotifier.addListener(() {
       setState(() {});
     });
    
     _cleaner = Timer.periodic(Duration(milliseconds: 100), (timer) {
       _controller.widgets.removeWhere((controller, widget) {
         if (controller.isCompleted) {
           controller.dispose();
           return true;
         }
         return false;
       });
     });
    
     super.initState();

    }

    @override
    void dispose() {
    debugPrint('[$TAG] dispose');
    _cleaner.cancel();
    _controller.clear();
    _controller.removeListener(handleBullets);
    if (widget.selfCreatedController) {
    _controller.dispose();
    }
    super.dispose();
    }

    @override
    Widget build(BuildContext context) {
    return Stack(fit: StackFit.expand, children: <Widget>[
    if (widget.debug)
    Container(
    color: Colors.lightBlueAccent.withOpacity(0.7),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    mainAxisAlignment: MainAxisAlignment.end,
    children: <Widget>[
    Text('BarrageWallValue: {_controller.value}'), Text( 'TimelineNotifier: {_controller.timelineNotifier?.value}'),
    Text('Timeline: {_controller.timeline}'), Text('Bullets: {_controller.widgets.length}'),
    Text(
    'UsedChannels: {_controller.usedChannel.toRadixString(2)}'), Text('LastBullets[0]: {_controller.lastBullets[0]}'),
    ])),
    widget.child,
    if (_controller.isEnabled)
    Stack(
    fit: StackFit.loose,
    children: <Widget>[..._controller.widgets.values]
    // ..addAll(_widgets.values ?? const SizedBox()),
    ),
    ]);
    }
    }

    typedef int KeyCalculator<T>(T t);

    class HashList<T> {
    /// key is the showTime in minutes
    Map<int, TreeSet<T>> _map = new HashMap();
    final Comparator<T>? comparator;
    final KeyCalculator<T> keyCalculator;

    HashList({required this.keyCalculator, this.comparator});

    void appendByMinutes(List<T> values) {
    values.forEach((value) {
    int key = keyCalculator(value);
    if (_map.containsKey(key)) {
    _map[key]!.add(value);
    } else {
    _map.putIfAbsent(
    key,
    () => TreeSet<T>(
    comparator: comparator ?? (dynamic a, b) => a.compareTo(b))
    ..add(value));
    }
    });
    }

    @override
    String toString() {
    return 'HashList{$_map}';
    }
    }

    class BarrageValue {
    final int timeline;
    final bool isPlaying;

    BarrageValue({this.timeline = -1, this.isPlaying = false});

    BarrageValue copyWith({int? timeline, bool? isPlaying}) => BarrageValue(
    timeline: timeline ?? this.timeline,
    isPlaying: isPlaying ?? this.isPlaying);

    @override
    String toString() {
    return 'BarrageValue{timeline: timeline, isPlaying: isPlaying}';
    }
    }

    class BarrageWallValue {
    final int showedTimeBefore;
    final int size;
    final int processedSize;
    final List<Bullet> waitingList;

    final HashList<Bullet> bullets;

    BarrageWallValue.fromList(List<Bullet> bullets,
    {this.showedTimeBefore = 0, this.waitingList = const []})
    : bullets = HashList<Bullet>(
    keyCalculator: (t) => Duration(milliseconds: t.showTime).inMinutes)
    ..appendByMinutes(bullets),
    size = bullets.length,
    processedSize = 0;

    BarrageWallValue({
    required this.bullets,
    this.showedTimeBefore = 0,
    this.waitingList = const [],
    this.size = 0,
    this.processedSize = 0,
    });

    BarrageWallValue copyWith({
    // int lastProcessedTime,
    required int processedSize,
    int? showedTimeBefore,
    List<Bullet>? waitingList,
    }) =>
    BarrageWallValue(
    bullets: bullets,
    showedTimeBefore: showedTimeBefore ?? this.showedTimeBefore,
    waitingList: waitingList ?? this.waitingList,
    size: this.size,
    processedSize: this.processedSize + processedSize,
    );

    @override
    String toString() {
    return 'BarrageWallValue{showedTimeBefore: showedTimeBefore, size: size, processed: processedSize, waitings: {waitingList.length}}';
    }
    }

    class BarrageWallController extends ValueNotifier<BarrageWallValue> {
    Map<AnimationController, Widget> _widgets = new LinkedHashMap();
    Map<dynamic, BulletPos> _lastBullets = {};
    int _usedChannel = 0;

    int timeline = 0;
    ValueNotifier<bool> enabledNotifier = ValueNotifier(true);
    bool _isDisposed = false;

    ValueNotifier<BarrageValue>? timelineNotifier;
    int? safeBottomHeight;
    Timer? _timer;

    bool get isEnabled => enabledNotifier.value;
    Map<AnimationController, Widget> get widgets => _widgets;
    Map<dynamic, BulletPos> get lastBullets => _lastBullets;
    int get usedChannel => _usedChannel;

    BarrageWallController({List<Bullet>? bullets, this.timelineNotifier})
    : super(BarrageWallValue.fromList(bullets ?? const []));

    BarrageWallController.withBarrages(List<Bullet>? bullets,
    {this.timelineNotifier})
    : super(BarrageWallValue.fromList(bullets ?? const []));

    Future<void> initialize() async {
    final Completer<void> initializingCompleter = Completer<void>();

    复制代码
     if (timelineNotifier == null) {
       _timer = Timer.periodic(const Duration(milliseconds: 100),
           (Timer timer) async {
         if (_isDisposed) {
           timer.cancel();
           return;
         }
    
         if (value.size == value.processedSize) {
           /*
           timer.cancel();*/
           return;
         }
    
         timeline += 100;
         tryFire();
       });
     } else {
       timelineNotifier!.addListener(_handleTimelineNotifier);
     }
    
     initializingCompleter.complete();
     return initializingCompleter.future;

    }

    /// reset the controller to new time state
    void reset(int showedTimeBefore) {
    value = value.copyWith(
    showedTimeBefore: showedTimeBefore, waitingList: [], processedSize: 0);
    }

    void updateChannel(Function(int usedChannel) onUpdate) {
    _usedChannel = onUpdate(_usedChannel);
    }

    /// clear all firing bullets
    void clear() {
    /// reset all widgets animation and clear the list
    widgets.forEach((controller, widget) => controller.dispose());
    widgets.clear();
    // release channels
    _usedChannel = 0;
    }

    void _handleTimelineNotifier() {
    final offset = (timeline - timelineNotifier!.value.timeline);
    final ifNeedReset = offset.abs() > 1000;
    if (ifNeedReset) {
    debugPrint("[TAG] offset: offset call reset to $timeline...");
    reset(timelineNotifier!.value.timeline);
    }
    if (timelineNotifier != null) timeline = timelineNotifier!.value.timeline;
    tryFire();
    }

    tryFire({List<Bullet> bullets = const []}) {
    final key = Duration(milliseconds: timeline).inMinutes;
    final exists = value.bullets._map.containsKey(key);

    复制代码
     if (exists || bullets.isNotEmpty) {
       List<Bullet> toBePrecessed = value.bullets._map[key]
               ?.where((barrage) =>
                   barrage.showTime > value.showedTimeBefore &&
                   barrage.showTime <= timeline)
               .toList() ??
           [];
    
       if (toBePrecessed.isNotEmpty || bullets.isNotEmpty) {
         value = value.copyWith(
             showedTimeBefore: timeline,
             waitingList: toBePrecessed..addAll(bullets),
             processedSize: toBePrecessed.length);
       }
     }

    }

    disable() {
    debugPrint("[TAG] disable barrage ... current: enabledNotifier");
    enabledNotifier.value = false;
    }

    enable() {
    debugPrint("[TAG] enable barrage ... current: enabledNotifier");
    enabledNotifier.value = true;
    }

    send(List<Bullet> bullets) {
    tryFire(bullets: bullets);
    }

    @override
    Future<void> dispose() async {
    if (!_isDisposed) {
    _timer?.cancel();
    }
    _isDisposed = true;
    timelineNotifier?.dispose();
    enabledNotifier.dispose();
    super.dispose();
    }
    }

    class Bullet implements Comparable<Bullet> {
    final Widget child;

    /// in milliseconds
    final int showTime;

    const Bullet({required this.child, this.showTime = 0});

    @override
    String toString() {
    return 'Bullet{child: child, showTime: showTime}';
    }

    @override
    int compareTo(Bullet other) {
    return showTime.compareTo(other.showTime);
    }
    }

    这段代码实现了一个 Flutter 弹幕组件,核心思路是通过时间线管理弹幕触发、通道划分避免重叠、动画控制移动轨迹,最终实现从右向左流动的弹幕效果。以下是核心逻辑拆解:

    1. 核心组件与职责划分
      BarrageWall:弹幕容器组件(StatefulWidget),负责渲染弹幕和管理视图状态,依赖控制器处理业务逻辑。
      BarrageWallController:控制器,核心角色是管理弹幕数据(待显示、已显示)、时间线(触发时机)和资源释放。
      Bullet:弹幕实体,包含要显示的 Widget 和触发时间(showTime,毫秒级)。
      BulletPos:弹幕位置跟踪器,记录弹幕所在通道、当前位置、宽度等,用于判断是否释放通道。
    2. 弹幕生命周期管理(核心流程)
      (1)初始化与时间线驱动
      控制器初始化时启动定时器(或监听外部时间通知),定期更新时间线(timeline,毫秒级)。
      时间线每更新一次,触发tryFire方法,从存储的弹幕列表中筛选出 "当前时间应显示" 的弹幕(showTime在已显示时间和当前时间之间),放入waitingList待处理。
      (2)通道划分与分配(避免重叠)
      将弹幕容器垂直划分为多个 "通道":通道数量 = 安全高度(总高度 - 底部安全区域)÷ 最大弹幕高度(maxBulletHeight)。
      用位掩码(usedChannel)记录已占用的通道(例如,第 0 通道占用则usedChannel=0b0001)。
      _nextChannel方法通过位运算判断空闲通道,优先分配空闲通道;若通道满且开启massiveMode,则随机分配(允许重叠)。
      (3)动画控制与位置更新
      为每个待显示弹幕创建AnimationController,动画时长由基础速度(speed)和通道修正值(speedCorrectionInMilliseconds)决定,实现不同通道速度差异。
      动画值从 0 到容器宽度的 2 倍(Tween(begin:0, end:width*2)),通过AnimatedBuilder实时更新弹幕位置:
      水平位置:width - 动画值(从右侧进入,向左移动)。
      垂直位置:通道索引 × 最大弹幕高度(固定在所属通道)。
      通过BulletPos跟踪弹幕实时位置和宽度,当弹幕完全移出屏幕(position > 宽度 + 自身宽度),标记为 "已释放" 并更新位掩码释放通道。
      (4)资源清理
      定时器(_cleaner)每 100ms 检查一次已完成的动画控制器,销毁并从列表中移除,避免内存泄漏。
      当容器销毁或控制器禁用时,清理所有动画资源并重置通道状态。
    3. 关键特性支持
      时间线同步:支持外部时间通知(timelineNotifier),可与视频等场景同步弹幕显示。
      安全区域:通过safeBottomHeight预留底部区域(如字幕区),弹幕不进入该区域。
      调试模式:开启debug后显示调试面板,包含弹幕数量、通道占用等信息。
      海量模式:massiveMode开启时,通道满后仍强制分配,允许弹幕重叠以保证高并发显示。
      总结
      核心逻辑可概括为:"时间线触发弹幕 → 通道分配避免重叠 → 动画控制移动轨迹 → 实时跟踪并释放资源",通过控制器与视图分离、位运算高效管理通道、动画驱动位置更新,实现了流畅且可配置的弹幕效果。
  2. flutter_easy_barrage:

    import 'dart:async';
    import 'dart:collection';
    import 'dart:math';
    import 'package:flutter/material.dart';

    class EasyBarrage extends StatefulWidget {
    final double width;
    final double height;

    final int rowNum;

    /// 行轨道之间的行间距高度
    final double rowSpaceHeight;

    ///一行中,每个item的水平间距宽度
    final double itemSpaceWidth;
    final Duration duration;

    ///是否随机
    final bool randomItemSpace;
    final EasyBarrageController controller;

    ///弹幕从某个位置开始出现,默认是0
    final double originStart;

    /// 轨道下标:对应轨道弹幕延迟出现的时间
    final Map<int, Duration>? channelDelayMap;

    /// 默认从右到左
    final TransitionDirection direction;

    EasyBarrage({
    Key? key,
    required this.width,
    required this.height,
    required this.controller,
    this.itemSpaceWidth = 45,
    this.rowNum = 3,
    this.channelDelayMap,
    this.originStart = 0,
    this.direction = TransitionDirection.rtl,
    this.randomItemSpace = false,
    this.duration = const Duration(seconds: 5),
    this.rowSpaceHeight = 10,
    }) : super(key: key);

    @override
    State<StatefulWidget> createState() {
    return EasyBarrageState();
    }
    }

    class EasyBarrageState extends State<EasyBarrage> {
    List<BarrageLineController> controllers = [];
    late EasyBarrageController _controller;
    final Random _random = Random();

    Timer? _timeline;

    @override
    void initState() {
    _controller = widget.controller;
    _timeline = Timer.periodic(const Duration(milliseconds: 50), (timer) {
    for (var element in controllers) {
    element.tick();
    }
    });
    _controller.addListener(handleBarrages);
    var rows = widget.rowNum;
    for (int i = 0; i < rows; i++) {
    BarrageLineController barrageController = BarrageLineController();
    controllers.add(barrageController);
    }

    复制代码
     _controller.speedNotify.addListener(() {
       controllers.forEach((element) {
         element.updateSpeed(_controller.speedNotify.value);
       });
     });
    
     super.initState();

    }

    @override
    void dispose() {
    releaseTimeLine();
    controllers.clear();
    super.dispose();
    }

    void handleBarrages() {
    double totalSpaceWidth = (widget.rowNum - 1) * widget.itemSpaceWidth;
    _controller.totalMapBarrageItems.forEach((key, value) {
    BarrageLineController ctrl = controllers[key];
    dispatch(ctrl, value, key, _controller.slideWidth + totalSpaceWidth);
    });

    复制代码
     if (_controller.totalBarrageItems.isNotEmpty) {
       for (int i = 0; i < controllers.length; i++) {
         List<BarrageItem> templist = [];
         templist.addAll(_controller.totalBarrageItems);
         dispatch(controllers[i], templist, i, _controller.slideWidth + totalSpaceWidth);
       }
     }

    }

    @override
    Widget build(BuildContext context) {
    return Container(
    width: widget.width,
    height: widget.height,
    child: Column(
    children: [...barrageLines()],
    ),
    );
    }

    List<Widget> barrageLines() {
    List<Widget> list = [];
    int rows = widget.rowNum;
    double height = ((widget.height - (rows - 1) * (widget.rowSpaceHeight)) / rows);
    for (int i = 0; i < rows; i++) {
    list.add(BarrageLine(
    direction: widget.direction,
    controller: controllers[i],
    fixedWidth: widget.width,
    itemSpaceWidth: widget.itemSpaceWidth,
    originStart: widget.originStart,
    height: height,
    duration: widget.duration,
    ));
    if (i != rows - 1) {
    list.add(SizedBox(
    height: widget.rowSpaceHeight,
    ));
    }
    }
    return list;
    }

    void _randTrigger(BarrageLineController ctrl, List<BarrageItem> value, double slideWidth) {
    if (_random.nextBool()) {
    Future.delayed(Duration(milliseconds: _random.nextInt(800)), () {
    ctrl.trigger(value, slideWidth);
    });
    } else {
    ctrl.trigger(value, slideWidth);
    }
    }

    void dispatch(BarrageLineController ctrl, List<BarrageItem> value, int channelIndex, double slideWidth) {
    if (widget.channelDelayMap != null) {
    Duration? duration = widget.channelDelayMap![channelIndex];
    if (duration != null) {
    Future.delayed(duration, () {
    ctrl.trigger(value, slideWidth);
    });
    } else {
    _randTrigger(ctrl, value, slideWidth);
    }
    } else {
    _randTrigger(ctrl, value, slideWidth);
    }
    }

    void releaseTimeLine() {
    _timeline?.cancel();
    _timeline = null;
    }
    }

    class EasyBarrageController extends ValueNotifier<BarrageItemValue> {
    List<BarrageItem> totalBarrageItems = [];
    HashMap<int, List<BarrageItem>> totalMapBarrageItems = HashMap<int, List<BarrageItem>>();
    ValueNotifier<int> speedNotify=ValueNotifier<int>(0);
    double slideWidth = 0;

    EasyBarrageController() : super(BarrageItemValue());

    void sendBarrage(List<BarrageItem> items) {
    clearCache();
    totalBarrageItems.addAll(items);

    复制代码
     double maxW = 0;
     double totalW = 0;
     for (var element in totalBarrageItems) {
       maxW = max(maxW, element.itemWidth);
       totalW += element.itemWidth;
     }
     slideWidth = totalW;
     value = BarrageItemValue(widgets: totalBarrageItems, slideWidth: slideWidth);

    }

    void sendChannelMapBarrage(HashMap<int, List<BarrageItem>>? channelMapItems) {
    if (channelMapItems != null) {
    clearCache();
    totalMapBarrageItems.addAll(channelMapItems);
    double totalWidth = 0;
    double originmaxW = 0;
    totalMapBarrageItems.forEach((key, value) {
    double totalW = 0;
    for (var element in value) {
    originmaxW = max(originmaxW, element.itemWidth);
    totalW += element.itemWidth;
    }
    totalWidth = max(totalWidth, totalW);
    });
    slideWidth = totalWidth;

    复制代码
       value = BarrageItemValue(mapItems: totalMapBarrageItems, slideWidth: slideWidth);
     }

    }

    void stop() {
    ///todo
    }

    void clearCache() {
    totalMapBarrageItems.clear();
    totalBarrageItems.clear();
    }

    void updateSpeed(int milliseconds) {
    speedNotify.value=milliseconds;

    }

    }

    typedef HandleComplete = void Function();

    class BarrageLine extends StatefulWidget {
    const BarrageLine(
    {required this.controller,
    Key? key,
    // this.bgchild,
    this.duration = const Duration(seconds: 5),
    this.onHandleComplete,
    this.itemSpaceWidth = 45,
    this.randomItemSpace = false,
    this.originStart = 0,
    required this.fixedWidth,
    required this.height,
    this.direction = TransitionDirection.rtl})
    : super(key: key);

    final double height;

    final bool randomItemSpace;
    final double itemSpaceWidth;

    /// 平移时间(秒)
    ///
    final Duration duration;

    final double originStart;

    ///弹幕展示的宽度
    final double fixedWidth;
    final BarrageLineController controller;

    ///
    /// 平移方向
    final TransitionDirection direction;
    final HandleComplete? onHandleComplete;

    getComplete() {}

    @override
    State<StatefulWidget> createState() => _BarrageLineState();
    }

    class _BarrageLineState extends State<BarrageLine> with TickerProviderStateMixin {
    // double _width = 0;
    // double _height = 0;
    late BarrageLineController controller;
    final Random _random = Random();
    bool hasCalled = false;

    @override
    void initState() {
    controller = widget.controller;
    controller.addListener(handleBarrages);
    controller.tickNotifier.addListener(() {
    handleWaitingBarrages();
    });

    复制代码
     super.initState();

    }

    @override
    void dispose() {
    controller.destroy();
    controller.removeListener(handleBarrages);
    super.dispose();
    }

    void handleBarrages() {}

    void handleWaitingBarrages() {
    double originStart = widget.originStart;

    复制代码
     if (controller.hasNoItem()) {
       if (!hasCalled) {
         widget.onHandleComplete?.call();
         controller._tickCont=1;
         hasCalled = true;
       }
       return;
     }
     hasCalled = false;
     if (!controller.hasExtraSpace(
         widget.randomItemSpace ? (_random.nextInt((widget.itemSpaceWidth + originStart).toInt())).toDouble() : (widget.itemSpaceWidth + originStart),
         widget.direction)) {
       return;
     }
     var element = controller.next();
     // double childWidth = controller.maxWidth;
    
     controller.lastBarrage(element.id, widget.fixedWidth);
     var duration=widget.duration;
     int? milliseconds=controller.milliseconds;
     if(milliseconds!=null){
       duration=Duration(milliseconds: milliseconds);
     }
     Animation<double> animation;
     AnimationController animationController = AnimationController(duration: duration, vsync: this)
       ..addStatusListener((status) {
         if (status == AnimationStatus.completed) {
           controller.barrageItems.removeWhere((element2) => element2.id == element.id);
         }
       });
    
     var begin = originStart;
     var end = widget.fixedWidth * 2; // 暂时设置为展示宽度的2倍,理论上应该是 fixedWidth+widget本身的长度。这样可以保证速度一致。
     // var end = widget.fixedWidth + childWidth + originStart; // 精准!但是有个问题,如果每次的弹幕宽度不一致,会导致速度不一样
    
     animation = Tween(begin: begin, end: end).animate(animationController..forward());
    
     var widgetBarrage = AnimatedBuilder(
       animation: animation,
       child: element.item,
       builder: (BuildContext context, Widget? child) {
         if (animation.isCompleted) {
           controller.updateLastItemPosition(BarrageItemPosition(animationValue: double.infinity, id: element.id));
           return const SizedBox();
         }
         double widgetWidth = 0.0;
    
         if (child != null) {
           RenderObject? renderBox = context.findRenderObject();
           if (renderBox != null) {
             var rb = renderBox as RenderBox;
             if (rb.hasSize == true) {
               widgetWidth = renderBox.size.width;
               if (widgetWidth > 0 && animation.value >= (widget.fixedWidth + widgetWidth - 2)) {
                 controller.updateLastItemPosition(BarrageItemPosition(id: element.id, animationValue: double.infinity));
                 return const SizedBox();
               }
             }
           }
         }
    
         var widthPos = widget.fixedWidth - animation.value;
         if (widget.direction == TransitionDirection.rtl) {
           widthPos = widget.fixedWidth - animation.value;
         } else if (widget.direction == TransitionDirection.ltr) {
           widthPos = animation.value - element.itemWidth;
         }
         controller.updateLastItemPosition(BarrageItemPosition(animationValue: animation.value, id: element.id, widgetWidth: widgetWidth));
         const heightPos = .0;
         return Transform.translate(
           offset: Offset(widthPos, heightPos),
           child: child,
         );
       },
     );
     controller.widgets.putIfAbsent(animationController, () => widgetBarrage);
     if(mounted){
       setState(() {});
     }

    }

    @override
    Widget build(BuildContext context) {
    return Container(
    alignment: Alignment.center,
    height: widget.height,
    // color: Colors.greenAccent,
    child: LayoutBuilder(builder: (_, snapshot) {
    // _width = widget.fixedWidth ?? snapshot.maxWidth;
    // _height = widget.height ?? snapshot.maxHeight;
    return Stack(
    fit: StackFit.expand,
    // alignment: Alignment.centerLeft,
    children: <Widget>[
    // widget.child,
    Stack(fit: StackFit.loose, alignment: Alignment.centerLeft, children: <Widget>[...controller.widgets.values]),
    ]);
    }),
    );
    }
    }

    class BarrageLineController extends ValueNotifier<BarrageItemValue> {
    List<BarrageItem> barrageItems = [];
    double maxWidth = 0;

    Map<AnimationController, Widget> get widgets => _widgets;
    BarrageItemPosition? _itemPosition;

    final Map<AnimationController, Widget> _widgets = {};

    BarrageLineController() : super(BarrageItemValue());

    ValueNotifier<int> get tickNotifier => _tickNotifier;
    final ValueNotifier<int> _tickNotifier = ValueNotifier(0);
    int? milliseconds;
    int _tickCont = 1;

    void trigger(List<BarrageItem> items, double localMaxWidth) {
    barrageItems.addAll(items);
    maxWidth = localMaxWidth;

    复制代码
     value = BarrageItemValue(widgets: barrageItems);

    }

    void updateLastItemPosition(BarrageItemPosition itemPosition) {
    if (_itemPosition?.id == itemPosition.id) {
    _itemPosition?.animationValue = itemPosition.animationValue;
    _itemPosition?.widgetWidth = itemPosition.widgetWidth;
    }
    }

    bool hasExtraSpace(double itemSpaceWidth, TransitionDirection direction) {
    return _itemPosition == null || ((_itemPosition!.animationValue) > (_itemPosition!.widgetWidth + itemSpaceWidth));
    }

    void destroy() {
    dispose();
    _tickCont = 0;
    widgets.forEach((key, value) {
    key.dispose();
    });
    widgets.clear();
    barrageItems.clear();
    tickNotifier.dispose();
    }

    void lastBarrage(String itemId, double fixedWidth) {
    _itemPosition ??= BarrageItemPosition(id: itemId);
    _itemPosition!.id = itemId;
    _itemPosition!.fixedWidth = fixedWidth;
    }

    void tick() {
    widgets.removeWhere((controller, widget) {
    if (controller.isCompleted) {
    controller.dispose();
    return true;
    }
    return false;
    });

    复制代码
     tickNotifier.value = _tickCont++;

    }

    bool hasNoItem() {
    return barrageItems.isEmpty;
    }

    BarrageItem next() {
    return barrageItems.removeAt(0);
    }

    void updateSpeed(int milliseconds) {
    this.milliseconds=milliseconds;
    }

    }

    class BarrageItemPosition {
    double animationValue = 0, fixedWidth = 0, widgetWidth = 0;
    String id;

    BarrageItemPosition({this.animationValue = 0, this.fixedWidth = 0, this.widgetWidth = 0, required this.id});
    }

    class BarrageItemValue {
    List<BarrageItem>? widgets;
    HashMap<int, List<BarrageItem>>? mapItems;

    double slideWidth; //用来计算行程

    BarrageItemValue({this.widgets, this.mapItems, this.slideWidth = 0});
    }

    class BarrageItem {
    Widget item;
    double itemWidth;
    String id = "";
    final Random _random = Random();

    BarrageItem({required this.item, required this.itemWidth}) {
    id = "{DateTime.now().toIso8601String()}:{_random.nextInt(1000)}";
    }
    }

    enum TransitionDirection {
    ///
    /// 从左到右
    ///
    ltr,

    ///
    /// 从右到左
    ///
    rtl,

    ///
    /// 从上到下
    ///
    // ttb, todo

    ///
    /// 从下到上
    ///
    // btt todo
    }

    这段代码实现了一个更轻量、可配置的多轨道弹幕组件(EasyBarrage),核心思路是通过垂直划分固定轨道、轨道内弹幕队列管理、动画控制移动方向与速度,实现可定制的弹幕流效果。以下是核心逻辑拆解:

    1. 核心组件与职责划分
      EasyBarrage:弹幕容器(StatefulWidget),负责整体布局(划分多行轨道),协调轨道控制器与数据更新。
      EasyBarrageController:全局控制器,管理待发送的弹幕数据(支持普通列表和按轨道划分的映射),提供发送、清空、速度控制等接口。
      BarrageLine:单轨道组件(每行弹幕),负责渲染当前轨道的弹幕,通过动画控制弹幕移动。
      BarrageLineController:单轨道控制器,管理该轨道的弹幕队列、动画资源和位置跟踪,确保轨道内弹幕不重叠。
      BarrageItem:弹幕实体,包含要显示的 Widget、宽度(提前指定)和唯一 ID(用于跟踪)。
    2. 弹幕生命周期管理(核心流程)
      (1)初始化与轨道划分
      容器初始化时,根据rowNum创建对应数量的BarrageLineController(每个对应一行轨道),并启动定时器(每 50ms 触发一次tick)。
      轨道高度计算:总高度减去轨道间距(rowSpaceHeight)后,平均分配给每行(height = (总高度 - (行数-1)间距) / 行数)。
      (2)弹幕发送与分配
      发送方式:支持两种发送模式:
      普通模式:通过sendBarrage发送弹幕列表,由容器平均分配到所有轨道。
      轨道指定模式:通过sendChannelMapBarrage发送按轨道索引划分的弹幕映射(HashMap<int, List<BarrageItem>>),精准分配到指定轨道。
      分配逻辑:发送时计算弹幕总宽度(用于动画行程),并通知容器处理。容器通过dispatch方法将弹幕分发给对应轨道的控制器,支持:
      轨道延迟:通过channelDelayMap为指定轨道设置弹幕延迟出现时间。
      随机延迟:无轨道延迟时,随机添加 0-800ms 延迟,避免所有轨道弹幕同步出现。
      (3)轨道内弹幕调度(核心逻辑)
      队列管理:每个轨道的BarrageLineController维护一个弹幕队列(barrageItems),通过定时器的tick触发调度。
      空间检查:调度时先判断轨道内是否有足够空间(基于上一个弹幕的位置和间距):
      间距规则:通过itemSpaceWidth配置水平间距,支持随机间距(randomItemSpace)。
      空间判断:hasExtraSpace方法检查上一个弹幕的位置是否超过 "自身宽度 + 间距",确保不重叠。
      动画创建:若有空间,从队列取出下一个弹幕,创建AnimationController:
      方向控制:默认从右到左(rtl),也支持从左到右(ltr),通过Tween控制动画值范围(起始位置originStart到结束位置fixedWidth
      2)。
      速度控制:基础时长由duration配置,支持通过updateSpeed动态修改动画时长(改变速度)。
      (4)位置更新与资源清理
      实时位置跟踪:通过BarrageItemPosition记录弹幕的动画值、宽度等,AnimatedBuilder实时更新位置:
      从右到左:widthPos = 轨道宽度 - 动画值(从右侧进入,向左移动)。
      从左到右:widthPos = 动画值 - 弹幕宽度(从左侧进入,向右移动)。
      资源清理:当弹幕完全移出轨道(动画完成或位置超出轨道宽度),销毁动画控制器并从列表中移除,避免内存泄漏。
    3. 关键特性支持
      多轨道隔离:垂直划分为rowNum行轨道,轨道间通过rowSpaceHeight分隔,避免垂直方向重叠。
      灵活方向:支持从右到左(默认)和从左到右两种移动方向(TransitionDirection)。
      间距控制:可配置固定水平间距,或开启随机间距使弹幕分布更自然。
      动态速度:通过控制器的updateSpeed实时调整动画时长,改变弹幕移动速度。
      精准分配:支持按轨道指定弹幕(sendChannelMapBarrage),满足特定轨道显示特定内容的需求。
      总结
      核心逻辑可概括为:"容器划分多轨道 → 控制器分配弹幕到轨道 → 轨道内按队列调度(检查空间) → 动画驱动弹幕按方向 / 速度移动 → 实时跟踪并清理资源"。通过轨道隔离、队列管理和灵活配置,实现了轻量且可定制的弹幕效果,适合需要简单集成多轨道弹幕的场景。
  3. flutter_barrage_craft:

    import 'package:flutter/material.dart';
    import 'package:flutter_barrage_craft/src/barrage_controller.dart';
    import 'package:flutter_barrage_craft/src/config/barrage_config.dart';
    import 'package:flutter_barrage_craft/src/model/barrage_model.dart';

    class BarrageView extends StatefulWidget {
    const BarrageView({Key? key, required this.controller}) : super(key: key);

    final BarrageController controller;

    @override
    State<BarrageView> createState() => _BarrageViewState();
    }

    class _BarrageViewState extends State<BarrageView> {
    @override
    void initState() {
    super.initState();
    widget.controller.setState = setState;
    }

    @override
    void dispose() {
    super.dispose();
    widget.controller.dispose();
    }

    Widget buildBarrage(BuildContext context, BarrageModel barrageModel) {
    return Positioned(
    right: barrageModel.offsetX,
    top: barrageModel.offsetY,
    child: GestureDetector(
    onTap: () => BarrageConfig.barrageTapCallBack(barrageModel),
    onDoubleTap: () => BarrageConfig.barrageDoubleTapCallBack(barrageModel),
    child: barrageModel.barrageWidget,
    ),
    );
    }

    List<Widget> buildAllBarrage(BuildContext context) {
    return List.generate(
    widget.controller.barrages.length,
    (index) => buildBarrage(
    context,
    widget.controller.barrages[index],
    ),
    );
    }

    @override
    Widget build(BuildContext context) {
    return Stack(
    children: [...buildAllBarrage(context)],
    );
    }
    }

  4. canvas_danmaku:

    import 'package:canvas_danmaku/utils/utils.dart';
    import 'package:flutter/material.dart';
    import 'models/danmaku_item.dart';
    import 'scroll_danmaku_painter.dart';
    import 'static_danmaku_painter.dart';
    import 'danmaku_controller.dart';
    import 'dart:ui' as ui;
    import 'models/danmaku_option.dart';
    import '/models/danmaku_content_item.dart';
    import 'dart:math';

    class DanmakuScreen extends StatefulWidget {
    // 创建Screen后返回控制器
    final Function(DanmakuController) createdController;
    final DanmakuOption option;

    const DanmakuScreen({
    required this.createdController,
    required this.option,
    super.key,
    });

    @override
    State<DanmakuScreen> createState() => _DanmakuScreenState();
    }

    class _DanmakuScreenState extends State<DanmakuScreen>
    with TickerProviderStateMixin, WidgetsBindingObserver {
    /// 视图宽度
    double _viewWidth = 0;

    /// 弹幕控制器
    late DanmakuController _controller;

    /// 弹幕动画控制器
    late AnimationController _animationController;

    /// 静态弹幕动画控制器
    late AnimationController _staticAnimationController;

    /// 弹幕配置
    DanmakuOption _option = DanmakuOption();

    /// 滚动弹幕
    final List<DanmakuItem> _scrollDanmakuItems = [];

    /// 顶部弹幕
    final List<DanmakuItem> _topDanmakuItems = [];

    /// 底部弹幕
    final List<DanmakuItem> _bottomDanmakuItems = [];

    /// 弹幕高度
    late double _danmakuHeight;

    /// 弹幕轨道数
    late int _trackCount;

    /// 弹幕轨道位置
    final List<double> _trackYPositions = [];

    /// 内部计时器
    late int _tick;

    /// 运行状态
    bool _running = true;

    /// 因进入后台而暂停
    bool _pauseInBackground = false;

    @override
    void initState() {
    super.initState();
    // 计时器初始化
    _tick = 0;
    _startTick();
    _option = widget.option;
    _controller = DanmakuController(
    onAddDanmaku: addDanmaku,
    onUpdateOption: updateOption,
    onPause: pause,
    onResume: resume,
    onClear: clearDanmakus,
    );
    _controller.option = _option;
    widget.createdController.call(
    _controller,
    );

    复制代码
     _animationController = AnimationController(
       vsync: this,
       duration: Duration(seconds: _option.duration),
     )..repeat();
    
     _staticAnimationController = AnimationController(
       vsync: this,
       duration: Duration(seconds: _option.duration),
     );
    
     WidgetsBinding.instance.addObserver(this);

    }

    /// 处理 Android/iOS 应用后台或熄屏导致的动画问题
    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
    if ([
    AppLifecycleState.paused,
    AppLifecycleState.detached,
    ].contains(state) &&
    _pauseInBackground &&
    _running) {
    _pauseInBackground = true;
    pause();
    } else if (_pauseInBackground) {
    _pauseInBackground = false;
    resume();
    }
    super.didChangeAppLifecycleState(state);
    }

    @override
    void dispose() {
    _running = false;
    WidgetsBinding.instance.removeObserver(this);
    _animationController.dispose();
    _staticAnimationController.dispose();
    super.dispose();
    }

    /// 添加弹幕
    void addDanmaku(DanmakuContentItem content) {
    if (!_running || !mounted) {
    return;
    }
    // 在这里提前创建 Paragraph 缓存防止卡顿
    final textPainter = TextPainter(
    text: TextSpan(
    text: content.text,
    style: TextStyle(
    fontSize: _option.fontSize,
    fontWeight: FontWeight.values[_option.fontWeight])),
    textDirection: TextDirection.ltr,
    )..layout();
    final danmakuWidth = textPainter.width;
    final danmakuHeight = textPainter.height;

    复制代码
     final ui.Paragraph paragraph = Utils.generateParagraph(
         content, danmakuWidth, _option.fontSize, _option.fontWeight, _option.opacity);
    
     ui.Paragraph? strokeParagraph;
     if (_option.strokeWidth > 0) {
       strokeParagraph = Utils.generateStrokeParagraph(content, danmakuWidth,
           _option.fontSize, _option.fontWeight, _option.strokeWidth, _option.opacity);
     }
    
     int idx = 1;
     for (double yPosition in _trackYPositions) {
       if (content.type == DanmakuItemType.scroll && !_option.hideScroll) {
         bool scrollCanAddToTrack =
             _scrollCanAddToTrack(yPosition, danmakuWidth);
    
         if (scrollCanAddToTrack) {
           _scrollDanmakuItems.add(DanmakuItem(
               yPosition: yPosition,
               xPosition: _viewWidth,
               width: danmakuWidth,
               height: danmakuHeight,
               creationTime: _tick,
               content: content,
               paragraph: paragraph,
               strokeParagraph: strokeParagraph));
           break;
         }
    
         /// 无法填充自己发送的弹幕时强制添加
         if (content.selfSend && idx == _trackCount) {
           _scrollDanmakuItems.add(DanmakuItem(
               yPosition: _trackYPositions[0],
               xPosition: _viewWidth,
               width: danmakuWidth,
               height: danmakuHeight,
               creationTime: _tick,
               content: content,
               paragraph: paragraph,
               strokeParagraph: strokeParagraph));
           break;
         }
    
         /// 海量弹幕启用时进行随机添加
         if (_option.massiveMode && idx == _trackCount) {
           final random = Random();
           var randomYPosition =
               _trackYPositions[random.nextInt(_trackYPositions.length)];
           _scrollDanmakuItems.add(DanmakuItem(
               yPosition: randomYPosition,
               xPosition: _viewWidth,
               width: danmakuWidth,
               height: danmakuHeight,
               creationTime: _tick,
               content: content,
               paragraph: paragraph,
               strokeParagraph: strokeParagraph));
           break;
         }
       }
    
       if (content.type == DanmakuItemType.top && !_option.hideTop) {
         bool topCanAddToTrack = _topCanAddToTrack(yPosition);
    
         if (topCanAddToTrack) {
           _topDanmakuItems.add(DanmakuItem(
               yPosition: yPosition,
               xPosition: _viewWidth,
               width: danmakuWidth,
               height: danmakuHeight,
               creationTime: _tick,
               content: content,
               paragraph: paragraph,
               strokeParagraph: strokeParagraph));
           break;
         }
       }
    
       if (content.type == DanmakuItemType.bottom && !_option.hideBottom) {
         bool bottomCanAddToTrack = _bottomCanAddToTrack(yPosition);
    
         if (bottomCanAddToTrack) {
           _bottomDanmakuItems.add(DanmakuItem(
               yPosition: yPosition,
               xPosition: _viewWidth,
               width: danmakuWidth,
               height: danmakuHeight,
               creationTime: _tick,
               content: content,
               paragraph: paragraph,
               strokeParagraph: strokeParagraph));
           break;
         }
       }
       idx++;
     }
    
     if ((_scrollDanmakuItems.isNotEmpty ||
             _topDanmakuItems.isNotEmpty ||
             _bottomDanmakuItems.isNotEmpty) &&
         !_animationController.isAnimating) {
       _animationController.repeat();
     }
    
     /// 重绘静态弹幕
     setState(() {
       _staticAnimationController.value = 0;
     });

    }

    /// 暂停
    void pause() {
    if (!mounted) return;
    if (_running) {
    setState(() {
    _running = false;
    });
    if (_animationController.isAnimating) {
    _animationController.stop();
    }
    }
    }

    /// 恢复
    void resume() {
    if (!mounted) return;
    if (!_running) {
    setState(() {
    _running = true;
    });
    if (!_animationController.isAnimating) {
    _animationController.repeat();
    // 重启计时器
    _startTick();
    }
    }
    }

    /// 更新弹幕设置
    void updateOption(DanmakuOption option) {
    bool needRestart = false;
    bool needClearParagraph = false;
    if (_animationController.isAnimating) {
    _animationController.stop();
    needRestart = true;
    }

    复制代码
     if (option.fontSize != _option.fontSize) {
       needClearParagraph = true;
     }
    
     /// 需要隐藏弹幕时清理已有弹幕
     if (option.hideScroll && !_option.hideScroll) {
       _scrollDanmakuItems.clear();
     }
     if (option.hideTop && !_option.hideTop) {
       _topDanmakuItems.clear();
     }
     if (option.hideBottom && !_option.hideBottom) {
       _bottomDanmakuItems.clear();
     }
     _option = option;
     _controller.option = _option;
    
     /// 清理已经存在的 Paragraph 缓存
     if (needClearParagraph) {
       for (DanmakuItem item in _scrollDanmakuItems) {
         if (item.paragraph != null) {
           item.paragraph = null;
         }
         if (item.strokeParagraph != null) {
           item.strokeParagraph = null;
         }
       }
       for (DanmakuItem item in _topDanmakuItems) {
         if (item.paragraph != null) {
           item.paragraph = null;
         }
         if (item.strokeParagraph != null) {
           item.strokeParagraph = null;
         }
       }
       for (DanmakuItem item in _bottomDanmakuItems) {
         if (item.paragraph != null) {
           item.paragraph = null;
         }
         if (item.strokeParagraph != null) {
           item.strokeParagraph = null;
         }
       }
     }
     if (needRestart) {
       _animationController.repeat();
     }
     setState(() {});

    }

    /// 清空弹幕
    void clearDanmakus() {
    if (!mounted) return;
    setState(() {
    _scrollDanmakuItems.clear();
    _topDanmakuItems.clear();
    _bottomDanmakuItems.clear();
    });
    _animationController.stop();
    }

    /// 确定滚动弹幕是否可以添加
    bool _scrollCanAddToTrack(double yPosition, double newDanmakuWidth) {
    for (var item in _scrollDanmakuItems) {
    if (item.yPosition == yPosition) {
    final existingEndPosition = item.xPosition + item.width;
    // 首先保证进入屏幕时不发生重叠,其次保证知道移出屏幕前不与速度慢的弹幕(弹幕宽度较小)发生重叠
    if (_viewWidth - existingEndPosition < 0) {
    return false;
    }
    if (item.width < newDanmakuWidth) {
    if ((1 -
    ((_viewWidth - item.xPosition) / (item.width + _viewWidth))) >
    ((_viewWidth) / (_viewWidth + newDanmakuWidth))) {
    return false;
    }
    }
    }
    }
    return true;
    }

    /// 确定顶部弹幕是否可以添加
    bool _topCanAddToTrack(double yPosition) {
    for (var item in _topDanmakuItems) {
    if (item.yPosition == yPosition) {
    return false;
    }
    }
    return true;
    }

    /// 确定底部弹幕是否可以添加
    bool _bottomCanAddToTrack(double yPosition) {
    for (var item in _bottomDanmakuItems) {
    if (item.yPosition == yPosition) {
    return false;
    }
    }
    return true;
    }

    // 基于Stopwatch的计时器同步
    void _startTick() async {
    final stopwatch = Stopwatch()..start();
    int lastElapsedTime = 0;

    复制代码
     while (_running && mounted) {
       await Future.delayed(const Duration(milliseconds: 1));
       int currentElapsedTime = stopwatch.elapsedMilliseconds; // 获取当前的已用时间
       int delta = currentElapsedTime - lastElapsedTime; // 计算自上次记录以来的时间差
       _tick += delta;
       lastElapsedTime = currentElapsedTime; // 更新最后记录的时间
       if (lastElapsedTime % 100 == 0) {
         // 移除屏幕外滚动弹幕
         _scrollDanmakuItems
             .removeWhere((item) => item.xPosition + item.width < 0);
         // 移除顶部弹幕
         _topDanmakuItems.removeWhere((item) =>
             ((_tick - item.creationTime) > (_option.duration * 1000)));
         // 移除底部弹幕
         _bottomDanmakuItems.removeWhere((item) =>
             ((_tick - item.creationTime) > (_option.duration * 1000)));
    
         /// 重绘静态弹幕
         if (mounted) {
           setState(() {
             _staticAnimationController.value = 0;
           });
         }
       }
     }
    
     stopwatch.stop();

    }

    @override
    Widget build(BuildContext context) {
    /// 计算弹幕轨道
    final textPainter = TextPainter(
    text: TextSpan(text: '弹幕', style: TextStyle(fontSize: _option.fontSize)),
    textDirection: TextDirection.ltr,
    )..layout();
    _danmakuHeight = textPainter.height;
    return LayoutBuilder(builder: (context, constraints) {
    /// 计算视图宽度
    if (constraints.maxWidth != _viewWidth) {
    _viewWidth = constraints.maxWidth;
    }

    复制代码
       /// 为字幕留出余量
       _trackCount =
           (constraints.maxHeight * _option.area / _danmakuHeight).floor() - 1;
    
       _trackYPositions.clear();
       for (int i = 0; i < _trackCount; i++) {
         _trackYPositions.add(i * _danmakuHeight);
       }
       return ClipRect(
         child: IgnorePointer(
           child: Stack(children: [
             RepaintBoundary(
                 child: AnimatedBuilder(
               animation: _animationController,
               builder: (context, child) {
                 return CustomPaint(
                   painter: ScrollDanmakuPainter(
                       _animationController.value,
                       _scrollDanmakuItems,
                       _option.duration,
                       _option.fontSize,
                       _option.fontWeight,
                       _option.strokeWidth,
                       _option.opacity,
                       _danmakuHeight,
                       _running,
                       _tick),
                   child: Container(),
                 );
               },
             )),
             RepaintBoundary(
                 child: AnimatedBuilder(
               animation: _staticAnimationController,
               builder: (context, child) {
                 return CustomPaint(
                   painter: StaticDanmakuPainter(
                       _staticAnimationController.value,
                       _topDanmakuItems,
                       _bottomDanmakuItems,
                       _option.duration,
                       _option.fontSize,
                       _option.fontWeight,
                       _option.strokeWidth,
                       _option.opacity,
                       _danmakuHeight,
                       _running,
                       _tick),
                   child: Container(),
                 );
               },
             )),
           ]),
         ),
       );
     });

    }
    }

    这段代码实现了一个高性能的弹幕渲染组件(DanmakuScreen),核心思路是通过自定义绘制(CustomPaint) 替代传统Widget树渲染,结合轨道管理动画驱动,支持滚动、顶部、底部三种弹幕类型,兼顾性能与灵活性。以下是核心逻辑拆解:

    1. 核心组件与职责划分

    • DanmakuScreen:弹幕容器(StatefulWidget),负责管理弹幕状态、轨道计算、动画控制和生命周期,是整个弹幕系统的核心协调者。
    • DanmakuController:外部交互接口,提供添加弹幕、更新配置、暂停/恢复、清空等方法,解耦UI与业务逻辑。
    • ScrollDanmakuPainter / StaticDanmakuPainter:自定义画笔,分别负责绘制滚动弹幕(从右向左移动)和静态弹幕(顶部/底部固定),直接操作Canvas提升渲染性能。
    • DanmakuItem:弹幕实体,包含位置、尺寸、文本内容、绘制缓存(Paragraph)等信息,是绘制的最小单元。
    • DanmakuOption:配置项,支持自定义字体大小、权重、描边、透明度、显示时长、轨道区域比例等,灵活控制弹幕样式和行为。

    2. 弹幕生命周期管理(核心流程)

    (1)初始化与轨道划分

    • 初始化时创建动画控制器(_animationController用于滚动弹幕,_staticAnimationController用于静态弹幕)和计时器(_startTick),计时器通过毫秒级精度的Stopwatch同步时间(_tick),用于计算弹幕生命周期。
    • 轨道计算:根据屏幕高度、弹幕高度(基于字体大小)和显示区域比例(option.area),垂直划分多个轨道(_trackYPositions),每个轨道高度等于单条弹幕高度,避免垂直方向重叠。

    (2)弹幕添加与轨道分配

    • 添加逻辑:通过addDanmaku方法接收弹幕内容(DanmakuContentItem),提前创建文本绘制缓存(ui.Paragraph和描边strokeParagraph),减少绘制时的计算开销(避免实时计算文本尺寸导致卡顿)。
    • 类型区分
      • 滚动弹幕:检查目标轨道是否有足够空间(_scrollCanAddToTrack),确保新弹幕与轨道内已有弹幕不重叠(通过位置和宽度计算);若开启"海量模式"(massiveMode)或为"自己发送的弹幕",强制分配到随机轨道或首轨道。
      • 顶部/底部弹幕:每个轨道只能有一条(_topCanAddToTrack/_bottomCanAddToTrack),避免重叠,固定显示一段时间后自动消失。

    (3)绘制与动画驱动

    • 滚动弹幕:由ScrollDanmakuPainter绘制,通过_animationController的动画值(0~1)计算位置:
      • 移动逻辑:根据弹幕创建时间(creationTime)和总时长(option.duration),计算当前应处的X坐标(从右向左移动,超出左边界后被清理)。
      • 绘制优化:使用RepaintBoundary隔离滚动弹幕区域,避免与静态弹幕互相触发重绘。
    • 静态弹幕:由StaticDanmakuPainter绘制,顶部弹幕固定在轨道顶部,底部弹幕固定在轨道底部,根据存在时间(_tick - creationTime)判断是否过期(超过duration后被清理)。

    (4)生命周期维护与清理

    • 计时器(_startTick)每100ms触发一次清理:
      • 滚动弹幕:移除X坐标+宽度 < 0(完全移出左边界)的弹幕。
      • 静态弹幕:移除存在时间超过duration的弹幕。
    • 应用生命周期适配:监听AppLifecycleState,在应用进入后台或暂停时停止动画,恢复时重启,避免后台无效消耗资源。

    3. 性能优化核心策略

    • 自定义绘制:使用CustomPaint直接操作Canvas,替代传统Widget树渲染,减少布局计算和Widget重建开销,适合高并发弹幕场景。
    • 文本缓存:提前创建ui.Paragraph(文本绘制对象),避免每次绘制时重新计算文本尺寸和样式,提升绘制效率。
    • 轨道隔离:垂直划分轨道,严格控制同一轨道内弹幕的位置关系,避免重叠检查的全局遍历,降低计算复杂度。
    • 区域隔离:通过RepaintBoundary将滚动弹幕和静态弹幕分为两个绘制区域,各自独立重绘,减少不必要的刷新。
    • 按需清理:定期移除过期或离屏弹幕,保持弹幕列表精简,避免绘制时遍历大量无效数据。

    4. 灵活性与可配置性

    • 多类型支持:同时支持滚动(从右向左)、顶部(固定)、底部(固定)三种弹幕类型,满足不同场景需求。
    • 样式自定义:通过DanmakuOption配置字体大小、权重、描边宽度、透明度、显示时长等,灵活适配UI风格。
    • 行为控制:支持隐藏特定类型弹幕(hideScroll/hideTop/hideBottom)、开启海量模式(允许重叠)、暂停/恢复/清空等操作,适配交互需求。

    总结

    核心逻辑可概括为:"轨道划分隔离弹幕 → 自定义绘制提升性能 → 动画与计时器驱动生命周期 → 配置项支持灵活定制"。通过直接操作Canvas、提前缓存文本资源、严格轨道管理,在保证高并发弹幕流畅显示的同时,提供了丰富的自定义能力,适合视频、直播等需要高密度弹幕的场景。

4. 运行效果

  1. flutter_barrage:
  1. flutter_easy_barrage:
  1. flutter_barrage_craft:
  1. canvas_danmaku:

觉得有帮助,可以点赞或收藏

相关推荐
鸿蒙小白龙3 小时前
OpenHarmony后台服务开发指南:ServiceAbility与ServiceExtensionAbility全解析
harmonyos·鸿蒙系统·open harmony
运维行者_4 小时前
DDI 与 OpManager 集成对企业 IT 架构的全维度优化
运维·网络·数据库·华为·架构·1024程序员节·snmp监控
用户096 小时前
Flutter插件与包的本质差异
android·flutter·面试
浅蓝色7 小时前
flutter平台判断,这次应该没问题了。支持鸿蒙,插件已发布
flutter·harmonyos
怀君7 小时前
Flutter——打印之PdfPreview功能详细教程
flutter
我是华为OD~HR~栗栗呀9 小时前
华为od-22届考研-C++面经
java·前端·c++·python·华为od·华为·面试
我是华为OD~HR~栗栗呀9 小时前
华为OD, 测试面经
java·c++·python·华为od·华为·面试
我是华为OD~HR~栗栗呀11 小时前
华为OD-23届-测试面经
java·前端·c++·python·华为od·华为·面试
我是华为OD~HR~栗栗呀11 小时前
华为od面经-23届-Java面经
java·c语言·c++·python·华为od·华为·面试