Flutter Parallax Scroll Effect with PageView
Parallax effects are awesome. Having elements move at different speeds during scrolling can easily provide a unique feeling for the application and they can make the user think that your app is well-polished. In this post, I will try to achieve a parallax effect using PageView, Transforms, Alignments, and some basic math.
Parallax effect —
parallax scrolling is a website technique where we make our background move at a little slower pace than the foreground we make. This is a 3D effect as the user scrolls down the site, putting in a sense of depth and forming a more mesmeric experience while browsing.
Our eyes recognize objects close to us comparatively larger than those far away from our eyes. We perceive distant objects as if they are moving very slowly. Parallax bounds the same idea as human eyes which is a so-called optical illusion.
flutter makes it very easy to implement the parallax effect in our apps by using a few widgets, such as Stack, Positioned, etc. We’re going to use the Stack and Positioned widget in the demo project below. With the help of flutter widgets here we quickly can implement parallax scrolling.
What is a Stack widget?
It is a widget in Flutter SDK that allows us to make layers of widgets by putting one over the other.
This class is significant if you want to overlap several children simply. It is like putting objects in a single bucket one after the other(First goes in, last comes out). Similarly, if we assemble the same example pattern in a flutter, we could imagine having a few texts and an image, overlaid with a gradient and an elevated button attached at the bottom.
It gets mixed very smoothly and makes the UI much more interactive than before.
Constructor of Stack Class:
Stack(
{Key key,
AlignmentGeometry alignment: AlignmentDirectional.topStart,
TextDirection textDirection,
StackFit fit: StackFit.loose,
Overflow overflow: Overflow.clip,
Clip clipBehavior: Clip.hardEdge,
List<Widget> children: const <Widget>[]}
)
Here’s how our main.dart file would look like-
import 'package:flutter/material.dart'; import 'package:google_signin/pages/login_page.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Google SignIn', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomeScreen(), debugShowCheckedModeBanner: false, ); } }
import 'package:flutter/material.dart';
import 'package:parallaxeffect/widgets/SlidingCardView.dart';
import 'package:parallaxeffect/widgets/header.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 8),
Header(),
SizedBox(height: 20),
SlidingCardView(),
],
),
)
],
),
);
}
}
import 'package:flutter/material.dart';
class Header extends StatelessWidget {
const Header({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 32),
child: Text(
"Parallax Effect",
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
),
);
}
}
Create a Model folder in lib and create a file for demo dataimport 'package:flutter/material.dart';
import 'package:parallaxeffect/models/carddata.dart';
import 'dart:math' as math;
import 'package:parallaxeffect/screens/detail.dart';
class SlidingCardView extends StatefulWidget {
const SlidingCardView({super.key});
@override
State<SlidingCardView> createState() => _SlidingCardViewState();
}
class _SlidingCardViewState extends State<SlidingCardView> {
late PageController pageController;
late List<CardData> card = [
CardData("Water pic", "assets/images/0.jpeg"),
CardData("Color pic", "assets/images/1.jpeg"),
CardData("Space pic", "assets/images/2.jpeg"),
CardData("Wave pic", "assets/images/3.jpeg"),
CardData("light pic", "assets/images/4.jpeg"),
CardData("zoom pic", "assets/images/5.jpeg")
];
@override
void initState() {
// TODO: implement initState
super.initState();
pageController = PageController(viewportFraction: 0.8);
}
@override
void dispose() {
// TODO: implement dispose
pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.55,
child: PageView.builder(
clipBehavior: Clip.none,
controller: pageController,
itemCount: card.length, //dynamic list show
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: pageController,
builder: (context, child) {
double pageOffset = 0;
if (pageController.position.haveDimensions) {
pageOffset = pageController.page! - index;
}
double gauss =
math.exp(-(math.pow((pageOffset.abs() - 0.5), 2) / 0.08));
return Transform.translate(
offset: Offset(-32 * gauss * pageOffset.sign, 0),
child: Container(
clipBehavior: Clip.none,
margin: EdgeInsets.only(left: 8, right: 8, bottom: 24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(32),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(8, 20),
blurRadius: 24)
]),
child: Column(
children: [
ClipRRect(
borderRadius:
BorderRadius.vertical(top: Radius.circular(32)),
child: Image.asset(
card[index].url,
height: MediaQuery.of(context).size.height * 0.4,
alignment: Alignment(-pageOffset.abs(), 0),
fit: BoxFit.none,
),
),
Expanded(child: child!)
],
),
),
);
},
child: Column(
children: [
SizedBox(
height: 8,
),
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(card[index].title,
style: TextStyle(fontSize: 20)),
),
Spacer(),
Row(
children: [
ElevatedButton(
onPressed: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => DetaiScreen()));
},
child: Text(
"Detail",
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
textStyle: TextStyle(color: Colors.white),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32))),
),
SizedBox(
width: 16,
)
],
)
],
),
),
)
],
),
);
},
),
);
}
}
class CardData {
final String title;
final String url;
CardData(this.title, this.url);
}
Output: