Flutter弹幕效果
JulyYu 人气:3前言
需求要点如下:
- 弹幕行数为3行,每条弹幕相互依靠但不存在重叠
- 每条弹幕可交互点击跳转
- 滚动速度恒定 触摸不可暂停播放
- 弹幕数据固定一百条且支持轮询播放
弹幕排序规则如下:
1 4 7
2 5 8
3 6 9
通用弹幕实现方案
Flutter Dev Package
已有开源弹幕实现组件,这里举例barrage_page
的实现方式(大多数实现底层逻辑基本一样)。
基本架构采用Stack然后向布局中提交弹幕布局,添加时设置好弹幕偏移量来设置弹幕位置。
Stack(fit: StackFit.expand, children: <Widget>[ widget.child, _controller.isEnabled ? Stack( fit: StackFit.loose, children: <Widget>[] ..addAll(_widgets.values ?? const SizedBox())) : const SizedBox(), ]); });
但因为每条弹幕可能会出现重叠情况无法合理定位每条弹幕的位置因此放弃该方案。
PS:widget只有在build到布局后才能获取到它基础信息(相对位置信息,宽高等)就无法计算出所有弹幕的位置信息。
ListView弹幕方案实现
最先想到使用瀑布流flutter_staggered_grid_view
实现弹幕布局但由于组件暂时不支持横向布局就放弃了。
基本框架
采用三个ListView实现每一行弹幕效果。虽然不太推荐以这种形式实现但从快速实现效果来说是比较简单便捷兜底方案。(可以实现但不推荐)
Container( height: 200, child: Column( children: [ Expanded( child: ListView.builder( scrollDirection: Axis.horizontal, controller: scrollController1, itemBuilder: (context, index) { return Common.getWidget(index, height: 30, width: random.nextInt(100).toDouble()); }, ), ), Expanded( child: ListView.builder( scrollDirection: Axis.horizontal, controller: scrollController2, itemBuilder: (context, index) { return Common.getWidget(index, height: 30, width: random.nextInt(100).toDouble()); }, )), Expanded( child: ListView.builder( scrollDirection: Axis.horizontal, controller: scrollController3, itemBuilder: (context, index) { return Common.getWidget(index, height: 30, width: random.nextInt(100).toDouble()); }, )) ], ), )
轮播滚动
添加定时器periodic
定时每秒钟执行一次scrollController
的animateTo
方法移动偏移量并且偏移量不断累加。
其次ListView
支持无限滑动只要ListView.builder
不设置itemCount
就能实现。
Timer _timer; scroll = () { offset += 100; scrollController1.animateTo(offset, duration: Duration(seconds: 1), curve: Curves.linear); scrollController2.animateTo(offset, duration: Duration(seconds: 1), curve: Curves.linear); scrollController3.animateTo(offset, duration: Duration(seconds: 1), curve: Curves.linear); }; _timer = Timer.periodic(Duration(seconds: 1), (timer) { scroll(); });
轮询算法
ListView
支持无限滑动后itemBuilder
回调下标Index
会超出数据源最大值。因此数据源也需要支持无限轮询来配合列表滚动。start
表示弹幕开始取值,这里设置为(0,1,2);index
表示itemBuilder
回调下标Index
。
int findIndex(int start, int index) { index = start + index * 3; if (expressList.length < index) { index = index % (expressList.length - 1); // 取余 } else if (expressList.length == index) { // 是否是最后一个数据 index = start; if (index >= expressList.length) { // 还需要判断数据源是否比start还小 index = (index % expressList.length - 1); } } return index; }
点击事件
一切都实现得很顺利最终就是弹幕点击实现。但实际上当ListView
的scrollController
在执行animateTo
时其实点击操作是失效的,ListView
无法响应点击事件。只有当animateTo
操作结束之后再执行点击才能执行点击。因此若要实现这个功能只能先将Timer
暂停再执行一次点击,再一次点击不可能是用户再去触发,这里只能采用模拟点击形式实现。
PS:ListView
无法响应点击事件具体原因还待研究,个人猜测列表做动画时对外部触摸事件进行了屏蔽处理。
GestureDetector( onTapUp: (details){ // 点击抬起之后暂停定时器 _timer?.cancel(); // 模拟一次点击 Timer(Duration(milliseconds: 100),() { GestureBinding.instance.handlePointerEvent(PointerAddedEvent(pointer: 0,position: details.globalPosition)); GestureBinding.instance.handlePointerEvent(PointerDownEvent(pointer: 0,position: details.globalPosition)); GestureBinding.instance.handlePointerEvent(PointerUpEvent(pointer: 0,position: details.globalPosition)); }); }, child: ListView.builder( controller: scrollController, physics: NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return GestureDetector( behavior: HitTestBehavior.opaque, child: Common.getWidget(index), onTap: () { // 内部响应点击事件 然后重新设置定时器滚动列表 _timer = Timer.periodic(Duration(seconds: 1), (timer) { scroll(); }); }, ); }, ), );
加载全部内容