motionx 0.1.0
motionx: ^0.1.0 copied to clipboard
A modern, highly customizable Flutter bottom navigation bar with floating indicators, smooth animations, badges, PageView synchronization, and extensible APIs.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:motionx/motionx.dart';
void main() {
runApp(const MotionXShowcaseApp());
}
class MotionXShowcaseApp extends StatelessWidget {
const MotionXShowcaseApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MotionX Interactive Showcase',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
home: const MotionXShowcaseHome(),
);
}
}
class MotionXShowcaseHome extends StatefulWidget {
const MotionXShowcaseHome({super.key});
@override
State<MotionXShowcaseHome> createState() => _MotionXShowcaseHomeState();
}
class _MotionXShowcaseHomeState extends State<MotionXShowcaseHome> {
late final MotionXController _navController;
late final PageController _pageController;
// Customization Studio State
MotionXIndicatorShape _indicatorShape = MotionXIndicatorShape.floatingCircle;
double _indicatorSize = 54.0;
Color _indicatorColor = const Color(0xFF6366F1);
bool _enableShadows = true;
bool _notchEnabled = true;
double _notchDepth = 0.5;
double _notchCornerRadius = 14.0;
bool _useSmoothNotch = true;
// Badge Studio State
bool _showBadges = true;
String _badgeText = '8';
String _badgePositionPreset = 'topRight';
double _badgeOffsetX = 6.0;
double _badgeOffsetY = -4.0;
double _badgeSize = 22.0;
double _badgeContentSize = 11.0;
double _badgePaddingH = 6.0;
double _badgePaddingV = 3.0;
double _badgeBorderRadius = 11.0;
bool _badgeFollowIndicator = true;
bool _badgeEnableBorder = true;
bool _badgeEnableShadow = true;
bool _enableHaptics = true;
double _barHeight = 65.0;
bool _useSafeArea = true;
bool _isRtl = false;
// Interceptor and Animation settings
bool _enableInterceptor = false;
double _animationDurationMs = 350.0;
String _curveName = 'easeOutCubic';
int _comparisonTab = 0;
final List<Color> _accentColors = const [
Color(0xFF6366F1), // Indigo
Color(0xFF10B981), // Emerald
Color(0xFFEC4899), // Pink
Color(0xFFF59E0B), // Amber
Color(0xFF8B5CF6), // Purple
];
@override
void initState() {
super.initState();
_navController = MotionXController.create(initialIndex: 0);
_pageController = PageController(initialPage: 0);
_navController.attachPageController(_pageController);
}
@override
void dispose() {
_pageController.dispose();
_navController.dispose();
super.dispose();
}
Curve get _selectedCurve {
switch (_curveName) {
case 'SpringCurve':
return const SpringCurve();
case 'easeInOutBack':
return Curves.easeInOutBack;
case 'bounceOut':
return Curves.bounceOut;
default:
return Curves.easeOutCubic;
}
}
MotionXBadgePosition get _selectedBadgePosition {
Alignment baseAlignment;
switch (_badgePositionPreset) {
case 'topLeft':
baseAlignment = Alignment.topLeft;
break;
case 'bottomRight':
baseAlignment = Alignment.bottomRight;
break;
case 'bottomLeft':
baseAlignment = Alignment.bottomLeft;
break;
case 'topCenter':
baseAlignment = Alignment.topCenter;
break;
default:
baseAlignment = Alignment.topRight;
break;
}
return MotionXBadgePosition(
alignment: baseAlignment,
offset: Offset(_badgeOffsetX, _badgeOffsetY),
);
}
List<MotionXItem> get _items => [
const MotionXItem(
label: 'Explore',
icon: Icon(Icons.explore_outlined),
activeIcon: Icon(Icons.explore),
),
MotionXItem(
label: 'Messages',
icon: const Icon(Icons.chat_bubble_outline),
activeIcon: const Icon(Icons.chat_bubble),
badge: _showBadges && _badgeText.isNotEmpty
? MotionXBadge(
text: _badgeText,
badgeSize: _badgeSize,
contentSize: _badgeContentSize,
padding: EdgeInsets.symmetric(
horizontal: _badgePaddingH,
vertical: _badgePaddingV,
),
borderRadius: _badgeBorderRadius,
position: _selectedBadgePosition,
followIndicator: _badgeFollowIndicator,
border: _badgeEnableBorder
? Border.all(color: Colors.white, width: 1.5)
: null,
shadows: _badgeEnableShadow
? [
BoxShadow(
color: Colors.black.withValues(alpha: 0.25),
blurRadius: 4.0,
offset: const Offset(0, 2),
),
]
: null,
)
: null,
),
MotionXItem(
label: 'Updates',
icon: const Icon(Icons.notifications_none),
activeIcon: const Icon(Icons.notifications),
badge: _showBadges
? MotionXBadge.dot(
color: Colors.amber,
size: 10.0,
position: _selectedBadgePosition,
followIndicator: _badgeFollowIndicator,
border: _badgeEnableBorder
? Border.all(color: Colors.white, width: 1.5)
: null,
)
: null,
),
const MotionXItem(
label: 'Settings',
icon: Icon(Icons.settings_outlined),
activeIcon: Icon(Icons.settings),
),
];
FutureOr<bool> _tabGuardInterceptor(int targetIndex) async {
if (!_enableInterceptor) return true;
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Navigation'),
content: Text('Switch to tab ${targetIndex + 1}?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Proceed'),
),
],
),
);
return result ?? false;
}
String _generateDartCode() {
final shapeName = _indicatorShape.name;
final notchShapeCode = _useSmoothNotch
? 'SmoothNotchShape(cornerRadius: $_notchCornerRadius)'
: 'const ClassicNotchShape()';
final curveCode = _curveName == 'SpringCurve'
? 'const SpringCurve()'
: 'Curves.$_curveName';
return '''
MotionXNavigationBar(
controller: _navController,
height: $_barHeight,
enableHaptics: $_enableHaptics,
useSafeArea: $_useSafeArea,
animationDuration: Duration(milliseconds: ${_animationDurationMs.round()}),
animationCurve: $curveCode,
${_enableInterceptor ? "onTabWillChange: (targetIndex) async => true," : ""}
indicatorStyle: MotionXIndicatorStyle(
shape: MotionXIndicatorShape.$shapeName,
size: $_indicatorSize,
color: Color(0x${_indicatorColor.toARGB32().toRadixString(16).toUpperCase()}),
),
notchStyle: MotionXNotchStyle(
enabled: $_notchEnabled,
depth: $_notchDepth,
cornerRadius: $_notchCornerRadius,
shape: $notchShapeCode,
),
items: [
const MotionXItem(
label: 'Explore',
icon: Icon(Icons.explore_outlined),
activeIcon: Icon(Icons.explore),
),
MotionXItem(
label: 'Messages',
icon: const Icon(Icons.chat_bubble_outline),
badge: ${_showBadges ? "MotionXBadge(\n text: '$_badgeText',\n badgeSize: $_badgeSize,\n contentSize: $_badgeContentSize,\n padding: EdgeInsets.symmetric(horizontal: $_badgePaddingH, vertical: $_badgePaddingV),\n borderRadius: $_badgeBorderRadius,\n followIndicator: $_badgeFollowIndicator,\n position: MotionXBadgePosition(\n alignment: Alignment.$_badgePositionPreset,\n offset: Offset($_badgeOffsetX, $_badgeOffsetY),\n ),\n border: ${_badgeEnableBorder ? "Border.all(color: Colors.white, width: 1.5)" : "null"},\n )" : "null"},
),
MotionXItem(
label: 'Updates',
icon: const Icon(Icons.notifications_none),
badge: ${_showBadges ? "MotionXBadge.dot(\n followIndicator: $_badgeFollowIndicator,\n position: MotionXBadgePosition(\n alignment: Alignment.$_badgePositionPreset,\n offset: Offset($_badgeOffsetX, $_badgeOffsetY),\n ),\n )" : "null"},
),
const MotionXItem(
label: 'Settings',
icon: Icon(Icons.settings_outlined),
),
],
)
''';
}
void _showCodeModal() {
final code = _generateDartCode();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Theme.of(context).cardColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24.0)),
),
builder: (context) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.7,
maxChildSize: 0.95,
minChildSize: 0.4,
builder: (context, scrollController) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Generated Code',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
ElevatedButton.icon(
icon: const Icon(Icons.copy, size: 18.0),
label: const Text('Copy'),
onPressed: () {
Clipboard.setData(ClipboardData(text: code));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Code copied to clipboard!'),
duration: Duration(seconds: 2),
),
);
Navigator.pop(context);
},
),
],
),
const SizedBox(height: 16.0),
Expanded(
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(16.0),
),
child: SingleChildScrollView(
controller: scrollController,
child: Text(
code,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13.0,
color: Color(0xFFA7F3D0),
),
),
),
),
),
],
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Widget scaffoldContent = Scaffold(
appBar: AppBar(
title: const Text(
'MotionX Showcase',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
icon: const Icon(Icons.code),
tooltip: 'View Generated Code',
onPressed: _showCodeModal,
),
],
),
body: PageView(
controller: _pageController,
children: [
_buildStudioControlPanel(theme),
_buildIndicatorGalleryScreen(theme),
_buildUpdatesScreen(theme),
_buildSettingsScreen(theme),
],
),
bottomNavigationBar: MotionXNavigationBar(
controller: _navController,
items: _items,
height: _barHeight,
useSafeArea: _useSafeArea,
backgroundColor: theme.cardColor,
enableHaptics: _enableHaptics,
animationDuration: Duration(milliseconds: _animationDurationMs.round()),
animationCurve: _selectedCurve,
onTabWillChange: _enableInterceptor ? _tabGuardInterceptor : null,
indicatorStyle: MotionXIndicatorStyle(
shape: _indicatorShape,
size: _indicatorSize,
color: _indicatorColor,
customPainter: _indicatorShape == MotionXIndicatorShape.custom
? _DemoCustomPainter(_indicatorColor)
: null,
shadows: _enableShadows
? [
BoxShadow(
color: _indicatorColor.withValues(alpha: 0.35),
blurRadius: 12.0,
offset: const Offset(0, 4),
),
]
: null,
),
notchStyle: MotionXNotchStyle(
enabled: _notchEnabled,
depth: _notchDepth,
cornerRadius: _notchCornerRadius,
shape: _useSmoothNotch
? SmoothNotchShape(cornerRadius: _notchCornerRadius)
: const ClassicNotchShape(),
),
),
);
if (_isRtl) {
scaffoldContent = Directionality(
textDirection: TextDirection.rtl,
child: scaffoldContent,
);
}
return scaffoldContent;
}
Widget _buildStudioControlPanel(ThemeData theme) {
return SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
elevation: 0,
color: theme.colorScheme.primaryContainer.withValues(alpha: 0.35),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.auto_awesome, color: theme.colorScheme.primary),
const SizedBox(width: 12.0),
Expanded(
child: Text(
'Customize every MotionX feature in real-time. Tap code (< >) above to inspect generated Dart code.',
style: theme.textTheme.bodyMedium,
),
),
],
),
),
),
const SizedBox(height: 20.0),
// Indicator Color Picker
Text(
'Indicator Color',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
Row(
children: _accentColors.map((color) {
final isSelected = _indicatorColor == color;
return GestureDetector(
onTap: () => setState(() => _indicatorColor = color),
child: Container(
margin: const EdgeInsets.only(right: 12.0),
width: 38.0,
height: 38.0,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(
color: theme.colorScheme.onSurface,
width: 3.0,
)
: null,
),
),
);
}).toList(),
),
const SizedBox(height: 20.0),
// Indicator Shape Selector
Text(
'Indicator Shape',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
Wrap(
spacing: 8.0,
children: MotionXIndicatorShape.values.map((shape) {
return ChoiceChip(
label: Text(shape.name),
selected: _indicatorShape == shape,
onSelected: (selected) {
if (selected) setState(() => _indicatorShape = shape);
},
);
}).toList(),
),
const SizedBox(height: 20.0),
// Indicator Size & Shadows
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Indicator Size',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text('${_indicatorSize.round()} px'),
],
),
Slider(
value: _indicatorSize,
min: 36.0,
max: 68.0,
divisions: 16,
onChanged: (val) => setState(() => _indicatorSize = val),
),
SwitchListTile(
title: const Text('Indicator Shadow'),
value: _enableShadows,
onChanged: (val) => setState(() => _enableShadows = val),
),
const SizedBox(height: 12.0),
// Notch Controls
SwitchListTile(
title: const Text('Notch Cutout'),
subtitle: const Text('Enable 2D notch path geometry'),
value: _notchEnabled,
onChanged: (val) => setState(() => _notchEnabled = val),
),
if (_notchEnabled) ...[
SwitchListTile(
title: const Text('Smooth Bezier Notch'),
subtitle: const Text(
'Toggle between Smooth Bezier and Classic Notch',
),
value: _useSmoothNotch,
onChanged: (val) => setState(() => _useSmoothNotch = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Notch Depth'),
Text(_notchDepth.toStringAsFixed(2)),
],
),
Slider(
value: _notchDepth,
min: 0.2,
max: 0.8,
divisions: 12,
onChanged: (val) => setState(() => _notchDepth = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Corner Radius'),
Text('${_notchCornerRadius.round()} px'),
],
),
Slider(
value: _notchCornerRadius,
min: 8.0,
max: 24.0,
divisions: 16,
onChanged: (val) => setState(() => _notchCornerRadius = val),
),
],
const Divider(height: 32.0),
// Badge System & Customization Controls
Text(
'Badge Customization Engine',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
SwitchListTile(
title: const Text('Show Badges'),
value: _showBadges,
onChanged: (val) => setState(() => _showBadges = val),
),
if (_showBadges) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: TextField(
decoration: const InputDecoration(
labelText: 'Messages Badge Text',
border: OutlineInputBorder(),
isDense: true,
),
controller: TextEditingController(text: _badgeText),
onChanged: (val) => setState(() => _badgeText = val),
),
),
SwitchListTile(
title: const Text('Follow Active Indicator Motion'),
subtitle: const Text(
'Badge lifts into indicator when tab is selected',
),
value: _badgeFollowIndicator,
onChanged: (val) => setState(() => _badgeFollowIndicator = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Badge Outer Size'),
Text('${_badgeSize.round()} px'),
],
),
Slider(
value: _badgeSize,
min: 16.0,
max: 36.0,
divisions: 20,
onChanged: (val) => setState(() => _badgeSize = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Content / Font Size'),
Text('${_badgeContentSize.round()} px'),
],
),
Slider(
value: _badgeContentSize,
min: 8.0,
max: 18.0,
divisions: 10,
onChanged: (val) => setState(() => _badgeContentSize = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Horizontal Padding (dx)'),
Text('${_badgePaddingH.round()} px'),
],
),
Slider(
value: _badgePaddingH,
min: 0.0,
max: 12.0,
divisions: 12,
onChanged: (val) => setState(() => _badgePaddingH = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Vertical Padding (dy)'),
Text('${_badgePaddingV.round()} px'),
],
),
Slider(
value: _badgePaddingV,
min: 0.0,
max: 8.0,
divisions: 8,
onChanged: (val) => setState(() => _badgePaddingV = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Border Radius'),
Text('${_badgeBorderRadius.round()} px'),
],
),
Slider(
value: _badgeBorderRadius,
min: 0.0,
max: 20.0,
divisions: 20,
onChanged: (val) => setState(() => _badgeBorderRadius = val),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Badge Position Anchor:',
style: theme.textTheme.bodySmall,
),
),
const SizedBox(height: 4.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
spacing: 8.0,
children:
[
'topRight',
'topLeft',
'bottomRight',
'bottomLeft',
'topCenter',
].map((preset) {
return ChoiceChip(
label: Text(preset),
selected: _badgePositionPreset == preset,
onSelected: (selected) {
if (selected)
setState(() => _badgePositionPreset = preset);
},
);
}).toList(),
),
),
const SizedBox(height: 12.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Offset X (dx)'),
Text('${_badgeOffsetX.toStringAsFixed(1)} px'),
],
),
Slider(
value: _badgeOffsetX,
min: -15.0,
max: 15.0,
divisions: 30,
onChanged: (val) => setState(() => _badgeOffsetX = val),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Offset Y (dy)'),
Text('${_badgeOffsetY.toStringAsFixed(1)} px'),
],
),
Slider(
value: _badgeOffsetY,
min: -15.0,
max: 15.0,
divisions: 30,
onChanged: (val) => setState(() => _badgeOffsetY = val),
),
SwitchListTile(
title: const Text('White Contrast Border Outline'),
value: _badgeEnableBorder,
onChanged: (val) => setState(() => _badgeEnableBorder = val),
),
SwitchListTile(
title: const Text('Badge Elevation Shadow'),
value: _badgeEnableShadow,
onChanged: (val) => setState(() => _badgeEnableShadow = val),
),
],
const Divider(height: 32.0),
// Animation Duration & Curve
Text(
'Animation & Physics',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Animation Duration'),
Text('${_animationDurationMs.round()} ms'),
],
),
Slider(
value: _animationDurationMs,
min: 150.0,
max: 700.0,
divisions: 11,
onChanged: (val) => setState(() => _animationDurationMs = val),
),
const SizedBox(height: 8.0),
Wrap(
spacing: 8.0,
children:
[
'easeOutCubic',
'SpringCurve',
'easeInOutBack',
'bounceOut',
].map((name) {
return ChoiceChip(
label: Text(name),
selected: _curveName == name,
onSelected: (selected) {
if (selected) setState(() => _curveName = name);
},
);
}).toList(),
),
const Divider(height: 32.0),
// Layout & Bar Options
Text(
'Layout & Interactions',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Bar Container Height'),
Text('${_barHeight.round()} px'),
],
),
Slider(
value: _barHeight,
min: 55.0,
max: 80.0,
divisions: 25,
onChanged: (val) => setState(() => _barHeight = val),
),
SwitchListTile(
title: const Text('Use SafeArea'),
value: _useSafeArea,
onChanged: (val) => setState(() => _useSafeArea = val),
),
SwitchListTile(
title: const Text('RTL Directionality'),
subtitle: const Text('Test Right-to-Left localization layout'),
value: _isRtl,
onChanged: (val) => setState(() => _isRtl = val),
),
SwitchListTile(
title: const Text('Navigation Guard Interceptor'),
subtitle: const Text('Async onTabWillChange confirmation modal'),
value: _enableInterceptor,
onChanged: (val) => setState(() => _enableInterceptor = val),
),
SwitchListTile(
title: const Text('Enable Haptics'),
value: _enableHaptics,
onChanged: (val) => setState(() => _enableHaptics = val),
),
],
),
);
}
Widget _buildIndicatorGalleryScreen(ThemeData theme) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Indicator Gallery',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4.0),
Text(
'Compare all 4 MotionX indicator shapes side-by-side.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16.0),
_buildGalleryCard(
theme,
title: '1. Floating Circle (Elevated FAB)',
shape: MotionXIndicatorShape.floatingCircle,
color: const Color(0xFF6366F1),
useNotch: true,
),
const SizedBox(height: 16.0),
_buildGalleryCard(
theme,
title: '2. Capsule (Active Background Highlight)',
shape: MotionXIndicatorShape.capsule,
color: const Color(0xFF10B981),
useNotch: false,
),
const SizedBox(height: 16.0),
_buildGalleryCard(
theme,
title: '3. Pill (Top Floating Accent Bar)',
shape: MotionXIndicatorShape.pill,
color: const Color(0xFFEC4899),
useNotch: false,
),
const SizedBox(height: 16.0),
_buildGalleryCard(
theme,
title: '4. Underline (Bottom Active Strip)',
shape: MotionXIndicatorShape.underline,
color: const Color(0xFFF59E0B),
useNotch: false,
),
],
),
);
}
Widget _buildGalleryCard(
ThemeData theme, {
required String title,
required MotionXIndicatorShape shape,
required Color color,
required bool useNotch,
}) {
return Card(
elevation: 2,
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12.0),
),
child: MotionXNavigationBar(
index: _comparisonTab,
onTabSelected: (idx) => setState(() => _comparisonTab = idx),
backgroundColor: theme.cardColor,
indicatorStyle: MotionXIndicatorStyle(
shape: shape,
size: 48.0,
color: color,
),
notchStyle: MotionXNotchStyle(enabled: useNotch, depth: 0.5),
items: const [
MotionXItem(label: 'Home', icon: Icon(Icons.home)),
MotionXItem(label: 'Search', icon: Icon(Icons.search)),
MotionXItem(label: 'Profile', icon: Icon(Icons.person)),
],
),
),
],
),
),
);
}
Widget _buildUpdatesScreen(ThemeData theme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.notifications_active_outlined,
size: 64.0,
color: Colors.amber,
),
const SizedBox(height: 16.0),
Text(
'Updates Demo',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
const Text('Dot notification badge indicator.'),
],
),
);
}
Widget _buildSettingsScreen(ThemeData theme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.settings_outlined,
size: 64.0,
color: theme.colorScheme.secondary,
),
const SizedBox(height: 16.0),
Text(
'Settings Demo',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8.0),
const Text('Extensible MotionX configuration studio.'),
],
),
);
}
}
/// CustomPainter demonstrating MotionXIndicatorShape.custom in example app
class _DemoCustomPainter extends CustomPainter {
final Color color;
_DemoCustomPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final path = Path()
..moveTo(size.width / 2, 0)
..lineTo(size.width, size.height / 2)
..lineTo(size.width / 2, size.height)
..lineTo(0, size.height / 2)
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant _DemoCustomPainter oldDelegate) =>
oldDelegate.color != color;
}