Flutter Horizontal Listview with a Snap Effect

Last updated Mar 16, 2025

This implementation demonstrates a horizontal list in Flutter with an elegant snap effect, showing how to create smooth, attractive horizontal scrolling UI components. The code features:

Key Features

  1. PageView with Snap Effect - Automatically snaps to the nearest item when scrolling ends
  2. Visual Feedback - Items scale up when centered and down when not, providing clear visual cues
  3. Smooth Animations - Uses TweenAnimationBuilder for fluid transitions between states
  4. Page Indicators - Shows the current position with interactive dot indicators
  5. Multiple Implementation Options - Includes both PageView and ListView.builder approaches

Implementation Details

The solution uses PageView.builder with a custom PageController that has a viewportFraction of 0.8, allowing users to see partial items on either side while focusing on the current one. The snap effect is built into PageView by default.

For enhanced user experience, the current item scales to 1.0 while others scale to 0.9, creating a subtle but effective focus effect. The transition between these states is smoothly animated using TweenAnimationBuilder.

 

#1 Flutter Horizontal List with Snap Effect: A Complete Guide

import 'package:flutter/material.dart';

class SnapHorizontalList extends StatefulWidget {
  const SnapHorizontalList({Key? key}) : super(key: key);

  @override
  State<SnapHorizontalList> createState() => _SnapHorizontalListState();
}

class _SnapHorizontalListState extends State<SnapHorizontalList> {
  final PageController _pageController = PageController(
    viewportFraction: 0.8, // Shows partial next/previous items
    initialPage: 0,
  );
  
  int _currentPage = 0;

  @override
  void initState() {
    super.initState();
    _pageController.addListener(_onScroll);
  }

  @override
  void dispose() {
    _pageController.removeListener(_onScroll);
    _pageController.dispose();
    super.dispose();
  }

