Parallax effect in flutter.1000X600

How to Add the Parallax Effect in Flutter?

While designing the user interface, developers need to amalgamate various effects for movement, scrolling, content visibility, and so on. The parallax effect is one technique related to scrolling web pages on any browser. According to this, there is an apparent movement between the background and forefront images and content to create an optical illusion.

The parallax effect can be easily observed in everyday routines. For example, when you see the tips of two pens aligned with each other straight through your spectacles’ lens, their position seems to be the same. But as you move from right to left, the position of the tips will change. This is mainly caused due to light refraction. However, here we are talking about the website’s interface, and to bring parallax effect, you need to add a separate block in the code base.

Flutter Developers from Flutter Agency

Does Flutter allow to introduction parallax effect?

Yes, Flutter is one such programming language with which the parallax effect can be introduced with ease. This website development framework is mainly used to define the components and elements of the user interface. Since it comes with several reusable widgets, you don’t have to code for the parallax effect from scratch.

How to add the parallax effect in the UI with Flutter?

The parallax effect has become quite popular since it creates an amazing visual appeal in the website’s user interface. However, when you are using Flutter to implement the same, there are certain steps you need to follow. The below section describes the efforts to introduce this new scrolling effect in the Flutter development UI.

How to create a list to store all parallax items?

You first need to create a list to store the parallax items in one place. It can be images, text boxes, videos, and so on. First, you need to create a stateless widget called ParallaxRecipe in which you need to develop a widget tree.

Since all the elements will be viewed on a single page, you need to use the SingleChildScrollView widget and for defining the columns, you need the widget named Column.

class ParallaxRecipe extends StatelessWidget {
  const ParallaxRecipe({super.key});

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: const [],
      ),
    );
  }
}

Display of parallax items with text and image

In the list you have created, there is a widget tree containing all the items to be displayed in the parallax. Now, the first list element is the background image. There need to be multiple images for each list. You can define the shape in which the background image will be displayed. You can also determine the aspect ratio of the shape you have chosen, provided you have opted for a rectangle or square.

If you want any text to be displayed on the screen, add the text to the code base and define its position. You can specify the position by “left” and “right” keywords and assign an integer value to each. Here, you need to define a child widget named Column, which will contain the following attributes:

a. Color of the text

b. Font size that you want to be displayed

c. Font weight like bold, semi bold, regular, light, extra light, and more

For adding multiple texts in different lines, you can define a children array, and each Text block will define the text line displayed on the background images. If you do not define any border, users will have difficulty identifying the images, especially if each image represents something unique. This border will be present between the text and the image.

Once you are defining the layout and structure of the parallax, you need to add the items to the parallax list stored in the ParallaxRecipe widget. It will extend the StatelessWidget. If the values are recurring, you need to implement the loop and keep the attributes that will store the text data within this loop.

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

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(
        body: Center(
          child: ExampleParallax(),
        ),
      ),
    );
  }
}

class ExampleParallax extends StatelessWidget {
  const ExampleParallax({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          for (final location in locations)
            LocationListItem(
              imageUrl: location.imageUrl,
              name: location.name,
              country: location.place,
            ),
        ],
      ),
    );
  }
}

class LocationListItem extends StatelessWidget {
  LocationListItem({
    super.key,
    required this.imageUrl,
    required this.name,
    required this.country,
  });

  final String imageUrl;
  final String name;
  final String country;
  final GlobalKey _backgroundImageKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
      child: AspectRatio(
        aspectRatio: 16 / 9,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(16),
          child: Stack(
            children: [
              _buildParallaxBackground(context),
              _buildGradient(),
              _buildTitleAndSubtitle(),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildParallaxBackground(BuildContext context) {
    return Flow(
      delegate: ParallaxFlowDelegate(
        scrollable: Scrollable.of(context)!,
        listItemContext: context,
        backgroundImageKey: _backgroundImageKey,
      ),
      children: [
        Image.network(
          imageUrl,
          key: _backgroundImageKey,
          fit: BoxFit.cover,
        ),
      ],
    );
  }

  Widget _buildGradient() {
    return Positioned.fill(
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            stops: const [0.6, 0.95],
          ),
        ),
      ),
    );
  }

