You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现类似音乐标题的文本轮播(Carousel)动画?

Flutter实现文本轮播(音乐标题式滚动效果)

要实现GIF中那种长文本超出容器时自动横向滚动、循环播放的效果,你可以选择自定义动画组件或者使用第三方包,以下是两种方案:

方案一:自定义实现(无依赖)

通过AnimationController和Transform.translate手动控制文本滚动,完全掌握效果细节:

1. 自定义MarqueeText组件

import 'package:flutter/material.dart';

class MarqueeText extends StatefulWidget {
  final String text;
  final TextStyle? style;
  final Duration animationDuration;
  final Duration pauseDuration;
  final double blankSpace;

  const MarqueeText({
    super.key,
    required this.text,
    this.style,
    this.animationDuration = const Duration(seconds: 5),
    this.pauseDuration = const Duration(seconds: 1),
    this.blankSpace = 20.0,
  });

  @override
  State<MarqueeText> createState() => _MarqueeTextState();
}

class _MarqueeTextState extends State<MarqueeText> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  final GlobalKey _textKey = GlobalKey();
  double _textWidth = 0.0;
  double _containerWidth = 0.0;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    );

    // 动画完成后暂停,再重置播放
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        Future.delayed(widget.pauseDuration, () {
          if (mounted) {
            _animationController.reset();
            _animationController.forward();
          }
        });
      }
    });
  }

  @override
  void didUpdateWidget(covariant MarqueeText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.text != widget.text) {
      _textWidth = 0.0;
      _animationController.reset();
      _startAnimationIfNeeded();
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _startAnimationIfNeeded() {
    if (_textWidth > _containerWidth && mounted) {
      _animationController.forward();
    }
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        _containerWidth = constraints.maxWidth;
        // 测量文本实际宽度
        WidgetsBinding.instance.addPostFrameCallback((_) {
          final renderBox = _textKey.currentContext?.findRenderObject() as RenderBox?;
          if (renderBox != null && renderBox.size.width != _textWidth) {
            setState(() {
              _textWidth = renderBox.size.width;
            });
            _startAnimationIfNeeded();
          }
        });

        return ClipRect(
          child: Stack(
            children: [
              // 隐藏的文本,用于测量宽度
              Positioned(
                child: Opacity(
                  opacity: 0,
                  child: Text(widget.text, key: _textKey, style: widget.style),
                ),
              ),
              // 滚动的文本
              AnimatedBuilder(
                animation: _animationController,
                builder: (context, child) {
                  final scrollDistance = _textWidth + widget.blankSpace + _containerWidth;
                  return Transform.translate(
                    offset: Offset(-scrollDistance * _animationController.value, 0),
                    child: Row(
                      children: [
                        Text(widget.text, style: widget.style),
                        SizedBox(width: widget.blankSpace),
                        Text(widget.text, style: widget.style),
                      ],
                    ),
                  );
                },
              ),
            ],
          ),
        );
      },
    );
  }
}

2. 使用示例

class MusicPlayerPage extends StatelessWidget {
  const MusicPlayerPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(title: const Text('音乐播放器')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 模拟音乐标题容器
            Container(
              width: 300,
              padding: const EdgeInsets.symmetric(vertical: 8),
              child: MarqueeText(
                text: 'A Long Long Music Title That Needs To Scroll Horizontally',
                style: const TextStyle(color: Colors.white, fontSize: 18),
                animationDuration: const Duration(seconds: 6),
                pauseDuration: const Duration(seconds: 2),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

方案二:使用第三方包(快捷高效)

如果不想手动实现,可以使用marquee包,它封装了更丰富的滚动配置:

1. 添加依赖

在pubspec.yaml中添加:

dependencies:
  marquee: ^2.2.3

2. 使用示例

import 'package:marquee/marquee.dart';

// 在Widget中使用
Marquee(
  text: '超长音乐标题示例:这是一首很好听的歌但名字特别长',
  style: TextStyle(fontSize: 18, color: Colors.white),
  scrollAxis: Axis.horizontal,
  blankSpace: 30.0,
  velocity: 30.0, // 滚动速度
  pauseAfterRound: Duration(seconds: 1), // 每轮滚动后暂停时长
  startPadding: 10.0,
  accelerationCurve: Curves.linear,
)

关键效果说明

  • 滚动触发条件:只有当文本宽度超过容器宽度时,才会启动滚动动画,短文本会正常显示。
  • 无缝循环:通过拼接两个相同文本+空白间距,实现滚动的连续性,避免突兀跳转。
  • 自定义配置:可以调整动画时长、暂停时间、滚动速度等参数,匹配你的UI风格。

内容的提问来源于stack exchange,提问作者guti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 15:55:28