I’m pretty new to flutter and I’m working on replicating the text scrolling feature for long track titles from the Audible app, (see attached). Here’s the behavior I’m trying to achieve:
• Display the long title initially – a portion of this won’t be visible because it’s too long.
• Pause for a predefined number of seconds, the text should wait a few seconds in this paused state before starting scrolling.
• Start scrolling the text.
• Ensure the entire text is shown via the scroll.
• After the end of the text, insert a specific number of spaces.
• As the spaces conclude, I want the beginning of the text to reappear and continue scrolling.
• The text should scroll until it seems like it’s back to its original position.
• Restart the loop (wait -> scroll -> loop).
I’ve managed to implement most of this. However, my reset point isn’t perfect (see attached). I’ve duplicated the text to make it scroll twice, then I reset the animation when the second set reaches the first text’s start position. The timing isn’t precise, causing imperfect resets.
It just feels that there’s probably a better way to go about this than I have done, but I’m so new to Flutter I don’t know what that way might be.
Is there a more efficient way to achieve this, or does somebody have suggestions to improve my existing code?
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:async';
import 'package:the_audiobook_app/utils/audio_player_handler.dart';
class ScrollingText extends StatefulWidget {
final String? text;
final double speed;
final double containerWidth;
final double containerHeight;
final int spaceCount;
final Duration delay;
ScrollingText({
this.text,
this.speed = 1.0,
this.containerWidth = 150.0,
this.containerHeight = 50.0,
this.spaceCount = 5,
this.delay = const Duration(seconds: 2),
});
@override
_ScrollingTextState createState() => _ScrollingTextState();
}
class _ScrollingTextState extends State<ScrollingText> with TickerProviderStateMixin {
late ScrollController _controller;
late Ticker _ticker;
late TextPainter textPainter;
bool shouldScroll = false;
late String spacedText; // Added this to ensure spaceCount is applied consistently
double singleTextWidth = 0;
@override
void initState() {
super.initState();
_controller = ScrollController();
_ticker = Ticker(_onTick);
//spacedText = widget.text + ' ' * widget.spaceCount + widget.text;
//spacedText = 'Loading...${' ' * widget.spaceCount}Loading...'; // The fact this is being set here is probably not a good thing
}
void initializeScrollingText(String title){
spacedText = title + ' ' * widget.spaceCount + title;
textPainter = TextPainter(
text: TextSpan(text: title, style: TextStyle(fontSize: 20)),
textDirection: TextDirection.ltr,
)..layout();
singleTextWidth = textPainter.width;
if (textPainter.width >= widget.containerWidth) {
textPainter.text = TextSpan(text: spacedText, style: TextStyle(fontSize: 20));
textPainter.layout();
}
shouldScroll = textPainter.width > widget.containerWidth;
if (shouldScroll && !_ticker.isActive) {
_startScrollingWithDelay();
}
}
void _startScrollingWithDelay() {
Future.delayed(widget.delay, () {
if (mounted) {
_ticker.start();
}
});
}
void _onTick(Duration elapsed) {
double current = _controller.offset + widget.speed;
if (current >= singleTextWidth) { // When the second instance is at the beginning.
_ticker.stop();
_controller.jumpTo(0);
Future.delayed(widget.delay, () {
if (mounted) {
_ticker.start();
}
});
} else {
_controller.jumpTo(current);
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int?>(
stream: AudioPlayerHandler.instance.player.currentIndexStream,
builder: (context, snapshot) {
String title = 'Loading...'; // default text
if (snapshot.connectionState == ConnectionState.active && snapshot.data != null) {
title = AudioPlayerHandler.instance.getCurrentTrackTitle() ?? 'NULL VALUE';
}
initializeScrollingText(title); // Initialize scrolling for the new title
return Container(
width: widget.containerWidth,
height: widget.containerHeight,
child: shouldScroll
? ListView.builder(
itemCount: 1,
controller: _controller,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
return Text(spacedText,
style: TextStyle(fontSize: 20),
softWrap: false,
overflow: TextOverflow.visible);
},
)
: Center(
child: Text(spacedText.split(' ')[0], // Since spacedText has repetitions, we only take the first occurrence
style: TextStyle(fontSize: 20),
softWrap: true,
overflow: TextOverflow.ellipsis),
),
);
},
);
}
@override
void dispose() {
_ticker.dispose();
_controller.dispose();
super.dispose();
}
}
2
Answers
This kind of scroll animation is called "marquee". You need exactly this extension: https://pub.dev/packages/text_scroll
I have written one example for your case;
Good Luck 👋
I think you could use something like this…
The basic idea is ListView keeps on repeating child when itemCount is not given.