  Widget _buildTitleAndSubtitle() {
    return Positioned(
      left: 20,
      bottom: 20,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            name,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          Text(
            country,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 14,
            ),
          ),
        ],
      ),
    );
  }
}

class ParallaxFlowDelegate extends FlowDelegate {
  ParallaxFlowDelegate({
    required this.scrollable,
    required this.listItemContext,
    required this.backgroundImageKey,
  }) : super(repaint: scrollable.position);


  final ScrollableState scrollable;
  final BuildContext listItemContext;
  final GlobalKey backgroundImageKey;

  @override
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
    return BoxConstraints.tightFor(
      width: constraints.maxWidth,
    );
  }

  @override
  void paintChildren(FlowPaintingContext context) {
    // Calculate the position of this list item within the viewport.
    final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
    final listItemBox = listItemContext.findRenderObject() as RenderBox;
    final listItemOffset = listItemBox.localToGlobal(
        listItemBox.size.centerLeft(Offset.zero),
        ancestor: scrollableBox);

    // Determine the percent position of this list item within the
    // scrollable area.
    final viewportDimension = scrollable.position.viewportDimension;
    final scrollFraction =
        (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0);

    // Calculate the vertical alignment of the background
    // based on the scroll percent.
    final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);

    // Convert the background alignment into a pixel offset for
    // painting purposes.
    final backgroundSize =
        (backgroundImageKey.currentContext!.findRenderObject() as RenderBox)
            .size;
    final listItemSize = context.size;
    final childRect =
        verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);

    // Paint the background.
    context.paintChild(
      0,
      transform:
          Transform.translate(offset: Offset(0.0, childRect.top)).transform,
    );
  }

  @override
  bool shouldRepaint(ParallaxFlowDelegate oldDelegate) {
    return scrollable != oldDelegate.scrollable ||
        listItemContext != oldDelegate.listItemContext ||
        backgroundImageKey != oldDelegate.backgroundImageKey;
  }
}

class Parallax extends SingleChildRenderObjectWidget {
  const Parallax({
    super.key,
    required Widget background,
  }) : super(child: background);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderParallax(scrollable: Scrollable.of(context)!);
  }

  @override
  void updateRenderObject(
      BuildContext context, covariant RenderParallax renderObject) {
    renderObject.scrollable = Scrollable.of(context)!;
  }
}

class ParallaxParentData extends ContainerBoxParentData<RenderBox> {}

class RenderParallax extends RenderBox
    with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin {
  RenderParallax({
    required ScrollableState scrollable,
  }) : _scrollable = scrollable;

  ScrollableState _scrollable;

  ScrollableState get scrollable => _scrollable;

  set scrollable(ScrollableState value) {
    if (value != _scrollable) {
      if (attached) {
        _scrollable.position.removeListener(markNeedsLayout);
      }
      _scrollable = value;
      if (attached) {
        _scrollable.position.addListener(markNeedsLayout);
      }
    }
  }

  @override
  void attach(covariant PipelineOwner owner) {
    super.attach(owner);
    _scrollable.position.addListener(markNeedsLayout);
  }

  @override
  void detach() {
    _scrollable.position.removeListener(markNeedsLayout);
    super.detach();
  }

  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! ParallaxParentData) {
      child.parentData = ParallaxParentData();
    }
  }

  @override
  void performLayout() {
    size = constraints.biggest;

    // Force the background to take up all available width
    // and then scale its height based on the image's aspect ratio.
    final background = child!;
    final backgroundImageConstraints =
        BoxConstraints.tightFor(width: size.width);
    background.layout(backgroundImageConstraints, parentUsesSize: true);

    // Set the background's local offset, which is zero.
    (background.parentData as ParallaxParentData).offset = Offset.zero;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // Get the size of the scrollable area.
    final viewportDimension = scrollable.position.viewportDimension;

    // Calculate the global position of this list item.
    final scrollableBox = scrollable.context.findRenderObject() as RenderBox;
    final backgroundOffset =
        localToGlobal(size.centerLeft(Offset.zero), ancestor: scrollableBox);

    // Determine the percent position of this list item within the
    // scrollable area.
    final scrollFraction =
        (backgroundOffset.dy / viewportDimension).clamp(0.0, 1.0);

    // Calculate the vertical alignment of the background
    // based on the scroll percent.
    final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1);

    // Convert the background alignment into a pixel offset for
    // painting purposes.
    final background = child!;
    final backgroundSize = background.size;
    final listItemSize = size;
    final childRect =
        verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize);

    // Paint the background.
    context.paintChild(
        background,
        (background.parentData as ParallaxParentData).offset +
            offset +
            Offset(0.0, childRect.top));
  }
}

