Flutter Horizontal List 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.

This implementation is perfect for product carousels, image galleries, feature showcases, or any horizontal scrolling content that benefits from clear item separation and focus

#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 createState() => _SnapHorizontalListState();
}

class _SnapHorizontalListState extends State {
  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 Horizontal List with Snap Effect

Flutter offers various widgets in which Listview is the most used widget in the application and in today's tutorial we will see how you can get a snapping effect to listview i.e "snaping" event to an item at the end of user-scroll.

Let's start

Step 1: Create a new Flutter Application

Step 2: Add a line like this to your package's pubspec.yaml

dependencies:
  scroll_snap_list: ^0.8.4

 

Step 3: You can create a list that you want in which there is an item view for example we have a list of ten numbers that we will display in a container.

List data = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];

 

Now Once the Package is installed we will get access to a widget called ScrollSnapList it has required properties

  • itemBuilder: where you will give your list item view.

  • itemCount: where you will give the list item count just like normal listview.

  •  itemSize: Composed of the size of each item + its margin/padding. Size used is width if scrollDirection is Axis.horizontal, height if Axis.vertical.

  • onItemFocus: Callback function when list snaps to list index.

when the item reaches to last it has a property called onReachEnd which will help you launch any function

for example:

   Expanded(

                child: ScrollSnapList(

              onItemFocus: _onItemFocus,

              itemBuilder: _buildItemList,

              itemSize: 150,

              dynamicItemSize: true,

              onReachEnd: () {

                print('Done!');

              },

              itemCount: data.length,

            )),

 

this is the simple example where you will see "Done!" printed in your terminal when the list reaches to end using onReachEnd Function.

 

Full Source Code to try

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 {

  int _focusedIndex = 0;

  List data = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];

  void _onItemFocus(int index) {

    setState(() {

      _focusedIndex = index;

    });

  }

 

  Widget _buildItemList(BuildContext context, int index) {

    if (index == data.length)

      return Center(

        child: CircularProgressIndicator(),

      );

    return Container(

      width: 150,

      child: Column(

        mainAxisAlignment: MainAxisAlignment.center,

        children: [

          Container(

            color: Colors.yellow,

            width: 150,

            height: 200,

            child: Center(

              child: Text(

                '${data[index]}',

                style: TextStyle(fontSize: 50.0, color: Colors.black),

              ),

            ),

          ),

        ],

      ),

    );

  }

 

  @override

  Widget build(BuildContext context) {

    return Scaffold(

      appBar: AppBar(

        title: Text(

          'Horizontal list',

          style: TextStyle(color: Colors.white),

        ),

        centerTitle: true,

      ),

      body: Container(

        child: Column(

          children: [

            Expanded(

                child: ScrollSnapList(

              onItemFocus: _onItemFocus,

              itemBuilder: _buildItemList,

              itemSize: 150,

              dynamicItemSize: true,

              onReachEnd: () {

                print('Done!');

              },

              itemCount: data.length,

            )),

          ],

        ),

      ),

    );

  }

}

 

 

Output: 

 

Video Tutorial

Conclusion: In this way, we have learned how you can get a snapping effect for a horizontal list in Flutter.

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

1324 Views