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,
)
相关推荐
blanks20207 小时前
如何查看 drift 在 本机的 sqlite 文件路径
flutter
m0_7381858215 小时前
Flutter 鸿蒙化实战:flutter_iot_wifi 适配 OpenHarmony,IoT 设备 WiFi 配网
物联网·flutter·华为·harmonyos·鸿蒙
律宏阔1 天前
Flutter Riverpod 3:统一打印 Provider 错误与自动重试日志
前端·flutter
m0_738185821 天前
Flutter 鸿蒙化实战:flutter_exif_plugin 适配 OpenHarmony,图片 EXIF 读写
flutter·华为·harmonyos·鸿蒙
恋猫de小郭1 天前
Shopify 回应为什么从 RN 回到原生,为什么不用 KMP ?
android·前端·flutter
事圆则缓2 天前
Flutter 状态管理框架对比(五):GetX 的响应式、依赖注册与生命周期
前端·javascript·flutter
事圆则缓2 天前
Flutter 状态管理框架对比(六):同一个购物车,四种方案怎么落地?
前端·javascript·flutter
恋猫de小郭2 天前
Android 原生的 Compose A2UI 也来了,你还抱着 XML 养老吗?
android·前端·flutter
事圆则缓3 天前
Flutter 状态管理框架对比(三):Riverpod 如何连起购物车与异步价格
开发语言·javascript·flutter