motionx 0.1.0 copy "motionx: ^0.1.0" to clipboard
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;
}
0
likes
150
points
30
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A modern, highly customizable Flutter bottom navigation bar with floating indicators, smooth animations, badges, PageView synchronization, and extensible APIs.

Repository (GitHub)
View/report issues

Topics

#flutter #navigation #bottom-navigation #animation #ui

License

unknown (license)

Dependencies

flutter

More

Packages that depend on motionx