Flutter:跑马灯公告栏

组件

js 复制代码
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:ducafe_ui_core/ducafe_ui_core.dart';

class MarqueeNotice extends StatefulWidget {
  /// 公告数据列表,每条公告包含title和desc
  final List<Map<String, String>> notices;
  
  /// 滚动速度,数值越小滚动越快(像素/秒)
  final double scrollSpeed;
  
  /// 背景颜色
  final Color backgroundColor;
  
  /// 公告栏高度
  final double height;
  
  /// 公告图标
  final Widget? icon;

  /// 公告文本颜色
  final Color textColor;
  
  /// 公告字体大小
  final double fontSize;
  
  /// 公告条目间距
  final double itemSpacing;

  const MarqueeNotice({
    super.key,
    required this.notices,
    this.scrollSpeed = 50.0,
    this.backgroundColor = Colors.transparent,
    this.height = 40,
    this.icon,
    this.textColor = Colors.white,
    this.fontSize = 24,
    this.itemSpacing = 60,
  });

  @override
  State<MarqueeNotice> createState() => _MarqueeNoticeState();
}

class _MarqueeNoticeState extends State<MarqueeNotice> with SingleTickerProviderStateMixin {
  late ScrollController _scrollController;
  late Timer _timer;
  late List<Map<String, String>> _extendedNotices;
  double _scrollOffset = 0.0;
  double _maxScrollExtent = 0.0;
  
  @override
  void initState() {
    super.initState();
    
    // 扩展公告列表使其可以循环滚动(复制一份公告列表)
    _extendedNotices = [...widget.notices, ...widget.notices];
    
    _scrollController = ScrollController();
    
    // 在布局完成后开始滚动
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _startScrolling();
    });
  }
  
  // 开始滚动动画
  void _startScrolling() {
    // 测量滚动区域总宽度的一半(原始公告列表的宽度)
    _maxScrollExtent = _scrollController.position.maxScrollExtent / 2;
    
    // 设置定时器,实现滚动效果
    _timer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
      _scrollOffset += 1.0;
      
      // 当滚动到第一份公告列表的末尾时,重置到开头位置
      if (_scrollOffset >= _maxScrollExtent) {
        _scrollOffset = 0.0;
        _scrollController.jumpTo(_scrollOffset);
      } else {
        _scrollController.animateTo(
          _scrollOffset,
          duration: const Duration(milliseconds: 50),
          curve: Curves.linear,
        );
      }
    });
  }
  
  @override
  void dispose() {
    _timer.cancel();
    _scrollController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Container(
      height: widget.height.w,
      color: widget.backgroundColor,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        controller: _scrollController,
        physics: const NeverScrollableScrollPhysics(), // 禁用手动滚动
        child: Row(
          children: _buildNoticeItems(),
        ),
      ),
    );
  }
  
  List<Widget> _buildNoticeItems() {
    return _extendedNotices.map((notice) {
      return Container(
        padding: EdgeInsets.symmetric(horizontal: widget.itemSpacing.w / 2),
        child: Row(
          children: [
            // 如果提供了图标,则显示图标
            if (widget.icon != null) ...[
              widget.icon!,
              SizedBox(width: 10.w),
            ],
            
            // 公告文本
            RichText(
              text: TextSpan(
                children: [
                  TextSpan(
                    text: "${notice['title']} ",
                    style: TextStyle(
                      color: widget.textColor,
                      fontSize: widget.fontSize.sp,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                  TextSpan(
                    text: notice['desc'],
                    style: TextStyle(
                      color: widget.textColor,
                      fontSize: widget.fontSize.sp,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      );
    }).toList();
  }
} 

页面调用

js 复制代码
// 模拟数据
final List notices = [
  {
    'title': '用户**1564**',
    'desc': '获得了 20.06 USDT 佣金',
  },
  {
    'title': '用户**3721**',
    'desc': '获得了 35.82 USDT 佣金',
  },
  {
    'title': '用户**9823**',
    'desc': '获得了 15.45 USDT 佣金',
  },
  {
    'title': '用户**6471**',
    'desc': '获得了 42.18 USDT 佣金',
  },
  {
    'title': '用户**2389**',
    'desc': '获得了 28.67 USDT 佣金',
  },
];
MarqueeNotice(
  noti
  ces: List<Map<String, String>>.from(controller.notices),
  height: 80,
  backgroundColor: Colors.transparent,
  textColor: AppTheme.color999,
  fontSize: 24,
  itemSpacing: 100,
)
相关推荐
码林鼠几秒前
flutter BLoC 单向数据流完整教程
flutter
恋猫de小郭5 小时前
Flutter 官方表示:AI 时代代码越来越容易写,为什么还要一套代码库?
android·前端·flutter
喵个咪1 天前
GoWind ShortPlay 拆解:Flutter 沉浸式 Feed 的播放器工程——生命周期、手势状态机与游客降级
前端·flutter·架构
恋猫de小郭2 天前
Flutter 的文字为什么比 SwiftUI 更细?问题主要在 Skia 的 256 项查找表里
android·前端·flutter
好的佩奇3 天前
Dart 之类型系统
flutter·dart
恋猫de小郭3 天前
Dart 3.15 新特性:this 终于支持类型提升了
android·前端·flutter
FungLeo4 天前
成为全栈·Flutter App 篇·主题与可访问性:深浅色、大字号和触控体验
flutter·移动开发·深色模式·可访问性·成为全栈·大字号适配
用户95421573334855 天前
只是选个照片,怎么留下了这么多访问记录?Flutter 图片选择器排查与优化实录
flutter
FungLeo5 天前
成为全栈·Flutter App 篇·第四批复盘:复用了什么,移动端又新增了什么
flutter·移动开发·跨端开发·成为全栈·api契约·全栈复盘