class Location {
  const Location({
    required this.name,
    required this.place,
    required this.imageUrl,
  });

  final String name;
  final String place;
  final String imageUrl;
}

const urlPrefix =
    'https://docs.flutter.dev/cookbook/img-files/effects/parallax';
const locations = [
  Location(
    name: 'Mount Rushmore',
    place: 'U.S.A',
    imageUrl: '$urlPrefix/01-mount-rushmore.jpg',
  ),
  Location(
    name: 'Gardens By The Bay',
    place: 'Singapore',
    imageUrl: '$urlPrefix/02-singapore.jpg',
  ),
  Location(
    name: 'Machu Picchu',
    place: 'Peru',
    imageUrl: '$urlPrefix/03-machu-picchu.jpg',
  ),
  Location(
    name: 'Vitznau',
    place: 'Switzerland',
    imageUrl: '$urlPrefix/04-vitznau.jpg',
  ),
  Location(
    name: 'Bali',
    place: 'Indonesia',
    imageUrl: '$urlPrefix/05-bali.jpg',
  ),
  Location(
    name: 'Mexico City',
    place: 'Mexico',
    imageUrl: '$urlPrefix/06-mexico-city.jpg',
  ),
  Location(
    name: 'Cairo',
    place: 'Egypt',
    imageUrl: '$urlPrefix/07-cairo.jpg',
  ),
];

Output

How to implement the Parallax effect in the codebase?

The basic logic for implementing the parallax effect is to introduce slight opposition in the direction flow of the pictures (list items) and the screen. For example, if the screen is scrolled down, the images will appear to move up and vice versa. The current position of a list is defined in the Scrollable class. However, it won’t be changed until the entire webpage layout phase is over.

Flutter developers will be able to change the position of the background images only in the painting phase. Therefore, you need the Flow widget to establish more control over changing the list items and the background images on the go. If the Flow child has more than one child, you need to use the widget FlowDelegate to decide on each child’s block’s time and position.

To define the background images’ position, you must incorporate the three most important factors. These are:

a. Image size post scaling down according to the aspect ratio of the list

b. Scroller bounds

c. Bound of an individual item stored in the list

Conclusion

In this article, we have shared the brief about adding the parallax effect to the Flutter applicaton in the easiest possible manner. Since the StatelessWidget, ParallaxRecipe is already available in the SDK of Flutter, all you need to do is define different parameters for the list of items stored inside the widget tree. Implementation of Parallax is much easier with this framework than with others.

If you want to develop an Flutter app for your business then you can connect with Flutter app development company like Flutter Agency who can give the assistance for your project and also take care that the app must perfect fit into your budget.

Frequently Asked Questions (FAQs)

1. What is scrollbar class in Flutter?

An object of the Scrollbar class is utilized to include the horizontal and vertical scrollbar. It is the GUI components which permits us to view the invisible number of columns and rows. It can add the top-level container such as a frame or component like a panel. It extends the component class.

2. State the flow delegate widget in the Flutter app

Flow comes under the widget library that sizes and positions its children proficiently as it is indicated by the rationale of a FlowDelegate. This is helpful when the children should reposition the utilized changed matrices.

3. Define stateless widget

A widget that does not need the mutable state. The stateless widget will describe the part of an user interface by creating the constellation of the other widgets which will describe an user interface more concretely.

Book Your Flutter Developer Now

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

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

Discuss Your Project

Connect with Flutter Agency's proficient skilled team for your app development projects across different technologies. We'd love to hear from you! Fill out the form below to discuss your project.

Have Project For Us

Get in Touch

"*" indicates required fields

ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields