Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a constant looping auto-scroll in Flutter?

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)
          ),
        ),
      );
  }
}
like image 621
Kdon Avatar asked Oct 17 '25 17:10

Kdon


2 Answers

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

like image 111
Ben Ha Avatar answered Oct 20 '25 09:10

Ben Ha


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]),
                        ),
                     );
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

enter image description here

like image 40
Kdon Avatar answered Oct 20 '25 09:10

Kdon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!