  void _onScroll() {
    if (_pageController.page!.round() != _currentPage) {
      setState(() {
        _currentPage = _pageController.page!.round();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Horizontal List with Snap Effect'),
      ),
      body: Column(
        children: [
          const SizedBox(height: 20),
          
          // Page indicator
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: List.generate(
              10,
              (index) => Container(
                margin: const EdgeInsets.symmetric(horizontal: 4),
                width: 10,
                height: 10,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: index == _currentPage 
                    ? Colors.blue 
                    : Colors.grey.shade300,
                ),
              ),
            ),
          ),
          
          const SizedBox(height: 20),
          
          // Main horizontal list with snap effect
          SizedBox(
            height: 200,
            child: PageView.builder(
              controller: _pageController,
              itemCount: 10,
              // The PageView will automatically snap to items
              itemBuilder: (context, index) {
                // Calculate scale factor for animation
                double scale = _currentPage == index ? 1.0 : 0.9;
                
                return TweenAnimationBuilder(
                  tween: Tween(begin: scale, end: scale),
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeInOutCubic,
                  builder: (context, double value, child) {
                    return Transform.scale(
                      scale: value,
                      child: Container(
                        margin: const EdgeInsets.symmetric(horizontal: 10),
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(15),
                          color: Colors.primaries[index % Colors.primaries.length],
                          boxShadow: [
                            BoxShadow(
                              color: Colors.black.withOpacity(0.2),
                              blurRadius: 8,
                              offset: const Offset(0, 4),
                            ),
                          ],
                        ),
                        alignment: Alignment.center,
                        child: Text(
                          'Item ${index + 1}',
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 24,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    );
                  },
                );
              },
            ),
          ),
          
          const SizedBox(height: 30),
          
          // Button to demonstrate programmatic scrolling
          ElevatedButton(
            onPressed: () {
              final nextPage = (_currentPage + 1) % 10;
              _pageController.animateToPage(
                nextPage,
                duration: const Duration(milliseconds: 500),
                curve: Curves.easeInOut,
              );
            },
            child: const Text('Next Item'),
          ),
          
          // Alternative horizontal scrolling implementation
          const SizedBox(height: 40),
          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            child: Text(
              'Alternative ListView.builder Implementation',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
          ),
          
          const SizedBox(height: 10),
          
          SizedBox(
            height: 120,
            child: ListView.builder(
              scrollDirection: Axis.horizontal,
              itemCount: 10,
              itemBuilder: (context, index) {
                return Container(
                  width: 120,
                  margin: const EdgeInsets.symmetric(horizontal: 8),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(8),
                    color: Colors.amber[(index % 9 + 1) * 100],
                  ),
                  alignment: Alignment.center,
                  child: Text('Item ${index + 1}'),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

// Example of how to use this widget in a main app
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Horizontal Snap List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const SnapHorizontalList(),
    );
  }
}

 

#2 Flutter Horizontal LIstview with Snpa Effect

How to create a list of cards scrolling horizontally with snap to fit effect when swiped either from left or right. 

In this post we will learn how to create Horizontal Scollable Card with Snap effect.

For this Example i used  scroll_snap_list plugin.

 

This example  i havecreated a List of Data classes which contains Image and Count of items to be selected.

 

Let's get started

 

Step 1: Create a Flutter application

Step2: Add required plugin in pubspec.yaml file.

dependencies:
  flutter:
    sdk: flutter
  scroll_snap_list:

 

Import plugin in dart file

import 'package:scroll_snap_list/scroll_snap_list.dart';

 

Step 3:  Create a Data class file which contains image path and item count variable

class Data{
  int count;
  String path;
  Data(this.count,this.path);

}

 

Step 4: Create UI to display the data items

Widget _buildItemList(BuildContext context, int index){
  if(index == data.length)
    return Center(
      child: CircularProgressIndicator(),
    );
  return Container(
    width: 200,
    child: Center(
      child: Container(
        height: 380,

        child: Card(
          color: Colors.white,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              (index==current)?Container(child:RaisedButton(

                child: Icon(Icons.delete),
                color: Colors.red,
                onPressed: () {
                  setState(() {
                    data.removeAt(index);

                  });
                },
              )):Container(),
              Card(
                elevation: 10,
                child: Container(

                  width: 200,
                  height: 250,
                  child:
                  Image.network(data[index].path,scale: 1,fit: BoxFit.cover,),
                ),
              ),
              (index==current)?Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                Container(
                  width: 50,
                  child: RaisedButton(

                    child: Icon(Icons.add),
                    color: Colors.green,
                    onPressed: () {
                      setState(() {
                        if(data[index].count<10)
                          data[index].count=data[index].count+1;
                      });
                    },

                  ),
                ),
                Text("${data[index].count}",style: TextStyle(color: Colors.blue,fontSize: 16),),
                Container(width: 50,
                  child: RaisedButton(

                    child: Icon(Icons.exposure_minus_1),
                    color: Colors.red,
                    onPressed: () {

                      setState(() {
                        if(data[index].count>0)
                          data[index].count=data[index].count-1;
                      });
                    },
                  ),
                )
              ],):Container()
            ],
          ),
        ),
      ),
    ),
  );
}

 

Step 5:  Now its time  to add our List data to ScrollSnapList widget.

This widget will handle the list scrolling and make snap animation effect for the current list item.

 

ScrollSnapList(
  itemBuilder: _buildItemList,
  onItemFocus: (pos){
    setState(() {
      current=pos;
    });
    print('Done! $pos');
  },
  itemSize: 200,
  dynamicItemSize: true,
  onReachEnd: (){
    print('Done!');
  },
  itemCount: data.length,
)

 

This  ScrollSnapList widget has property called itemBuilder which will take the data as input.

 

Step 6: Run Application

 

 Flutter Horizontal Listview with a Snap Effect

 

 

Complete Example code

 

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: DemoApp(),
      theme: ThemeData(
        brightness: Brightness.dark,
      ),
    );
  }
}

