Gravid Banner

Building a ListView which scrolls neatly by item

Recently I wanted to horizontally scrolling ListView to display a number of panels. This is relatively easy to achieve, but there were two things that I did not like about it.

  • Firstly the fixed width of the items means the list displays shows parts of an panel
  • Secondly when the list scrolls it doesn’t snap to the edge of an panel
class MyListState extends State<MyList> {
  List<String> elements = ["One","Two","Three","Four","Five","Six","Seven", "Eight"];

  @override
  Widget build(BuildContext context) {
    double itemMargin = 5;  // Margin between list elements
    double pageMargin = 15; 
    double itemWidth = 120;
    
    return Padding(padding: EdgeInsets.all(pageMargin),
        child: Container(
        height: 100,
        child: ListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: elements.length,
            itemBuilder: (BuildContext context, int index) {
                return Padding(padding: EdgeInsets.only(left: itemMargin, right: itemMargin),
                    child:Container(
                      width : itemWidth,
                      color: Colors.blueGrey,
                      child: Center(child: Text(' ${elements[index]}')),
          ));
        }
    )));
  }
}
Dart

As it turns out both of these issues are also very easy to fix. First up, calculating the panel dimensions can be done by using a media query to get the screen width. Dividing the screen width by the minimum width of a panel and its margins gives us how many panels to display for a screen width. We can then calculate the individual panel width from the screen width, margins and number of panels.

double minWidth = 120;
Size sz = MediaQuery.of(context).size;

// How many panels to show
int numItems = (sz.width/(minWidth+itemMargin*2)).floor();

// Width of each panel
double itemWidth = ((sz.width-pageMargin*2)/numItems) - itemMargin*2;
Dart

Next we need to modify the ListView Builder to use a PageController with PageScrollPhysics. Normally PageController scrolls whole pages – the key to making it scroll on a per item basis is to use viewportFraction to set the fraction of width that each panel occupies. With these changes my ListView.builder changes to

        child: ListView.builder(
            scrollDirection: Axis.horizontal,
            itemCount: elements.length,
            physics: const PageScrollPhysics(),
            controller: PageController(
              viewportFraction:  1/numItems,
            ),
Dart

Running this in Chrome confirms the ListView now behaves exactly as I had hoped – showing only whole panels and bouncing nicely to the edge of a panel as the user scrolls.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *