image

21 Jan 2026

9K

35K

Flutter Parallax Scroll Animation for ListView

Creating engaging and dynamic user interfaces is a cornerstone of modern mobile application development. One highly effective technique to add depth and visual appeal is the parallax scroll effect. This article will guide you through implementing a sophisticated parallax scroll animation for a Flutter ListView, where background elements move at a different speed than foreground content, creating an immersive 3D illusion.

Understanding Parallax Scroll

Parallax scrolling is a web and mobile design technique where background images move past the camera slower than foreground images, creating an illusion of depth and movement. In the context of a ListView, this typically means that as the user scrolls, a background image within each list item shifts vertically at a different rate than the list item's main content, revealing more or less of the background image based on its position within the viewport.

The core principle involves calculating the relative position of an item within the scrollable area and applying a transformation (usually a vertical offset) to a nested background element. As an item moves from the bottom to the top of the screen (or vice-versa), its background element will appear to slide independently.

Implementing Parallax in Flutter ListView

To achieve the parallax effect in Flutter, we'll follow these steps:

  1. Set up a ListView.builder to efficiently display a large number of items.
  2. Use a ScrollController to monitor the scroll position of the ListView.
  3. Create a custom widget for each list item that contains an image (for the background) and potentially other content (for the foreground).
  4. Within each item widget, calculate the current vertical offset for its background image based on the ListView's scroll position and the item's own position within the scrollable area.
  5. Apply this calculated offset to the background image using Transform.translate or Positioned within a Stack.

1. Setting Up the Main View and ScrollController

First, let's create a basic StatefulWidget that will hold our ListView and manage the ScrollController. We'll also define some dummy data for our list items.


import 'package:flutter/material.dart';

class ParallaxListViewScreen extends StatefulWidget {
  @override
  _ParallaxListViewScreenState createState() => _ParallaxListViewScreenState();
}

class _ParallaxListViewScreenState extends State {
  late ScrollController _scrollController;
  final List _imageUrls = [
    'https://picsum.photos/id/10/800/600',
    'https://picsum.photos/id/20/800/600',
    'https://picsum.photos/id/30/800/600',
    'https://picsum.photos/id/40/800/600',
    'https://picsum.photos/id/50/800/600',
    'https://picsum.photos/id/60/800/600',
    'https://picsum.photos/id/70/800/600',
    'https://picsum.photos/id/80/800/600',
    'https://picsum.photos/id/90/800/600',
    'https://picsum.photos/id/100/800/600',
  ];

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    // No need to listen to scroll controller directly if we use NotificationListener
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Parallax ListView'),
      ),
      body: NotificationListener(
        onNotification: (ScrollNotification notification) {
          // This ensures the list rebuilds on scroll to update parallax effect.
          // In a real app, you might optimize this with an AnimatedBuilder.
          setState(() {}); 
          return false;
        },
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _imageUrls.length,
          itemBuilder: (context, index) {
            return ParallaxListItem(
              imageUrl: _imageUrls[index],
              height: 200, // Fixed height for simplicity
              scrollOffset: _scrollController.offset,
            );
          },
        ),
      ),
    );
  }
}

2. Creating a Parallax Item Widget

Next, we'll build the ParallaxListItem widget. This widget will contain the logic to calculate and apply the parallax effect. Each item will take an imageUrl, a fixed height, and the current scrollOffset of the ListView.


class ParallaxListItem extends StatelessWidget {
  final String imageUrl;
  final double height;
  final double scrollOffset;

  const ParallaxListItem({
    Key? key,
    required this.imageUrl,
    required this.height,
    required this.scrollOffset,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // We need to find the item's position relative to the screen.
    // Using a LayoutBuilder to get the current item's render box information.
    return SizedBox(
      height: height,
      child: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          // Get the RenderBox of the current widget (ParallaxListItem)
          final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
          if (renderBox == null) return Container(); // Should not happen in practice

          // Calculate the vertical position of the item's top edge relative to the viewport's top.
          // This takes into account the scroll position.
          final double itemOffset = renderBox.localToGlobal(Offset.zero).dy;

          // The amount of "extra" image height available for parallax.
          // We assume the image is taller than the item's display height.
          final double parallaxImageHeight = height * 1.5; // Example: image is 1.5x item height
          final double parallaxRange = parallaxImageHeight - height;

          // Determine how much the image should shift based on item's position.
          // We want the image to move as the item passes through the viewport.
          // A simplified approach: center the image when the item is in the center of the screen.
          final double screenHeight = MediaQuery.of(context).size.height;
          final double viewportCenter = screenHeight / 2;
          final double itemCenter = itemOffset + height / 2;

          // Calculate the normalized position relative to the viewport center.
          // 0 when item center is at viewport center, negative when above, positive when below.
          final double normalizedPosition = (viewportCenter - itemCenter) / screenHeight;

          // Calculate the parallax offset. We can scale this with a factor.
          final double parallaxOffset = normalizedPosition * parallaxRange;

          return ClipRRect(
            child: Stack(
              children: [
                Positioned.fill(
                  // Shift the image vertically based on parallaxOffset.
                  // We add a default offset to ensure the image is centered
                  // when the item is in the middle of the screen.
                  top: -parallaxRange / 2 + parallaxOffset,
                  child: Image.network(
                    imageUrl,
                    fit: BoxFit.cover,
                    alignment: Alignment.center, // The Image widget itself handles some centering
                  ),
                ),
                // Overlay text or other foreground content
                Align(
                  alignment: Alignment.center,
                  child: Text(
                    'Item at Y: ${itemOffset.toStringAsFixed(2)}',
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                      shadows: [
                        Shadow(
                          blurRadius: 3.0,
                          color: Colors.black,
                          offset: Offset(1.0, 1.0),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

3. Integrating and Running the Example

To see the parallax effect in action, ensure your main.dart file (or whichever file initializes your app) starts with ParallaxListViewScreen.


// In your main.dart or equivalent file:
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Parallax Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ParallaxListViewScreen(),
    );
  }
}

Explanation of the Parallax Logic in ParallaxListItem

Let's break down the key parts of the `ParallaxListItem`'s build method:

  1. LayoutBuilder: We wrap the content in a LayoutBuilder. This allows us to access the BuildContext of the item itself, which is crucial for finding its position on the screen.
  2. renderBox.localToGlobal(Offset.zero).dy: This line is critical. It gets the vertical position of the top-left corner of our `ParallaxListItem` relative to the global screen coordinates. As the list scrolls, this `itemOffset` changes.
  3. parallaxImageHeight and parallaxRange: We assume the background image is taller than the item's displayed height (e.g., 1.5 times). `parallaxRange` is the total vertical distance the background image can move.
  4. viewportCenter and itemCenter: We calculate the vertical center of both the entire screen (or the `ListView`'s visible area if constrained) and the current list item.
  5. normalizedPosition: This value indicates how far the item's center is from the screen's center, normalized to a range. It's negative if the item is above the screen center, positive if below.
  6. parallaxOffset: This is the final calculated vertical shift for the background image. It's derived from the `normalizedPosition` and the `parallaxRange`. When `normalizedPosition` is 0 (item at screen center), `parallaxOffset` is 0. As the item moves up or down, `parallaxOffset` becomes negative or positive, shifting the image accordingly.
  7. Positioned.fill and top: -parallaxRange / 2 + parallaxOffset:
    • `Positioned.fill` makes the image fill the `Stack`.
    • ` -parallaxRange / 2`: This is a base offset to initially center the larger background image within the item's smaller `height`. If `parallaxOffset` were always 0, this would simply center the `parallaxImageHeight` vertically within the `height`.
    • `+ parallaxOffset`: This adds our calculated parallax shift. As `parallaxOffset` changes, the `top` position of the image adjusts, creating the illusion of different scroll speeds.
  8. NotificationListener in `ParallaxListViewScreen`: The `setState(() {});` call inside the `onNotification` callback ensures that the `ParallaxListViewScreen` (and thus all its children, including `ParallaxListItem`s) rebuilds whenever the `ListView` scrolls. This forces each `ParallaxListItem` to re-evaluate its `itemOffset` and `parallaxOffset`, updating the image position dynamically. For performance in a production app, one might use an `AnimatedBuilder` that only rebuilds the `ParallaxListItem`s themselves based on the `ScrollController`, or use `ValueNotifier` to pass the scroll offset more efficiently.

Key Considerations

  • Performance: Constantly rebuilding widgets can impact performance. For very long lists, consider optimizing by using `AnimatedBuilder` with the `ScrollController` to only rebuild the necessary parts, or by passing a `ValueNotifier` holding the scroll offset.
  • Image Loading: For remote images, consider using an image caching library like `cached_network_image` to improve loading times and reduce network requests.
  • Item Height: In this example, we assumed a fixed `height` for each list item. If your list items have dynamic heights, you'll need a more robust way to calculate `itemOffset` or ensure the parallax effect is visually appealing across varying heights.
  • Parallax Factor: The `parallaxRange` and the way `normalizedPosition` is scaled determine the intensity of the parallax effect. Experiment with these values (`parallaxImageHeight`, `screenHeight`, etc.) to find the aesthetic that best suits your application.
  • ClipRRect: `ClipRRect` is used to ensure that the larger background image is clipped to the bounds of the `ParallaxListItem`, preventing it from overflowing.

Conclusion

Implementing a parallax scroll animation in a Flutter ListView can significantly enhance the visual appeal and user experience of your application. By carefully tracking scroll positions and applying calculated transformations to nested background images, you can create a dynamic and engaging sense of depth. While this example provides a solid foundation, remember to consider performance and scalability for real-world applications by optimizing rebuilds and image handling.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is