my_animated_widgets 0.0.4 copy "my_animated_widgets: ^0.0.4" to clipboard
my_animated_widgets: ^0.0.4 copied to clipboard

A collection of beautiful, production-ready animated Flutter widgets — Plasma Globe, Orbital Action Menu, Sky Theme Toggle, Liquid Progress Bar, and Dark Mode Toggle. Each widget is fully customisable [...]

example/lib/main.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:my_animated_widgets/my_animated_widgets.dart';

// ─────────────────────────────────────────────────────────────────────────────
//  ENTRY POINT
// ─────────────────────────────────────────────────────────────────────────────

void main() => runApp(const MyApp());

// ─────────────────────────────────────────────────────────────────────────────
//  APP ROOT
// ─────────────────────────────────────────────────────────────────────────────

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isDark = false;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'my_animated_widgets',
      debugShowCheckedModeBanner: false,
      theme: AppTheme.light(),
      darkTheme: AppTheme.dark(),
      themeMode: _isDark ? ThemeMode.dark : ThemeMode.light,
      home: ShowcasePage(
        isDark: _isDark,
        onThemeToggle: (v) => setState(() => _isDark = v),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  THEME
// ─────────────────────────────────────────────────────────────────────────────

class AppTheme {
  static const teal = Color(0xFF01696F);
  static const tealDark = Color(0xFF014E53);
  static const _darkBg = Color(0xFF111210);
  static const _darkSurface = Color(0xFF1B1A18);

  static ThemeData light() => ThemeData(
        useMaterial3: true,
        colorSchemeSeed: teal,
        brightness: Brightness.light,
        scaffoldBackgroundColor: const Color(0xFFF4F3EF),
        cardColor: Colors.white,
      );

  static ThemeData dark() => ThemeData(
        useMaterial3: true,
        colorSchemeSeed: teal,
        brightness: Brightness.dark,
        scaffoldBackgroundColor: _darkBg,
        cardColor: _darkSurface,
      );
}

// ─────────────────────────────────────────────────────────────────────────────
//  SHOWCASE PAGE  —  5 tabs, one per package widget
// ─────────────────────────────────────────────────────────────────────────────

/// Each entry: (emoji, short label, full title, subtitle, widget builder)
typedef _TabDef = ({
  String emoji,
  String label,
  String title,
  String subtitle,
  Widget Function(bool isDark, ValueChanged<bool> onToggle) builder,
});

class ShowcasePage extends StatelessWidget {
  final bool isDark;
  final ValueChanged<bool> onThemeToggle;

  const ShowcasePage({
    super.key,
    required this.isDark,
    required this.onThemeToggle,
  });

  List<_TabDef> get _tabs => [
        (
          emoji: '⚡',
          label: 'Plasma',
          title: 'Plasma Globe',
          subtitle: 'Interactive arcs that follow your touch',
          builder: (_, __) => const _PlasmaTab(),
        ),
        (
          emoji: '🌌',
          label: 'Orbital',
          title: 'Orbital Action Menu',
          subtitle: 'Circular FAB with sparkle orbit rings',
          builder: (_, __) => const _OrbitalTab(),
        ),
        (
          emoji: '🌗',
          label: 'Sky',
          title: 'Sky Theme Toggle',
          subtitle: 'Animated day-to-night sky switch',
          builder: (isDark, onToggle) =>
              _SkyTab(isDark: isDark, onToggle: onToggle),
        ),
        (
          emoji: '⏳',
          label: 'Liquid',
          title: 'Liquid Progress Bar',
          subtitle: 'Wavy liquid-fill animated progress',
          builder: (_, __) => const _LiquidTab(),
        ),
        (
          emoji: '🌙',
          label: 'Toggle',
          title: 'Dark Mode Toggle',
          subtitle: 'Sun/moon animated theme switch',
          builder: (isDark, onToggle) =>
              _DarkModeTab(isDark: isDark, onToggle: onToggle),
        ),
      ];

  @override
  Widget build(BuildContext context) {
    final tabs = _tabs;
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        appBar: _buildAppBar(context, tabs),
        body: TabBarView(
          physics: const BouncingScrollPhysics(),
          children: tabs
              .map((t) => t.builder(isDark, onThemeToggle))
              .toList(),
        ),
      ),
    );
  }

  PreferredSizeWidget _buildAppBar(
      BuildContext context, List<_TabDef> tabs) {
    final cs = Theme.of(context).colorScheme;
    return PreferredSize(
      preferredSize: const Size.fromHeight(120),
      child: Container(
        decoration: BoxDecoration(
          color: cs.surface,
          boxShadow: [
            BoxShadow(
              color: Colors.black.withValues(alpha: 0.06),
              blurRadius: 12,
              offset: const Offset(0, 3),
            ),
          ],
        ),
        child: SafeArea(
          child: Column(
            children: [
              // ── Top row: title + dark toggle ───────
              Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                child: Row(
                  children: [
                    // Package label
                    Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 10, vertical: 4),
                      decoration: BoxDecoration(
                        color:
                            cs.primaryContainer.withValues(alpha: 0.6),
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Text(
                        'pub.dev',
                        style: TextStyle(
                          fontSize: 11,
                          fontWeight: FontWeight.w700,
                          color: cs.primary,
                          letterSpacing: 0.5,
                        ),
                      ),
                    ),
                    const SizedBox(width: 10),
                    Expanded(
                      child: Text(
                        'my_animated_widgets',
                        style: TextStyle(
                          fontSize: 15,
                          fontWeight: FontWeight.w800,
                          color: cs.onSurface,
                          letterSpacing: -0.3,
                        ),
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    DarkModeToggle(
                      isDark: isDark,
                      onChanged: onThemeToggle,
                    ),
                  ],
                ),
              ),

              // ── Tab bar ─────────────────────────────
              TabBar(
                isScrollable: true,
                tabAlignment: TabAlignment.start,
                indicatorColor: cs.primary,
                indicatorWeight: 3,
                dividerColor: Colors.transparent,
                labelStyle: const TextStyle(
                  fontSize: 13,
                  fontWeight: FontWeight.w700,
                ),
                unselectedLabelStyle: const TextStyle(
                  fontSize: 13,
                  fontWeight: FontWeight.w500,
                ),
                tabs: tabs
                    .map((t) => Tab(text: '${t.emoji}  ${t.label}'))
                    .toList(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  SHARED: widget page shell
// ─────────────────────────────────────────────────────────────────────────────

/// Wraps every tab: gradient header card + demo area.
class _WidgetPage extends StatelessWidget {
  final String emoji;
  final String title;
  final String subtitle;
  final String className;
  final List<String> props;
  final Color headerColor;
  final Color headerColorDark;
  final Widget demo;

  const _WidgetPage({
    required this.emoji,
    required this.title,
    required this.subtitle,
    required this.className,
    required this.props,
    required this.headerColor,
    required this.headerColorDark,
    required this.demo,
  });

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    final cs = Theme.of(context).colorScheme;

    return SingleChildScrollView(
      physics: const BouncingScrollPhysics(),
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // ── Header card ──────────────────────────
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(20),
              gradient: LinearGradient(
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
                colors: isDark
                    ? [headerColorDark, headerColorDark.withValues(alpha: 0.4)]
                    : [headerColor, headerColor.withValues(alpha: 0.7)],
              ),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(emoji,
                    style: const TextStyle(fontSize: 36)),
                const SizedBox(height: 8),
                Text(
                  title,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 22,
                    fontWeight: FontWeight.w800,
                    letterSpacing: -0.3,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  subtitle,
                  style: TextStyle(
                    color: Colors.white.withValues(alpha: 0.75),
                    fontSize: 13,
                  ),
                ),
                const SizedBox(height: 16),
                // Class name chip
                Container(
                  padding: const EdgeInsets.symmetric(
                      horizontal: 10, vertical: 5),
                  decoration: BoxDecoration(
                    color: Colors.black.withValues(alpha: 0.25),
                    borderRadius: BorderRadius.circular(8),
                  ),
                  child: Text(
                    className,
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 13,
                      fontFamily: 'monospace',
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                ),
              ],
            ),
          ),

          const SizedBox(height: 20),

          // ── Props label ──────────────────────────
          Text(
            'Key Parameters',
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w700,
              color: cs.primary,
              letterSpacing: 0.2,
            ),
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 6,
            runSpacing: 6,
            children: props
                .map((p) => _PropChip(label: p))
                .toList(),
          ),

          const SizedBox(height: 24),

          // ── Demo label ───────────────────────────
          Text(
            'Live Demo',
            style: TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w700,
              color: cs.primary,
              letterSpacing: 0.2,
            ),
          ),
          const SizedBox(height: 12),

          // ── Demo widget ──────────────────────────
          demo,
        ],
      ),
    );
  }
}

class _PropChip extends StatelessWidget {
  final String label;
  const _PropChip({required this.label});

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
      decoration: BoxDecoration(
        color: cs.primaryContainer.withValues(alpha: 0.5),
        borderRadius: BorderRadius.circular(6),
        border: Border.all(
            color: cs.primary.withValues(alpha: 0.2)),
      ),
      child: Text(
        label,
        style: TextStyle(
          fontSize: 11,
          fontFamily: 'monospace',
          fontWeight: FontWeight.w600,
          color: cs.primary,
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  TAB 1 — PLASMA GLOBE
// ─────────────────────────────────────────────────────────────────────────────

class _PlasmaTab extends StatelessWidget {
  const _PlasmaTab();

  @override
  Widget build(BuildContext context) {
    return _WidgetPage(
      emoji: '⚡',
      title: 'Plasma Globe',
      subtitle: 'Plasma arcs that track your finger in real time',
      className: 'PlasmaGlobeWidget',
      props: const [
        'radius',
        'primaryColor',
        'secondaryColor',
        'arcCount',
      ],
      headerColor: const Color(0xFF6D28D9),
      headerColorDark: const Color(0xFF3B1278),
      demo: const _PlasmaDemo(),
    );
  }
}

class _PlasmaDemo extends StatefulWidget {
  const _PlasmaDemo();

  @override
  State<_PlasmaDemo> createState() => _PlasmaDemoState();
}

class _PlasmaDemoState extends State<_PlasmaDemo> {
  Color _primary = const Color(0xFF8B5CF6);
  Color _secondary = const Color(0xFF06B6D4);
  double _radius = 140;
  int _arcCount = 10;

  static const _presets = [
    ('Violet & Cyan', Color(0xFF8B5CF6), Color(0xFF06B6D4)),
    ('Pink & Orange', Color(0xFFEC4899), Color(0xFFF97316)),
    ('Green & Blue', Color(0xFF10B981), Color(0xFF3B82F6)),
    ('Gold & Red', Color(0xFFD97706), Color(0xFFDC2626)),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Globe demo
        ClipRRect(
          borderRadius: BorderRadius.circular(20),
          child: Container(
            height: 380,
            decoration: const BoxDecoration(
              gradient: RadialGradient(
                center: Alignment(0, 0.2),
                radius: 1.1,
                colors: [Color(0xFF1A0A30), Color(0xFF060618)],
              ),
            ),
            child: Stack(
              children: [
                const _StarFieldBg(),
                Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      PlasmaGlobeWidget(
                        radius: _radius,
                        primaryColor: _primary,
                        secondaryColor: _secondary,
                        arcCount: _arcCount,
                      ),
                      const SizedBox(height: 20),
                      Text(
                        't o u c h  m e',
                        style: TextStyle(
                          color: _primary.withValues(alpha: 0.7),
                          fontSize: 10,
                          letterSpacing: 6,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),

        const SizedBox(height: 20),

        // Controls
        _ControlCard(
          children: [
            _SliderRow(
              label: 'Radius',
              value: _radius,
              min: 80,
              max: 180,
              onChanged: (v) => setState(() => _radius = v),
              display: _radius.round().toString(),
            ),
            _SliderRow(
              label: 'Arc Count',
              value: _arcCount.toDouble(),
              min: 4,
              max: 16,
              divisions: 12,
              onChanged: (v) => setState(() => _arcCount = v.round()),
              display: _arcCount.toString(),
            ),
            const SizedBox(height: 8),
            const Text('Color Preset',
                style: TextStyle(
                    fontSize: 12, fontWeight: FontWeight.w600)),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: _presets.map((p) {
                final active =
                    _primary == p.$2 && _secondary == p.$3;
                return GestureDetector(
                  onTap: () => setState(() {
                        _primary = p.$2;
                        _secondary = p.$3;
                      }),
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 200),
                    padding: const EdgeInsets.symmetric(
                        horizontal: 12, vertical: 6),
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(10),
                      gradient: LinearGradient(
                          colors: [p.$2, p.$3]),
                      boxShadow: active
                          ? [
                              BoxShadow(
                                  color: p.$2.withValues(alpha: 0.4),
                                  blurRadius: 8)
                            ]
                          : [],
                    ),
                    child: Text(
                      p.$1,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 11,
                        fontWeight: FontWeight.w600,
                      ),
                    ),
                  ),
                );
              }).toList(),
            ),
          ],
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  TAB 2 — ORBITAL ACTION MENU
// ─────────────────────────────────────────────────────────────────────────────

class _OrbitalTab extends StatelessWidget {
  const _OrbitalTab();

  @override
  Widget build(BuildContext context) {
    return _WidgetPage(
      emoji: '🌌',
      title: 'Orbital Action Menu',
      subtitle: 'Radial FAB with animated orbit rings and sparkles',
      className: 'OrbitalActionMenu',
      props: const [
        'icon',
        'color',
        'actions',
        'orbitRadius',
        'buttonSize',
      ],
      headerColor: const Color(0xFF4C1D95),
      headerColorDark: const Color(0xFF2D1268),
      demo: const _OrbitalDemo(),
    );
  }
}

class _OrbitalDemo extends StatefulWidget {
  const _OrbitalDemo();

  @override
  State<_OrbitalDemo> createState() => _OrbitalDemoState();
}

class _OrbitalDemoState extends State<_OrbitalDemo> {
  Color _color = const Color(0xFF7C4DFF);
  double _orbitRadius = 110;
  double _buttonSize = 64;

  static const _colorOptions = [
    ('Purple', Color(0xFF7C4DFF)),
    ('Teal', Color(0xFF01696F)),
    ('Pink', Color(0xFFEC4899)),
    ('Amber', Color(0xFFD97706)),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Orbital demo area
        ClipRRect(
          borderRadius: BorderRadius.circular(20),
          child: Container(
            height: 400,
            decoration: const BoxDecoration(
              gradient: RadialGradient(
                center: Alignment.center,
                radius: 1.0,
                colors: [Color(0xFF1A1A2E), Color(0xFF0D0D14)],
              ),
            ),
            child: Center(
              child: OrbitalActionMenu(
                icon: Icons.auto_awesome,
                color: _color,
                orbitRadius: _orbitRadius,
                buttonSize: _buttonSize,
                actions: [
                  OrbitalAction(
                    icon: Icons.receipt_long_rounded,
                    label: 'Invoice',
                    color: const Color(0xFF00BCD4),
                    onTap: () {},
                  ),
                  OrbitalAction(
                    icon: Icons.person_add_rounded,
                    label: 'Client',
                    color: const Color(0xFF69F0AE),
                    onTap: () {},
                  ),
                  OrbitalAction(
                    icon: Icons.qr_code_scanner_rounded,
                    label: 'Scan',
                    color: const Color(0xFFFFD740),
                    onTap: () {},
                  ),
                  OrbitalAction(
                    icon: Icons.bar_chart_rounded,
                    label: 'Reports',
                    color: const Color(0xFFFF6D9F),
                    onTap: () {},
                  ),
                ],
              ),
            ),
          ),
        ),

        const SizedBox(height: 20),

        // Controls
        _ControlCard(
          children: [
            _SliderRow(
              label: 'Orbit Radius',
              value: _orbitRadius,
              min: 70,
              max: 150,
              onChanged: (v) => setState(() => _orbitRadius = v),
              display: _orbitRadius.round().toString(),
            ),
            _SliderRow(
              label: 'Button Size',
              value: _buttonSize,
              min: 44,
              max: 88,
              onChanged: (v) => setState(() => _buttonSize = v),
              display: _buttonSize.round().toString(),
            ),
            const SizedBox(height: 8),
            const Text('Accent Color',
                style: TextStyle(
                    fontSize: 12, fontWeight: FontWeight.w600)),
            const SizedBox(height: 8),
            Row(
              children: _colorOptions.map((opt) {
                final active = _color == opt.$2;
                return Padding(
                  padding: const EdgeInsets.only(right: 8),
                  child: GestureDetector(
                    onTap: () =>
                        setState(() => _color = opt.$2),
                    child: AnimatedContainer(
                      duration: const Duration(milliseconds: 200),
                      width: 36,
                      height: 36,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: opt.$2,
                        border: Border.all(
                          color: active
                              ? Colors.white
                              : Colors.transparent,
                          width: 2.5,
                        ),
                        boxShadow: active
                            ? [
                                BoxShadow(
                                  color: opt.$2.withValues(alpha: 0.5),
                                  blurRadius: 10,
                                )
                              ]
                            : [],
                      ),
                    ),
                  ),
                );
              }).toList(),
            ),
          ],
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  TAB 3 — SKY THEME TOGGLE
// ─────────────────────────────────────────────────────────────────────────────

class _SkyTab extends StatelessWidget {
  final bool isDark;
  final ValueChanged<bool> onToggle;
  const _SkyTab({required this.isDark, required this.onToggle});

  @override
  Widget build(BuildContext context) {
    return _WidgetPage(
      emoji: '🌗',
      title: 'Sky Theme Toggle',
      subtitle: 'Day-to-night sky transition with shooting stars',
      className: 'SkyThemeToggle',
      props: const ['isDark', 'onChanged', 'width'],
      headerColor: isDark ? const Color(0xFF1A1A4E) : const Color(0xFF0284C7),
      headerColorDark:
          isDark ? const Color(0xFF0D0D2A) : const Color(0xFF014E53),
      demo: _SkyDemo(isDark: isDark, onToggle: onToggle),
    );
  }
}

class _SkyDemo extends StatefulWidget {
  final bool isDark;
  final ValueChanged<bool> onToggle;
  const _SkyDemo({required this.isDark, required this.onToggle});

  @override
  State<_SkyDemo> createState() => _SkyDemoState();
}

class _SkyDemoState extends State<_SkyDemo> {
  double _width = 220;

  @override
  Widget build(BuildContext context) {
    final isDark = widget.isDark;
    return Column(
      children: [
        // Sky ambient background card
        AnimatedContainer(
          duration: const Duration(milliseconds: 700),
          curve: Curves.easeInOut,
          width: double.infinity,
          padding: const EdgeInsets.symmetric(vertical: 48),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20),
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: isDark
                  ? [const Color(0xFF060618), const Color(0xFF0A0A1A)]
                  : [const Color(0xFF5BBEE8), const Color(0xFFB0E0FF)],
            ),
          ),
          child: Column(
            children: [
              AnimatedSwitcher(
                duration: const Duration(milliseconds: 500),
                child: Text(
                  isDark ? '🌙 Good Night' : '☀️ Good Morning',
                  key: ValueKey(isDark),
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.w700,
                    color:
                        isDark ? Colors.white70 : const Color(0xFF1A3A4A),
                  ),
                ),
              ),
              const SizedBox(height: 32),
              SkyThemeToggle(
                isDark: isDark,
                onChanged: widget.onToggle,
                width: _width,
              ),
              const SizedBox(height: 32),
              AnimatedDefaultTextStyle(
                duration: const Duration(milliseconds: 400),
                style: TextStyle(
                  fontSize: 12,
                  color:
                      isDark ? Colors.white30 : const Color(0xFF3A6A7A),
                  letterSpacing: 1,
                ),
                child: Text(
                  isDark ? 'NIGHT MODE ACTIVE' : 'DAY MODE ACTIVE',
                ),
              ),
            ],
          ),
        ),

        const SizedBox(height: 20),

        _ControlCard(
          children: [
            _SliderRow(
              label: 'Width',
              value: _width,
              min: 140,
              max: 300,
              onChanged: (v) => setState(() => _width = v),
              display: _width.round().toString(),
            ),
          ],
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  TAB 4 — LIQUID PROGRESS BAR
// ─────────────────────────────────────────────────────────────────────────────

class _LiquidTab extends StatelessWidget {
  const _LiquidTab();

  @override
  Widget build(BuildContext context) {
    return _WidgetPage(
      emoji: '⏳',
      title: 'Liquid Progress Bar',
      subtitle: 'Animated wavy liquid fill with percentage label',
      className: 'LiquidProgressBar',
      props: const ['progress', 'color', 'height'],
      headerColor: const Color(0xFF01696F),
      headerColorDark: const Color(0xFF014E53),
      demo: const _LiquidDemo(),
    );
  }
}

class _LiquidDemo extends StatefulWidget {
  const _LiquidDemo();

  @override
  State<_LiquidDemo> createState() => _LiquidDemoState();
}

class _LiquidDemoState extends State<_LiquidDemo> {
  double _progress = 0.6;
  double _height = 28;

  static const _bars = [
    ('Monthly Target', Color(0xFF01696F)),
    ('Weekly Target', Color(0xFF964219)),
    ('Daily Goal', Color(0xFF4C1D95)),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _ControlCard(
          children: [
            // Live bars
            ...List.generate(_bars.length, (i) {
              final factor = (1 - i * 0.2).clamp(0.0, 1.0);
              return Padding(
                padding: const EdgeInsets.only(bottom: 16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment:
                          MainAxisAlignment.spaceBetween,
                      children: [
                        Text(_bars[i].$1,
                            style: const TextStyle(
                                fontSize: 12,
                                fontWeight: FontWeight.w600)),
                        Text(
                          '${(_progress * factor * 100).round()}%',
                          style: TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.bold,
                            color: _bars[i].$2,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    LiquidProgressBar(
                      progress: _progress * factor,
                      color: _bars[i].$2,
                      height: _height,
                    ),
                  ],
                ),
              );
            }),

            const Divider(height: 24),

            // Controls
            _SliderRow(
              label: 'Progress',
              value: _progress,
              min: 0,
              max: 1,
              onChanged: (v) => setState(() => _progress = v),
              display: '${(_progress * 100).round()}%',
            ),
            _SliderRow(
              label: 'Height',
              value: _height,
              min: 16,
              max: 48,
              onChanged: (v) => setState(() => _height = v),
              display: _height.round().toString(),
            ),
          ],
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  TAB 5 — DARK MODE TOGGLE
// ─────────────────────────────────────────────────────────────────────────────

class _DarkModeTab extends StatelessWidget {
  final bool isDark;
  final ValueChanged<bool> onToggle;
  const _DarkModeTab({required this.isDark, required this.onToggle});

  @override
  Widget build(BuildContext context) {
    return _WidgetPage(
      emoji: '🌙',
      title: 'Dark Mode Toggle',
      subtitle: 'Elastic sun/moon animated switch',
      className: 'DarkModeToggle',
      props: const ['isDark', 'onChanged', 'animationDuration'],
      headerColor: isDark ? const Color(0xFF1E1B4B) : const Color(0xFF374151),
      headerColorDark: const Color(0xFF111827),
      demo: _DarkModeDemo(isDark: isDark, onToggle: onToggle),
    );
  }
}

class _DarkModeDemo extends StatefulWidget {
  final bool isDark;
  final ValueChanged<bool> onToggle;
  const _DarkModeDemo({required this.isDark, required this.onToggle});

  @override
  State<_DarkModeDemo> createState() => _DarkModeDemoState();
}

class _DarkModeDemoState extends State<_DarkModeDemo> {
  Duration _duration = const Duration(milliseconds: 400);

  static const _durations = [
    ('Snappy', Duration(milliseconds: 200)),
    ('Normal', Duration(milliseconds: 400)),
    ('Slow', Duration(milliseconds: 900)),
  ];

  @override
  Widget build(BuildContext context) {
    final isDark = widget.isDark;
    return Column(
      children: [
        // Demo card
        AnimatedContainer(
          duration: const Duration(milliseconds: 600),
          width: double.infinity,
          padding: const EdgeInsets.symmetric(vertical: 40),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20),
            color: isDark
                ? const Color(0xFF111827)
                : const Color(0xFFF9FAFB),
            border: Border.all(
              color: isDark
                  ? Colors.white.withValues(alpha: 0.07)
                  : Colors.black.withValues(alpha: 0.07),
            ),
          ),
          child: Column(
            children: [
              AnimatedSwitcher(
                duration: const Duration(milliseconds: 400),
                child: Text(
                  isDark ? '🌙  Dark Mode' : '☀️  Light Mode',
                  key: ValueKey(isDark),
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.w700,
                    color: isDark ? Colors.white : Colors.black87,
                  ),
                ),
              ),
              const SizedBox(height: 28),
              DarkModeToggle(
                isDark: isDark,
                onChanged: widget.onToggle,
                animationDuration: _duration,
              ),
              const SizedBox(height: 16),
              Text(
                isDark ? 'Tap to switch to Light' : 'Tap to switch to Dark',
                style: TextStyle(
                  fontSize: 12,
                  color: isDark ? Colors.white38 : Colors.black38,
                ),
              ),
            ],
          ),
        ),

        const SizedBox(height: 20),

        // Controls
        _ControlCard(
          children: [
            const Text(
              'Animation Speed',
              style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
            ),
            const SizedBox(height: 10),
            Row(
              children: _durations.map((d) {
                final active = _duration == d.$2;
                final cs = Theme.of(context).colorScheme;
                return Padding(
                  padding: const EdgeInsets.only(right: 8),
                  child: GestureDetector(
                    onTap: () =>
                        setState(() => _duration = d.$2),
                    child: AnimatedContainer(
                      duration:
                          const Duration(milliseconds: 200),
                      padding: const EdgeInsets.symmetric(
                          horizontal: 14, vertical: 8),
                      decoration: BoxDecoration(
                        color: active
                            ? cs.primary
                            : cs.primaryContainer
                                .withValues(alpha: 0.4),
                        borderRadius:
                            BorderRadius.circular(10),
                      ),
                      child: Text(
                        d.$1,
                        style: TextStyle(
                          fontSize: 12,
                          fontWeight: FontWeight.w600,
                          color: active
                              ? Colors.white
                              : cs.primary,
                        ),
                      ),
                    ),
                  ),
                );
              }).toList(),
            ),
          ],
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  SHARED CONTROL WIDGETS
// ─────────────────────────────────────────────────────────────────────────────

class _ControlCard extends StatelessWidget {
  final List<Widget> children;
  const _ControlCard({required this.children});

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Theme.of(context).cardColor,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: isDark
              ? Colors.white.withValues(alpha: 0.06)
              : Colors.black.withValues(alpha: 0.05),
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: isDark ? 0.3 : 0.06),
            blurRadius: 14,
            offset: const Offset(0, 5),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: children,
      ),
    );
  }
}

class _SliderRow extends StatelessWidget {
  final String label;
  final double value;
  final double min;
  final double max;
  final int? divisions;
  final ValueChanged<double> onChanged;
  final String display;

  const _SliderRow({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.onChanged,
    required this.display,
    this.divisions,
  });

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(label,
                style: const TextStyle(
                    fontSize: 12, fontWeight: FontWeight.w600)),
            Container(
              padding: const EdgeInsets.symmetric(
                  horizontal: 8, vertical: 2),
              decoration: BoxDecoration(
                color: cs.primaryContainer.withValues(alpha: 0.5),
                borderRadius: BorderRadius.circular(6),
              ),
              child: Text(
                display,
                style: TextStyle(
                  fontSize: 11,
                  fontWeight: FontWeight.w700,
                  color: cs.primary,
                ),
              ),
            ),
          ],
        ),
        SliderTheme(
          data: SliderTheme.of(context).copyWith(
            activeTrackColor: cs.primary,
            thumbColor: cs.primary,
            overlayColor: cs.primary.withValues(alpha: 0.12),
            inactiveTrackColor: cs.primary.withValues(alpha: 0.18),
            trackHeight: 3,
          ),
          child: Slider(
            value: value,
            min: min,
            max: max,
            divisions: divisions,
            onChanged: onChanged,
          ),
        ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
//  STAR FIELD BACKGROUND (for Plasma tab)
// ─────────────────────────────────────────────────────────────────────────────

class _StarFieldBg extends StatelessWidget {
  const _StarFieldBg();

  @override
  Widget build(BuildContext context) {
    return SizedBox.expand(
      child: CustomPaint(painter: _StarFieldPainter(Random(99))),
    );
  }
}

class _StarFieldPainter extends CustomPainter {
  final Random rng;
  _StarFieldPainter(this.rng);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.white.withValues(alpha: 0.3);
    for (int i = 0; i < 55; i++) {
      canvas.drawCircle(
        Offset(rng.nextDouble() * size.width,
            rng.nextDouble() * size.height),
        rng.nextDouble() * 1.1 + 0.3,
        paint,
      );
    }
  }

  @override
  bool shouldRepaint(_StarFieldPainter old) => false;
}
0
likes
145
points
15
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A collection of beautiful, production-ready animated Flutter widgets — Plasma Globe, Orbital Action Menu, Sky Theme Toggle, Liquid Progress Bar, and Dark Mode Toggle. Each widget is fully customisable, dark-mode aware, and Material 3 compatible.

Repository (GitHub)
View/report issues

Topics

#animation #widget #dark-mode #ui #material

License

MIT (license)

Dependencies

flutter

More

Packages that depend on my_animated_widgets