parallax_onboarding 0.1.0
parallax_onboarding: ^0.1.0 copied to clipboard
A parallax onboarding carousel for Flutter with per-layer depth, slot-based pages, page-indicator dots, and first-class RTL and reduced-motion support.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:parallax_onboarding/parallax_onboarding.dart';
void main() => runApp(const DemoApp());
/// One onboarding slide expressed as plain data so the demo can map it onto
/// [OnboardingPage]s without repeating layout code.
class _Slide {
const _Slide({
required this.icon,
required this.title,
required this.body,
required this.accent,
required this.top,
required this.bottom,
});
final IconData icon;
final String title;
final String body;
final Color accent;
final Color top;
final Color bottom;
}
const List<_Slide> _slides = <_Slide>[
_Slide(
icon: Icons.layers_rounded,
title: 'Depth in every swipe',
body: 'Background, content and foreground glide at their own pace.',
accent: Color(0xFF6C8CFF),
top: Color(0xFF2B3A67),
bottom: Color(0xFF0B1026),
),
_Slide(
icon: Icons.swipe_rounded,
title: 'Swipe or tap',
body: 'Flick through pages, or use the built-in Next and Skip controls.',
accent: Color(0xFF36D6C3),
top: Color(0xFF0E5A52),
bottom: Color(0xFF05201D),
),
_Slide(
icon: Icons.accessibility_new_rounded,
title: 'Built for everyone',
body: 'First-class RTL and reduced-motion support, out of the box.',
accent: Color(0xFFB388FF),
top: Color(0xFF4A2A6B),
bottom: Color(0xFF1A0E2B),
),
_Slide(
icon: Icons.rocket_launch_rounded,
title: 'Ready to ship',
body: 'Drop in your pages and onboard your users in minutes.',
accent: Color(0xFFFFB270),
top: Color(0xFF8A4B1F),
bottom: Color(0xFF2B1206),
),
];
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'parallax_onboarding',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: Colors.black,
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: Colors.white),
),
),
home: const OnboardingDemo(),
);
}
}
class OnboardingDemo extends StatefulWidget {
const OnboardingDemo({super.key});
@override
State<OnboardingDemo> createState() => _OnboardingDemoState();
}
class _OnboardingDemoState extends State<OnboardingDemo> {
late final ParallaxOnboardingController _controller;
Timer? _autoplay;
int _direction = 1;
@override
void initState() {
super.initState();
_controller = ParallaxOnboardingController();
// Auto-play a smooth ping-pong sweep so the README capture shows the
// parallax in motion without anyone touching the screen.
_autoplay = Timer.periodic(const Duration(milliseconds: 2200), (_) {
if (!mounted) return;
final current = _controller.index;
if (current >= _slides.length - 1) {
_direction = -1;
} else if (current <= 0) {
_direction = 1;
}
_controller.animateToPage(
current + _direction,
duration: const Duration(milliseconds: 800),
curve: Curves.easeInOutCubic,
);
});
}
@override
void dispose() {
_autoplay?.cancel();
_controller.dispose();
super.dispose();
}
void _toast(String message) {
ScaffoldMessenger.of(context)
..clearSnackBars()
..showSnackBar(
SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 1),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ParallaxOnboarding(
controller: _controller,
onDone: () => _toast('Done!'),
onSkip: () => _toast('Skipped'),
background: _GradientBackground(controller: _controller),
indicatorBuilder: (context, controller) =>
_Dots(controller: controller),
pages: <OnboardingPage>[
for (final slide in _slides)
OnboardingPage(
backgroundFactor: -0.35,
contentFactor: 0.0,
foregroundFactor: 0.6,
foregroundAlignment: Alignment.bottomRight,
background: _Blobs(accent: slide.accent),
content: _Content(slide: slide),
foreground: Padding(
padding: const EdgeInsets.only(right: 12, bottom: 96),
child: Icon(
slide.icon,
size: 200,
color: slide.accent.withValues(alpha: 0.16),
),
),
),
],
),
);
}
}
/// Cross-fades the page gradient as the controller scrolls between slides.
class _GradientBackground extends StatelessWidget {
const _GradientBackground({required this.controller});
final ParallaxOnboardingController controller;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
final page = controller.page.clamp(0.0, (_slides.length - 1).toDouble());
final lower = page.floor();
final upper = (lower + 1).clamp(0, _slides.length - 1);
final t = page - lower;
final top = Color.lerp(_slides[lower].top, _slides[upper].top, t)!;
final bottom =
Color.lerp(_slides[lower].bottom, _slides[upper].bottom, t)!;
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[top, bottom],
),
),
);
},
);
}
}
/// Two soft accent circles that drift with the foreground parallax layer.
class _Blobs extends StatelessWidget {
const _Blobs({required this.accent});
final Color accent;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: -60,
left: -40,
child: _circle(220, accent.withValues(alpha: 0.22)),
),
Positioned(
bottom: 40,
right: -70,
child: _circle(260, accent.withValues(alpha: 0.14)),
),
],
);
}
Widget _circle(double size, Color color) => Container(
width: size,
height: size,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
/// The icon badge, title and body for a single slide.
class _Content extends StatelessWidget {
const _Content({required this.slide});
final _Slide slide;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: slide.accent.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(18),
),
child: Icon(slide.icon, size: 32, color: slide.accent),
),
const SizedBox(height: 28),
Text(
slide.title,
style: const TextStyle(
fontSize: 30,
fontWeight: FontWeight.w700,
color: Colors.white,
height: 1.1,
),
),
const SizedBox(height: 14),
Text(
slide.body,
style: const TextStyle(fontSize: 16, color: Colors.white70),
),
],
),
);
}
}
/// A compact animated page indicator built on top of the controller, used to
/// show how [ParallaxOnboarding.indicatorBuilder] can be fully customized.
class _Dots extends StatelessWidget {
const _Dots({required this.controller});
final ParallaxOnboardingController controller;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
final page = controller.page;
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
for (var i = 0; i < controller.count; i++)
_dot(distance: (page - i).abs().clamp(0.0, 1.0)),
],
);
},
);
}
Widget _dot({required double distance}) {
final width = lerpDouble(22, 8, distance)!;
final opacity = lerpDouble(1.0, 0.4, distance)!;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 3),
width: width,
height: 8,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: opacity),
borderRadius: BorderRadius.circular(4),
),
);
}
}
double? lerpDouble(num a, num b, double t) => a + (b - a) * t;