class DemoApp extends StatefulWidget {
  @override
  _DemoAppState createState() => _DemoAppState();
}

class _DemoAppState extends State<DemoApp> {

  int current=0;

  List<Data> data = [
    Data(0,
    "https://img.freepik.com/free-photo/milford-sound-new-zealand-travel-destination-concept_53876-42945.jpg"),
  Data(0,"https://watchandlearn.scholastic.com/content/dam/classroom-magazines/watchandlearn/videos/animals-and-plants/plants/what-are-plants-/What-Are-Plants.jpg"),
  Data(0,"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRc7drAdH_rr-5-s1dR37nexpspDiygTjd_eg&usqp=CAU"),
  Data(0,"https://images-na.ssl-images-amazon.com/images/I/51Gguy1yh9L.jpg"),
  Data(0,"https://nestreeo.com/wp-content/uploads/2020/03/Pyrostegia_venusta.jpg"),
  Data(0,"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQW7IvtmZDCGOoFuqg5s7QWSX7sWpf3bqZcsQ&usqp=CAU"),
    Data(0,"https://images-na.ssl-images-amazon.com/images/I/51Gguy1yh9L.jpg"),
    Data(0,"https://nestreeo.com/wp-content/uploads/2020/03/Pyrostegia_venusta.jpg"),
    Data(0,"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQW7IvtmZDCGOoFuqg5s7QWSX7sWpf3bqZcsQ&usqp=CAU"),
  ];

  Widget _buildItemList(BuildContext context, int index){
    if(index == data.length)
      return Center(
        child: CircularProgressIndicator(),
      );
    return Container(
      width: 200,
      child: Center(
        child: Container(
          height: 380,

          child: Card(
            color: Colors.white,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                (index==current)?Container(child:RaisedButton(

                  child: Icon(Icons.delete),
                  color: Colors.red,
                  onPressed: () {
                    setState(() {
                      data.removeAt(index);

                    });
                  },
                )):Container(),
                Card(
                  elevation: 10,
                  child: Container(

                    width: 200,
                    height: 250,
                    child:
                    Image.network(data[index].path,scale: 1,fit: BoxFit.cover,),
                  ),
                ),
                (index==current)?Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                  Container(
                    width: 50,
                    child: RaisedButton(

                      child: Icon(Icons.add),
                      color: Colors.green,
                      onPressed: () {
                        setState(() {
                          if(data[index].count<10)
                            data[index].count=data[index].count+1;
                        });
                      },

                    ),
                  ),
                  Text("${data[index].count}",style: TextStyle(color: Colors.blue,fontSize: 16),),
                  Container(width: 50,
                    child: RaisedButton(

                      child: Icon(Icons.exposure_minus_1),
                      color: Colors.red,
                      onPressed: () {

                        setState(() {
                          if(data[index].count>0)
                            data[index].count=data[index].count-1;
                        });
                      },
                    ),
                  )
                ],):Container()
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    current=0;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey,
      appBar: AppBar(
        backgroundColor: Colors.pinkAccent,
        title: Text('Horizontal list',style: TextStyle(color: Colors.white),),
        centerTitle: true,
      ),
      body: Container(
        child: Column(
          children: [
            Expanded(
                child: ScrollSnapList(
                  itemBuilder: _buildItemList,
                  onItemFocus: (pos){
                    setState(() {
                      current=pos;
                    });
                    print('Done! $pos');
                  },
                  itemSize: 200,
                  dynamicItemSize: true,
                  onReachEnd: (){
                    print('Done!');
                  },
                  itemCount: data.length,
                )
            ),
          ],
        ),
      ),
    );
  }
}

class Data{
  int count;
  String path;
  Data(this.count,this.path);

}

 

Also read Flutter Dynamic Expansion Listview Example

 

 

Article Contributed By :
https://www.rrtutors.com/site_assets/profile/assets/img/avataaars.svg

4759 Views