How to automatically scroll through all the ListTiles in the Listview.seperated in Flutter?

559

Here is a solution assuming that all your items in the ListView have the same itemExtent.

enter image description here

In this solution, I highlight the current Item as selected. You could also want to stop autoscrolling as soon as you reach the bottom of the list.

Full source code

import 'dart:async';

import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part '66455867.auto_scroll.freezed.dart';

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: HomePage(),
    ),
  );
}

class HomePage extends StatelessWidget {
  Future<List<News>> _fetchNews() async => dummyData;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('News')),
      body: FutureBuilder(
        future: _fetchNews(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return NewsList(newsList: snapshot.data);
          } else if (snapshot.hasError) {
            return Center(child: Text(snapshot.error.toString()));
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}

class NewsList extends StatefulWidget {
  final List<News> newsList;

  const NewsList({
    Key key,
    this.newsList,
  }) : super(key: key);

  @override
  _NewsListState createState() => _NewsListState();
}

class _NewsListState extends State<NewsList> {
  ScrollController _scrollController = ScrollController();
  Timer _timer;

  double _itemExtent = 100.0;
  Duration _scrollDuration = Duration(milliseconds: 300);
  Curve _scrollCurve = Curves.easeInOut;

  int _autoScrollIncrement = 1;
  int _currentScrollIndex = 0;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(Duration(seconds: 2), (_) async {
      _autoScrollIncrement = _currentScrollIndex == 0
          ? 1
          : _currentScrollIndex == widget.newsList.length - 1
              ? -1
              : _autoScrollIncrement;
      _currentScrollIndex += _autoScrollIncrement;
      _animateToIndex(_currentScrollIndex);
      setState(() {});
    });
  }

  void _animateToIndex(int index) {
    _scrollController.animateTo(
      index * _itemExtent,
      duration: _scrollDuration,
      curve: _scrollCurve,
    );
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      controller: _scrollController,
      itemExtent: _itemExtent,
      children: widget.newsList
          .map((news) => ListTile(
                title: Text(news.title),
                subtitle: Text(
                  news.description,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                selected: widget.newsList[_currentScrollIndex].id == news.id,
                selectedTileColor: Colors.amber.shade100,
              ))
          .toList(),
    );
  }
}

@freezed
abstract class News with _$News {
  const factory News({int id, String title, String description}) = _News;
}

final faker = Faker();
final dummyData = List.generate(
  10,
  (index) => News(
    id: faker.randomGenerator.integer(99999999),
    title: faker.sport.name(),
    description: faker.lorem.sentence(),
  ),
);

Packages used in the solution:

  • freeze for the News Domain Class
  • build_runner to generate the freezed code
  • faker to generate the list of random news

UPDATE : Scroll only once

To stop the autoscrolling at the bottom of the listview, you just need to modify the initState method:

int _currentScrollIndex;
News _selectedNews;

@override
void initState() {
  super.initState();
  _currentScrollIndex = -1;
  _timer = Timer.periodic(Duration(seconds: 2), (_) async {
    setState(() {
      if (_currentScrollIndex == widget.newsList.length - 1) {
        _timer.cancel();
        _selectedNews = null;
      } else {
        _selectedNews = widget.newsList[++_currentScrollIndex];
        _animateToIndex(_currentScrollIndex);
      }
    });
  });
}

We don't need the scroll direction defined as _autoScrollIncrement. However, I would introduce a new _selectedNews to easily unselect the last News item when we arrive at the bottom of the list. The selected flag of our ListTile would then become:

@override
Widget build(BuildContext context) {
  return ListView(
    [...]
    children: widget.newsList
      .map((news) => ListTile(
        [...]
        selected: _selectedNews?.id == news.id,
        [...]
      ))
      .toList(),
  );
}
Share:
559
Thejani Saranaguptha
Author by

Thejani Saranaguptha

Updated on December 28, 2022

Comments

  • Thejani Saranaguptha
    Thejani Saranaguptha over 1 year

    Scroll automatically (without any user interaction) through all the ListTiles in the Listview using a Timer in flutter. The below method makes only one ListTile to animate but I want to animate all the ListTiles from top to bottom one by one and again from bottom to top one by one.

    The below is the Listview:

    @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: FutureBuilder(
              future: fetchNews(),
              builder: (context, snap) {
                if (snap.hasData) {
                  news = snap.data;
                  return ListView.separated(
                    //controller: _controller,
                    scrollDirection: scrollDirection,
                    controller: controller,
                    itemBuilder: (context, i) {
                      final NewsModel _item = news[i];
                      return AutoScrollTag(
                        key: ValueKey(i),
                        controller: controller,
                        index: i,
                        child: ListTile(
                          title: Text('${_item.title}'),
                          subtitle: Text(
                            '${_item.description}',
                            // maxLines: 1,
                            //overflow: TextOverflow.ellipsis,
                          ),
                        ),
                      );
                    },
                    separatorBuilder: (context, i) => Divider(),
                    itemCount: news.length,
                  );
                } else if (snap.hasError) {
                  return Center(
                    child: Text(snap.error.toString()),
                  );
                } else {
                  return Center(
                    child: CircularProgressIndicator(),
                  );
                }
              },
            ),
          ),
        );
      }
    }
    

    This is the automatic scrolling i have tried:

    @override
      void initState() {
        super.initState();
        timer = Timer.periodic(Duration(seconds: 2), (Timer t) async {
            await controller.scrollToIndex(1,
                preferPosition: AutoScrollPosition.begin);
        });
    
  • Thejani Saranaguptha
    Thejani Saranaguptha over 3 years
    Everything turns to blue text and it does not scroll @Thierry
  • Thierry
    Thierry over 3 years
    Weird. Did you install the dependencies and run the build_runner to generate the News class with flutter pub run build_runner watch --delete-conflicting-outputs?
  • Thejani Saranaguptha
    Thejani Saranaguptha over 3 years
    Do you have any idea how to stop the autoscrolling at the bottom of the listview? @Thierry
  • Thierry
    Thierry over 3 years
    Sure, check the last update of this answer.