scroll_snap_list

A wrapper for ListView.builder widget that allows "snaping" event to an item at the end of user-scroll.

This widget allows unrestricted scrolling (unlike other widget that only able to "snap" to left/right neighbor at a time).

  • Support horizontal & vertical list
  • Use ListView.Builder, means no Animation

You can use this widget to achieve:

  • Unrestricted scrollable Jumbotron
  • Show daily entry for Health App and shows its detail directly below the list (reduce number of click)
  • Scrollable InputSelect, like datetime picker in cupertino
Jumbotron ListHorizontal ListVertical List
Horizontal Jumbotron ListHorizontal List with infinite-loadingVertical list with InkWell ListItems

Getting Started

In your flutter project pubspec.yaml add the dependency:

dependencies:
  ...
  scroll_snap_list: ^0.2.0

Usage example

This library doesn't use other library, so it should work out of the box.

Import scroll_snap_list.dart.

import 'package:scroll_snap_list/scroll_snap_list.dart';

Add ScrollSnapList in your build method. Make sure to enter correct itemSize (helps with "snapping" process).

Expanded(
  child: ScrollSnapList(
    onItemFocus: _onItemFocus,
    itemSize: 35,
    itemBuilder: _buildListItem,
    itemCount: data.length,
    reverse: true,
  ),
),

Full example:

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:scroll_snap_list/scroll_snap_list.dart';

void main() => runApp(HorizontalListDemo());

class HorizontalListDemo extends StatefulWidget {
  @override
  _HorizontalListDemoState createState() => _HorizontalListDemoState();
}

class _HorizontalListDemoState extends State<HorizontalListDemo> {
  List<int> data = [];
  int _focusedIndex = 0;

  @override
  void initState() {
    super.initState();

    for (int i = 0; i < 30; i++) {
      data.add(Random().nextInt(100) + 1);
    }
  }

  void _onItemFocus(int index) {
    setState(() {
      _focusedIndex = index;
    });
  }

  Widget _buildItemDetail() {
    if (data.length > _focusedIndex)
      return Container(
        height: 150,
        child: Text("index $_focusedIndex: ${data[_focusedIndex]}"),
      );

    return Container(
      height: 150,
      child: Text("No Data"),
    );
  }

  Widget _buildListItem(BuildContext context, int index) {
    //horizontal
    return Container(
      width: 35,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Container(
            height: data[index].toDouble()*2,
            width: 25,
            color: Colors.lightBlueAccent,
            child: Text("i:$index\n${data[index]}"),
          )
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Horizontal List Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text("Horizontal List"),
        ),
        body: Container(
          child: Column(
            children: <Widget>[
              Expanded(
                child: ScrollSnapList(
                  onItemFocus: _onItemFocus,
                  itemSize: 35,
                  itemBuilder: _buildListItem,
                  itemCount: data.length,
                  reverse: true,
                ),
              ),
              _buildItemDetail(),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  RaisedButton(
                    child: Text("Add Item"),
                    onPressed: () {
                      setState(() {
                        data.add(Random().nextInt(100) + 1);
                      });
                    },
                  ),
                  RaisedButton(
                    child: Text("Remove Item"),
                    onPressed: () {
                      int index = data.length > 1
                          ? Random().nextInt(data.length - 1)
                          : 0;
                      setState(() {
                        data.removeAt(index);
                      });
                    },
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

Other Notice

By default, SnapScrollList set focusOnItemTap as true. Means any tap event on items will trigger snap/focus event to that item.

This behavior use GestureDetector, so if the list's items have their own detector (e.g. InkWell, Button), SnapScrollList unable to trigger snap event on its own. To handle this, we may use GlobalKey<ScrollSnapListState> to trigger the event manually.

Add GlobalKey<ScrollSnapListState> in your widget

GlobalKey<ScrollSnapListState> sslKey = GlobalKey();

Add key parameter to SnapScrollList.

ScrollSnapList(
  onItemFocus: _onItemFocus,
  itemSize: 50,
  itemBuilder: _buildListItem,
  itemCount: data.length,
  key: sslKey,
  scrollDirection: Axis.vertical,
)

In your buildItem method, call focusToItem method.

Widget _buildListItem(BuildContext context, int index) {
  //horizontal
  return Container(
    height: 50,
    child: Material(
      color: _focusedIndex == index ? Colors.lightBlueAccent : Colors.white,
      child: InkWell(
        child: Text("Index: $index | Value: ${data[index]}"),
        onTap: () {
          print("Do anything here");

          //trigger focus manually
          sslKey.currentState.focusToItem(index);
        },
      ),
    ),
  );
}

Full example for this part can be found in example/vertical_list.dart.

Libraries

scroll_snap_list

Dart

VM

dart:ffi
Foreign Function Interface for interoperability with the C programming language. [...]

Web

dart:html
HTML elements and other resources for web-based applications that need to interact with the browser and the DOM (Document Object Model). [...]
dart:js
Low-level support for interoperating with JavaScript. [...]
dart:js_util
Utility methods to efficiently manipulate typed JSInterop objects in cases where the name to call is not known at runtime. You should only use these methods when the same effect cannot be achieved with @JS annotations. These methods would be extension methods on JSObject if Dart supported extension methods.