fluid_carousel 0.1.0
fluid_carousel: ^0.1.0 copied to clipboard
A modern, fluid, and extensible carousel widget for Flutter with zero-rebuild reactive indicators, 3D/parallax transformers, desktop gestures, and sliver support.
import 'package:flutter/material.dart';
import 'package:fluid_carousel/fluid_carousel.dart';
void main() {
runApp(const FluidCarouselExampleApp());
}
class FluidCarouselExampleApp extends StatelessWidget {
const FluidCarouselExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fluid Carousel Showcase',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.dark,
),
home: const CarouselShowcaseScreen(),
);
}
}
enum TransformerType { coverflow, scale, parallax, stack }
enum IndicatorType { worm, expanding, slidingBar }
class CarouselShowcaseScreen extends StatefulWidget {
const CarouselShowcaseScreen({super.key});
@override
State<CarouselShowcaseScreen> createState() => _CarouselShowcaseScreenState();
}
class _CarouselShowcaseScreenState extends State<CarouselShowcaseScreen> {
late final FluidCarouselController _controller;
TransformerType _transformerType = TransformerType.coverflow;
IndicatorType _indicatorType = IndicatorType.worm;
double _viewportFraction = 0.82;
bool _autoPlay = false;
bool _loop = true;
final List<Map<String, dynamic>> _items = [
{
'title': 'Aurora Borealis',
'category': 'Nature & Exploration',
'colors': [
const Color(0xFF0F2027),
const Color(0xFF203A43),
const Color(0xFF2C5364)
],
'icon': Icons.nightlight_round,
},
{
'title': 'Solar Flares',
'category': 'Astrophysics',
'colors': [const Color(0xFFF12711), const Color(0xFFF5AF19)],
'icon': Icons.wb_sunny_rounded,
},
{
'title': 'Deep Oceanic Trench',
'category': 'Marine Biology',
'colors': [const Color(0xFF1CB5E0), const Color(0xFF000046)],
'icon': Icons.water_drop_rounded,
},
{
'title': 'Cyber Metropolis',
'category': 'Future Cities',
'colors': [
const Color(0xFF8A2387),
const Color(0xFFE94057),
const Color(0xFFF27121)
],
'icon': Icons.location_city_rounded,
},
{
'title': 'Emerald Canopy',
'category': 'Rainforest Habitats',
'colors': [const Color(0xFF134E5E), const Color(0xFF71B280)],
'icon': Icons.park_rounded,
},
];
@override
void initState() {
super.initState();
_controller = FluidCarouselController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
FluidTransformer get _activeTransformer {
switch (_transformerType) {
case TransformerType.coverflow:
return const FluidCoverflowTransformer(
maxRotationAngle: 0.35,
depthOffset: 60.0,
perspective: 0.0015,
);
case TransformerType.scale:
return const FluidScaleTransformer(
minScale: 0.82,
minOpacity: 0.5,
);
case TransformerType.parallax:
return const FluidParallaxTransformer(
parallaxFactor: 0.4,
minScale: 0.9,
);
case TransformerType.stack:
return const FluidCardStackTransformer(
stackOffset: 24.0,
scaleStep: 0.08,
maxVisibleCards: 3,
);
}
}
FluidIndicatorEffect get _activeIndicatorEffect {
switch (_indicatorType) {
case IndicatorType.worm:
return const FluidWormEffect(
activeColor: Colors.amberAccent,
color: Colors.white24,
dotWidth: 12.0,
dotHeight: 12.0,
spacing: 10.0,
);
case IndicatorType.expanding:
return const FluidExpandingDotsEffect(
activeColor: Colors.cyanAccent,
color: Colors.white24,
dotWidth: 10.0,
dotHeight: 10.0,
expansionFactor: 3.2,
spacing: 8.0,
);
case IndicatorType.slidingBar:
return const FluidSlidingBarEffect(
activeColor: Colors.pinkAccent,
color: Colors.white24,
trackWidth: 140.0,
dotHeight: 6.0,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Fluid Carousel Showcase',
style: TextStyle(fontWeight: FontWeight.bold)),
elevation: 0,
actions: [
IconButton(
tooltip: 'Previous Page',
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () => _controller.previousPage(),
),
IconButton(
tooltip: 'Next Page',
icon: const Icon(Icons.arrow_forward_ios_rounded),
onPressed: () => _controller.nextPage(),
),
const SizedBox(width: 8),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Sub-pixel live telemetry banner
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ValueListenableBuilder<int>(
valueListenable: _controller.currentPage,
builder: (context, page, _) {
return Column(
children: [
const Text('SETTLED PAGE',
style: TextStyle(
fontSize: 10,
color: Colors.white54,
letterSpacing: 1.1)),
Text('$page / ${_items.length - 1}',
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16)),
],
);
},
),
ValueListenableBuilder<double>(
valueListenable: _controller.pageOffset,
builder: (context, offset, _) {
return Column(
children: [
const Text('SUB-PIXEL OFFSET',
style: TextStyle(
fontSize: 10,
color: Colors.white54,
letterSpacing: 1.1)),
Text(offset.toStringAsFixed(3),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.amberAccent)),
],
);
},
),
],
),
),
),
const SizedBox(height: 24),
// Fluid Carousel Widget
SizedBox(
height: 260,
child: FluidCarousel(
key:
ValueKey('carousel_${_transformerType}_$_viewportFraction'),
controller: _controller,
itemCount: _items.length,
transformer: _activeTransformer,
options: FluidCarouselOptions(
viewportFraction: _viewportFraction,
loop: _loop,
autoPlay: _autoPlay,
enableMouseDrag: true,
enableKeyboardNavigation: true,
),
itemBuilder: (context, index) {
final item = _items[index];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: item['colors'] as List<Color>,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: (item['colors'] as List<Color>)
.first
.withValues(alpha: 0.4),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
item['category'] as String,
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 1.2),
),
Icon(item['icon'] as IconData,
color: Colors.white, size: 28),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title'] as String,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'Card index #$index',
style: const TextStyle(
color: Colors.white60, fontSize: 13),
),
],
),
],
),
);
},
),
),
const SizedBox(height: 20),
// Zero-Rebuild Reactive Indicator
Center(
child: FluidIndicator(
controller: _controller,
count: _items.length,
effect: _activeIndicatorEffect,
),
),
const SizedBox(height: 32),
// Configuration Controls
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('VISUAL TRANSFORMER',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
letterSpacing: 1.1)),
const SizedBox(height: 8),
SegmentedButton<TransformerType>(
segments: const [
ButtonSegment(
value: TransformerType.coverflow,
label: Text('3D Coverflow')),
ButtonSegment(
value: TransformerType.scale, label: Text('Scale')),
ButtonSegment(
value: TransformerType.parallax,
label: Text('Parallax')),
ButtonSegment(
value: TransformerType.stack, label: Text('Stack')),
],
selected: {_transformerType},
onSelectionChanged: (selected) {
setState(() => _transformerType = selected.first);
},
),
const SizedBox(height: 20),
const Text('INDICATOR STYLE',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
letterSpacing: 1.1)),
const SizedBox(height: 8),
SegmentedButton<IndicatorType>(
segments: const [
ButtonSegment(
value: IndicatorType.worm,
label: Text('Elastic Worm')),
ButtonSegment(
value: IndicatorType.expanding,
label: Text('Expanding Dots')),
ButtonSegment(
value: IndicatorType.slidingBar,
label: Text('Sliding Bar')),
],
selected: {_indicatorType},
onSelectionChanged: (selected) {
setState(() => _indicatorType = selected.first);
},
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: Text(
'Viewport Fraction: ${_viewportFraction.toStringAsFixed(2)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
Slider(
value: _viewportFraction,
min: 0.6,
max: 1.0,
divisions: 8,
onChanged: (val) =>
setState(() => _viewportFraction = val),
),
],
),
SwitchListTile(
title:
const Text('Auto-Play (Pauses on Touch & Mouse Hover)'),
value: _autoPlay,
onChanged: (val) => setState(() => _autoPlay = val),
),
SwitchListTile(
title: const Text('Infinite Circular Looping'),
value: _loop,
onChanged: (val) => setState(() => _loop = val),
),
],
),
),
],
),
),
);
}
}