Creating a Parallax Animation with Persistent Shapes in Flutter
In our previous article, Building Adaptive Interfaces for Different Screen Sizes in Flutter, we explored how to design responsive UIs that adapt seamlessly across devices. Now, we’re taking things a step further by introducing parallax scrolling — a design technique that adds depth and liveliness to your UI. In this tutorial, you’ll learn how to create a parallax effect in Flutter with persistent shapes, localized greetings, and smooth scrolling behavior. Let’s dive in!
Parallax scrolling is a design technique that creates an illusion of depth by moving elements at different speeds relative to the user’s interaction. This tutorial demonstrates how to implement a parallax scrolling effect in Flutter with:
- Persistent shapes that maintain their position during scrolling.
- A real parallax effect achieved by dynamically offsetting elements.
- A clean architecture using a custom data model.
Let’s dive into how it works.
Setting Up the Project
First, we need a Flutter application where we can display a vertically scrollable list of cards. Each card will have:
- A gradient background.
- Localized greetings in different languages.
- Custom shapes that move at different speeds to create a parallax effect.
Basic Setup
Begin by creating the main app and defining the screen structure:
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ParallaxScreen(),
debugShowCheckedModeBanner: false,
);
}
}
The MyApp
widget initializes the app, and ParallaxScreen
will host our scrollable content.
Defining Data Models
To organize data effectively, let’s create a custom LanguageGreeting
class. This class will store a language and its corresponding greeting:
class LanguageGreeting {
final String language;
final String greeting;
LanguageGreeting({required this.language, required this.greeting});
}
We’ll also define a list of LanguageGreeting
objects:
final List<LanguageGreeting> languageGreetings = [
LanguageGreeting(language: "English", greeting: "Happy New Year!"),
LanguageGreeting(language: "German", greeting: "Frohes Neues Jahr!"),
LanguageGreeting(language: "Spanish", greeting: "Feliz Año Nuevo!"),
LanguageGreeting(language: "French", greeting: "Bonne Année!"),
LanguageGreeting(language: "Italian", greeting: "Buon Anno!"),
LanguageGreeting(language: "Russian", greeting: "С Новым Годом!"),
LanguageGreeting(language: "Portuguese", greeting: "Feliz Ano Novo!"),
LanguageGreeting(language: "Dutch", greeting: "Gelukkig Nieuwjaar!"),
LanguageGreeting(language: "Chinese", greeting: "新年快乐!"),
LanguageGreeting(language: "Korean", greeting: "새해 복 많이 받으세요!"),
];
This structure ensures that our data is clean and easy to manage.
Creating the Scrollable List
We’ll now create a scrollable list of cards. Each card will display a greeting and its corresponding language.
class ParallaxScreen extends StatefulWidget {
@override
_ParallaxScreenState createState() => _ParallaxScreenState();
}
class _ParallaxScreenState extends State<ParallaxScreen> {
final ScrollController _scrollController = ScrollController();
// Cache to store shapes for each card
final Map<int, List<Map<String, dynamic>>> shapesCache = {};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Parallax Animation"),
backgroundColor: Colors.deepPurple,
),
body: ListView.builder(
controller: _scrollController,
itemCount: languageGreetings.length,
itemBuilder: (context, index) {
return ParallaxCard(
scrollController: _scrollController,
languageGreeting: languageGreetings[index],
itemIndex: index,
shapesCache: shapesCache,
);
},
),
);
}
}
Here:
ListView.builder
dynamically creates and disposes of cards based on their visibility.shapesCache
ensures shapes are persistent, even if a card is re-rendered.
Adding Persistent Parallax Cards
Each card will display:
- A localized greeting and language.
- A gradient background.
- Custom shapes that move at different speeds to create the parallax effect.
How Parallax Works
- ScrollController tracks the current scroll offset.
- Each shape’s position is adjusted based on:
- Its card’s position (
baseOffset
). - The scroll offset (
scrollOffset
). - Its unique
parallaxFactor
.
This creates the illusion of depth, as shapes in the foreground move faster than those in the background.
Parallax Card
class ParallaxCard extends StatefulWidget {
final ScrollController scrollController;
final LanguageGreeting languageGreeting;
final int itemIndex;
final Map<int, List<Map<String, dynamic>>> shapesCache;
ParallaxCard({
required this.scrollController,
required this.languageGreeting,
required this.itemIndex,
required this.shapesCache,
});
@override
_ParallaxCardState createState() => _ParallaxCardState();
}
class _ParallaxCardState extends State<ParallaxCard> {
static const double containerHeight = 300;
late final List<Map<String, dynamic>> shapes;
@override
void initState() {
super.initState();
// Generate shapes if not already cached
shapes = widget.shapesCache[widget.itemIndex] ??
_generateShapes(); // Generate new shapes if none are cached
// Cache shapes for future reuse
widget.shapesCache[widget.itemIndex] = shapes;
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return AnimatedBuilder(
animation: widget.scrollController,
builder: (context, child) {
return Container(
height: containerHeight,
margin: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blueAccent, Colors.purpleAccent],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(15),
),
child: Stack(
children: [
...shapes.map((shape) => _buildShape(shape, screenWidth)).toList(),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.languageGreeting.greeting,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 10),
Text(
widget.languageGreeting.language,
style: TextStyle(
fontSize: 18,
color: Colors.white70,
),
),
],
),
),
],
),
);
},
);
}
Widget _buildShape(Map<String, dynamic> shapeData, double screenWidth) {
double scrollOffset = widget.scrollController.offset;
double baseOffset = widget.itemIndex * containerHeight;
double parallaxOffset = (baseOffset - scrollOffset) * shapeData['parallaxFactor'];
return Positioned(
left: shapeData['position'].dx * screenWidth,
top: shapeData['position'].dy * containerHeight + parallaxOffset,
child: Icon(
shapeData['icon'],
size: shapeData['size'],
color: shapeData['color'],
),
);
}
List<Map<String, dynamic>> _generateShapes() {
final random = Random();
final List<IconData> icons = [Icons.circle, Icons.star, Icons.square_rounded];
return List.generate(random.nextInt(5) + 3, (index) {
return {
'icon': icons[random.nextInt(icons.length)],
'size': random.nextDouble() * 40 + 20,
'color': Colors.primaries[random.nextInt(Colors.primaries.length)]
.withOpacity(0.7),
'position': Offset(
random.nextDouble(),
random.nextDouble(),
),
'parallaxFactor': 0.1 + random.nextDouble() * 0.2,
};
});
}
}
Conclusion
This tutorial demonstrated how to create a parallax scrolling effect in Flutter, with:
- Persistent shapes stored in a cache for efficient rendering.
- A real parallax effect achieved through dynamic offsets.
- A structured and scalable architecture using a custom data model.
Try experimenting with different shapes, animations, and layouts to create your own engaging UI designs!
Congratulations on creating a dynamic parallax scrolling effect in Flutter! This tutorial showcased how to combine responsive design principles with engaging visual techniques to enhance your app’s user experience. If you enjoyed this article and want to explore more Flutter tips, tricks, and tutorials, don’t forget to subscribe and stay tuned for our next deep dive. Let’s build something amazing together!