I am seeking to create a constant scroll of a dynamic number of images across my screen (similar to a news ticker) in Flutter. I want this to be automatic and a constant speed, that also loops.
The simplest solution I have found is to use the Carousel Package which ticks almost all the boxes, except one. I am unable to get a constant scroll speed
A possible solution was to adjust autoPlayInterval to zero, but unfortunately, this paramater appears to need a value of around 50 or greater to run - therefore creating an even scroll.
Any idea on how to tweak it this with this package? Or another suitable solution?
Simplified code:
@override
Widget build(BuildContext context) {
return Container(
child: CarouselSlider(
items: DynamicImages.list
.map(
(e) => Padding(
padding: const EdgeInsets.all(8.0),
child: Image.asset('assets/images/$e.png'),
),
)
.toList(),
options: CarouselOptions(
autoPlay: true,
autoPlayCurve: Curves.linear,
autoPlayInterval: Duration(milliseconds: 0), /// carousel will not run if set to zero
autoPlayAnimationDuration: Duration(milliseconds: 1000)
),
),
);
}
}
I found a workaround that still takes advantage of the carousel_slider package. Instead of using autoplay, you just give it a CarouselController
.
Start the animation by calling nextPage()
in initState
. Then in the carousel, set onPageChanged
to call nextPage()
again, which will continuously scroll from page to page with constant speed. Set the duration
for the two nextPage()
calls to whatever you want, as long as it's the same duration.
The only pause I experience is when it reaches the end of the list, where it needs to loop back to the first image. But it's negligible enough for my use case.
class _WelcomeScreenState extends State<WelcomeScreen> {
List<String> _images = [
"assets/image.jpg",
// ...
];
final CarouselController _carouselController = CarouselController();
@override
void initState() {
super.initState();
// Add this to start the animation
WidgetsBinding.instance.addPostFrameCallback((_) {
_carouselController.nextPage(duration: Duration(milliseconds: 2000));
});
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: ScreenBackgroundContainer(
child: Scaffold(
body: SafeArea(
child: Container(
height: double.maxFinite,
width: double.maxFinite,
child: SingleChildScrollView(
physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
children: [
// Carousel
CarouselSlider(
carouselController: _performerCarouselController,
options: CarouselOptions(
pageSnapping: false,
height: 146,
viewportFraction: 114 / MediaQuery.of(context).size.width,
// Add this
onPageChanged: (_, __) {
_performerCarouselController.nextPage(duration: Duration(milliseconds: 2000));
}),
items: _performerImages
.map((image) => ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Image.asset(image, width: 90, fit: BoxFit.cover)))
.toList(),
),
],
),
),
),
),
),
),
);
}
}
Screenshot
So, I've been working on this since posting and come up with a solution. I am posting my answer in case it helps any others in the future.
Basically, instead of using Carousel package, I used ListView.builder which then continuously grows as needed.
Of note, I needed to use WidgetsBinding.instance.addPostFrameCallback((timeStamp) {}); to get this to work.
It would still be great to see any other solutions, (as I am sure the below workaround could be improved).
import 'package:flutter/material.dart';
class ScrollLoop extends StatefulWidget {
const ScrollLoop({Key? key}) : super(key: key);
@override
_ScrollLoopState createState() => _ScrollLoopState();
}
class _ScrollLoopState extends State<ScrollLoop> {
ScrollController _controller = ScrollController();
/// [_list] is growable and holds the assets which will scroll.
List<String> _list = [
"assets/images/image1.png",
/// etc...
];
/// [_list2] holds duplicate data and is used to append to [_list].
List<String> _list2 = [];
/// [_listAppended] ensures [_list] is only appended once per cycle.
bool _listAppended = false;
@override
void initState() {
_list2.addAll(_list);
/// To auto-start the animation when the screen loads.
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_startScroll();
});
/// The [_controller] will notify [_list] to be appended when the animation is near completion.
_controller.addListener(
() {
if (_controller.position.pixels >
_controller.position.maxScrollExtent * 0.90) {
if (_listAppended == false) {
_list.addAll(_list2);
_listAppended = true;
}
}
/// The [_controller] will listen for when the animation cycle completes,
/// so this can immediately re-start from the completed position.
if (_controller.position.pixels ==
_controller.position.maxScrollExtent) {
_listAppended = false;
setState(() {});
WidgetsBinding.instance!.addPostFrameCallback(
(timeStamp) {
_startScroll();
},
);
}
},
);
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
void _startScroll() {
_controller.animateTo(_controller.position.maxScrollExtent,
duration: Duration(milliseconds: 8000), curve: Curves.linear);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final _size = MediaQuery.of(context).size;
return AbsorbPointer(
child: Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
controller: _controller,
scrollDirection: Axis.horizontal,
itemCount: _list.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: _size.width / 4,
height: _size.height / 10,
child: Image.asset(_list[index]),
),
);
},
),
),
],
),
),
),
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With