animated_notch_topbar 0.1.0 copy "animated_notch_topbar: ^0.1.0" to clipboard
animated_notch_topbar: ^0.1.0 copied to clipboard

A customizable animated top app bar with a sliding notch-style tab indicator and per-tab gradient themes.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:animated_notch_topbar/animated_notch_topbar.dart';

import 'destinations.dart';

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

// Simulates the current API response for this section — see destinations.dart
// for how each entry's tabBackground/tabRowBackground/isComingSoon map onto
// AnimatedNotchTopBar's TopBarTab/TopBarTheme.
const _destinationsApiResponse = '''
{
  "destinations": [
    {
      "_id": "6a9e7663e0e9afe8f60a8044",
      "key": "home",
      "unselectedImage": "assets/coffee_cover.png",
      "selectedImage": "assets/coffee_selected.png",
      "isComingSoon": false,
      "label": "Home",
      "order": 0,
      "tabBackground": {
        "type": "gradient",
        "colors": ["#8FBF9A", "#A9D0AF"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "tabRowBackground": {
        "type": "solid",
        "colors": ["#c28fff"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "pageBackground": {
        "type": "gradient",
        "colors": ["#EFF8E6", "#DDEFCB"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      }
    },
    {
      "_id": "6a9e7663e0e9afe8f60a8045",
      "key": "super_mall",
      "unselectedImage": "assets/mall_cover.png",
      "selectedImage": "assets/mall_selected.png",
      "isComingSoon": false,
      "label": "Super Mall",
      "order": 1,
      "tabBackground": {
        "type": "solid",
        "colors": ["#F1FBF2"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "tabRowBackground": {
        "type": "solid",
        "colors": ["#FFE8C6"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "pageBackground": {
        "type": "solid",
        "colors": ["#F1FBF2"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      }
    },
    {
      "_id": "6a9e7663e0e9afe8f60a8046",
      "key": "off_zone",
      "unselectedImage": "assets/offers_cover.png",
      "selectedImage": "assets/offers_selected.png",
      "isComingSoon": true,
      "label": "%Off Zone",
      "order": 2,
      "posterUrl": "assets/offers_cover.png",
      "tabBackground": {
        "type": "solid",
        "colors": ["#FFFFFF"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "tabRowBackground": {
        "type": "image",
        "colors": ["#EAF4D8"],
        "angle": 180,
        "image": "assets/offers_cover.png",
        "mediaType": "image"
      },
      "pageBackground": {
        "type": "gradient",
        "colors": ["#FFFDF6", "#FBF6E4"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      }
    },
    {
      "_id": "6a9e7663e0e9afe8f60a8047",
      "key": "make_a_print",
      "unselectedImage": "assets/print_cover.png",
      "selectedImage": "assets/print_selected.png",
      "isComingSoon": true,
      "label": "Make a Print",
      "order": 3,
      "posterUrl": "assets/print_cover.png",
      "comingSoonOpacity": 1,
      "tabBackground": {
        "type": "solid",
        "colors": ["#FFFFFF"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "tabRowBackground": {
        "type": "solid",
        "colors": ["#FFFFFF"],
        "angle": 180,
        "image": null,
        "mediaType": "image"
      },
      "pageBackground": {
        "type": "image",
        "colors": ["#FDF6EA"],
        "angle": 180,
        "image": "assets/print_cover.png",
        "mediaType": "image"
      }
    }
  ]
}
''';

// ─────────────────────────────────────────────────────────────────────────────
// Root App
// ─────────────────────────────────────────────────────────────────────────────
class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: _HomePage(),
    );
  }
}

// The bottom sheet in [_showComingSoonPoster] needs a BuildContext below
// MaterialApp (for its Navigator/MaterialLocalizations), so this state
// lives in its own widget nested under MaterialApp rather than in
// ExampleApp's own State.
class _HomePage extends StatefulWidget {
  const _HomePage();

  @override
  State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {
  int _index = 0;

  // ── Sliver-style scroll behaviour ───────────────────────────────────────────
  // A single controller is reused across all tab bodies.
  // We read userScrollDirection from the scroll position — the same signal
  // Flutter's own SliverAppBar uses — so:
  //   • ScrollDirection.reverse  = user scrolling DOWN  → hide header
  //   • ScrollDirection.forward  = user scrolling UP    → show header
  //   • ScrollDirection.idle     = momentum / fling     → do nothing
  // This completely avoids the delta-comparison flickering problem.
  final ScrollController _scrollController = ScrollController();
  bool _showHeader = true;

  void _onScroll() {
    if (!_scrollController.hasClients) return;
    final direction = _scrollController.position.userScrollDirection;
    final offset   = _scrollController.offset;

    if (direction == ScrollDirection.reverse && _showHeader && offset > 8) {
      // User actively scrolling DOWN past 8 px — hide the header.
      setState(() => _showHeader = false);
    } else if (direction == ScrollDirection.forward && !_showHeader) {
      // User actively scrolling UP — show the header immediately.
      setState(() => _showHeader = true);
    }
  }

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  @override
  void dispose() {
    _scrollController.removeListener(_onScroll);
    _scrollController.dispose();
    super.dispose();
  }

  // Parsed once from the (simulated) API response above. Each destination's
  // tabRowBackground/tabBackground/isComingSoon drives the corresponding
  // TopBarTab's theme, notch color, and enabled state — see destinations.dart.
  late final List<Destination> _destinations = Destination.listFromJson(
    jsonDecode(_destinationsApiResponse) as Map<String, dynamic>,
  );

  late final List<TopBarTab> _tabs =
      _destinations.map(destinationToTab).toList();

  // Body content is looked up by destination `key` — the page each
  // destination shows is app-level and independent of the API response.
  // Its background, though, comes from the response's `pageBackground`
  // (see destinations.dart), same as the bar's own colors.
  static const Map<String, Widget> _bodiesByKey = {
    'home': _CoffeeBody(),
    'super_mall': _MallBody(),
    'off_zone': _OffersBody(),
    'make_a_print': _PrintBody(),
  };

  // Tapping a disabled ("coming soon") tab doesn't switch content — it opens
  // a bottom sheet showing that destination's poster image instead.
  void _showComingSoonPoster(int index) {
    final destination = _destinations[index];
    showModalBottomSheet(
      context: context,
      showDragHandle: true,
      builder: (context) => Padding(
        padding: const EdgeInsets.fromLTRB(20, 4, 20, 24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (destination.posterUrl != null)
              ClipRRect(
                borderRadius: BorderRadius.circular(18),
                child: _posterImage(destination.posterUrl!),
              )
            else
              const Icon(Icons.hourglass_top_rounded, size: 48),
            const SizedBox(height: 16),
            Text(
              '${destination.label} is coming soon!',
              style: const TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w700,
                color: Color(0xFF1C1C1C),
              ),
            ),
            const SizedBox(height: 20),
            SizedBox(
              width: double.infinity,
              child: FilledButton(
                style: FilledButton.styleFrom(
                  overlayColor: Colors.transparent,
                ).copyWith(
                  splashFactory: NoSplash.splashFactory,
                ),
                onPressed: () {
                  Navigator.of(context).pop();
                  // TODO: navigate to a details/waitlist page for this
                  // destination.
                },
                child: const Text('Learn more'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _posterImage(String url) {
    if (url.startsWith('http://') || url.startsWith('https://')) {
      return Image.network(url, fit: BoxFit.cover);
    }
    return Image.asset(url, fit: BoxFit.cover);
  }

  @override
  Widget build(BuildContext context) {
    final activeKey = _destinations[_index].key;

    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: _tabs[_index].theme.useDarkForeground
          ? SystemUiOverlayStyle.dark
          : SystemUiOverlayStyle.light,
      child: Scaffold(
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // ── Top App Bar ──────────────────────────────────────────────
            AnimatedNotchTopBar(
              greetingName: '@master_zing_mz',
              locationLabel: 'New York, USA',
              tabs: _tabs,
              validateFourTabs: true,
              onTabChanged: (i) {
                setState(() {
                  _index = i;
                  // Always show header when switching tabs
                  _showHeader = true;
                });
                // Jump scroll back to top on tab switch
                if (_scrollController.hasClients) {
                  _scrollController.jumpTo(0);
                }
              },
              onDisabledTabTap: _showComingSoonPoster,
              borderRadius: 0,
              showHeader: _showHeader,
            ),
            // ── Body ─────────────────────────────────────────────────────
            Expanded(
              child: AnimatedContainer(
                duration: const Duration(milliseconds: 450),
                decoration:
                    _destinations[_index].pageBackground.toBoxDecoration(),
                child: AnimatedSwitcher(
                  duration: const Duration(milliseconds: 300),
                  transitionBuilder: (child, animation) => FadeTransition(
                    opacity: animation,
                    child: child,
                  ),
                  child: KeyedSubtree(
                    key: ValueKey(activeKey),
                    child: _bodiesByKey[activeKey] != null
                        ? _BodyWithController(
                            scrollController: _scrollController,
                            child: _bodiesByKey[activeKey]!,
                          )
                        : const SizedBox.shrink(),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Scroll controller injection
// Wraps a body widget with a PrimaryScrollController so the shared
// ScrollController from _HomePageState is used by all primary ListViews
// inside the body (the default ListView behaviour picks it up automatically).
// ─────────────────────────────────────────────────────────────────────────────
class _BodyWithController extends StatelessWidget {
  final ScrollController scrollController;
  final Widget child;
  const _BodyWithController({
    required this.scrollController,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return PrimaryScrollController(
      controller: scrollController,
      child: child,
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Shared helpers
// ─────────────────────────────────────────────────────────────────────────────
class _SectionTitle extends StatelessWidget {
  final String title;
  final String? action;
  const _SectionTitle({required this.title, this.action});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(
          title,
          style: const TextStyle(
            fontSize: 17,
            fontWeight: FontWeight.w700,
            color: Color(0xFF1C1C1C),
          ),
        ),
        if (action != null)
          Text(
            action!,
            style: const TextStyle(fontSize: 13, color: Color(0xFF9A9488)),
          ),
      ],
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// TAB 0 — Coffee Labs
// ─────────────────────────────────────────────────────────────────────────────
class _CoffeeBody extends StatelessWidget {
  const _CoffeeBody();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 20, 20, 28),
      children: [
        const _SectionTitle(title: 'Categories'),
        const SizedBox(height: 12),
        SizedBox(
          height: 90,
          child: ListView(
            scrollDirection: Axis.horizontal,
            children: const [
              _CoffeeChip(emoji: '☕', label: 'Espresso'),
              _CoffeeChip(emoji: '🍵', label: 'Matcha'),
              _CoffeeChip(emoji: '🥛', label: 'Latte'),
              _CoffeeChip(emoji: '🧋', label: 'Bubble Tea'),
              _CoffeeChip(emoji: '🍫', label: 'Mocha'),
            ],
          ),
        ),
        const SizedBox(height: 20),
        const _SectionTitle(title: "Today's Picks", action: 'See all'),
        const SizedBox(height: 12),
        const _DrinkCard(
          emoji: '☕',
          name: 'Café Americano',
          desc: 'Bold double-shot with hot water',
          price: r'$4.50',
          bg: Color(0xFFEAF4D8),
        ),
        const SizedBox(height: 12),
        const _DrinkCard(
          emoji: '🥛',
          name: 'Caramel Cloud Latte',
          desc: 'Espresso, steamed milk & caramel drizzle',
          price: r'$5.80',
          bg: Color(0xFFFFF3E0),
        ),
        const SizedBox(height: 12),
        const _DrinkCard(
          emoji: '🧋',
          name: 'Taro Bubble Tea',
          desc: 'Creamy taro with golden tapioca pearls',
          price: r'$6.20',
          bg: Color(0xFFEDE8F8),
        ),
        const SizedBox(height: 20),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
          decoration: BoxDecoration(
            gradient: const LinearGradient(
              colors: [Color(0xFF8FBF9A), Color(0xFFA9D0AF)],
            ),
            borderRadius: BorderRadius.circular(18),
          ),
          child: Row(
            children: [
              const Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '☕  Loyalty Card',
                      style: TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w800,
                        color: Colors.white,
                      ),
                    ),
                    SizedBox(height: 4),
                    Text(
                      '3 more cups to a FREE coffee!',
                      style: TextStyle(fontSize: 12, color: Color(0xDDFFFFFF)),
                    ),
                  ],
                ),
              ),
              Row(
                children: List.generate(6, (i) {
                  final filled = i < 3;
                  return Container(
                    width: 18,
                    height: 18,
                    margin: const EdgeInsets.only(left: 5),
                    decoration: BoxDecoration(
                      color: filled ? Colors.white : Colors.white30,
                      shape: BoxShape.circle,
                    ),
                    child: filled
                        ? const Icon(Icons.coffee,
                            size: 10, color: Color(0xFF8FBF9A))
                        : null,
                  );
                }),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _CoffeeChip extends StatelessWidget {
  final String emoji;
  final String label;
  const _CoffeeChip({required this.emoji, required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(right: 12),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(14),
        boxShadow: const [
          BoxShadow(
              color: Color(0x10000000), blurRadius: 8, offset: Offset(0, 2)),
        ],
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(emoji, style: const TextStyle(fontSize: 28)),
          const SizedBox(height: 4),
          Text(
            label,
            style: const TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.w600,
                color: Color(0xFF1C1C1C)),
          ),
        ],
      ),
    );
  }
}

class _DrinkCard extends StatelessWidget {
  final String emoji;
  final String name;
  final String desc;
  final String price;
  final Color bg;
  const _DrinkCard(
      {required this.emoji,
      required this.name,
      required this.desc,
      required this.price,
      required this.bg});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration:
          BoxDecoration(color: bg, borderRadius: BorderRadius.circular(18)),
      child: Row(
        children: [
          Container(
            width: 58,
            height: 58,
            decoration: BoxDecoration(
                color: Colors.white, borderRadius: BorderRadius.circular(14)),
            alignment: Alignment.center,
            child: Text(emoji, style: const TextStyle(fontSize: 28)),
          ),
          const SizedBox(width: 14),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name,
                    style: const TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w700,
                        color: Color(0xFF1C1C1C))),
                const SizedBox(height: 3),
                Text(desc,
                    style: const TextStyle(
                        fontSize: 12, color: Color(0xFF6B665C))),
              ],
            ),
          ),
          const SizedBox(width: 10),
          Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Text(price,
                  style: const TextStyle(
                      fontSize: 15,
                      fontWeight: FontWeight.w800,
                      color: Color(0xFF1C1C1C))),
              const SizedBox(height: 8),
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                decoration: BoxDecoration(
                    color: const Color(0xFF1C1C1C),
                    borderRadius: BorderRadius.circular(8)),
                child: const Text('Add',
                    style: TextStyle(
                        fontSize: 11,
                        fontWeight: FontWeight.w700,
                        color: Colors.white)),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// TAB 1 — Super Mall
// ─────────────────────────────────────────────────────────────────────────────
class _MallBody extends StatelessWidget {
  const _MallBody();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 20, 20, 28),
      children: [
        Container(
          padding: const EdgeInsets.all(18),
          decoration: BoxDecoration(
            gradient: const LinearGradient(
                colors: [Color(0xFF6C5CE0), Color(0xFF9B8DF0)]),
            borderRadius: BorderRadius.circular(18),
          ),
          child: Row(
            children: [
              const Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text('🎉 Weekend Sale!',
                        style: TextStyle(
                            fontSize: 17,
                            fontWeight: FontWeight.w800,
                            color: Colors.white)),
                    SizedBox(height: 4),
                    Text('Up to 40% off selected items',
                        style:
                            TextStyle(fontSize: 12, color: Color(0xDDFFFFFF))),
                  ],
                ),
              ),
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
                decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(10)),
                child: const Text('Shop Now',
                    style: TextStyle(
                        fontSize: 12,
                        fontWeight: FontWeight.w700,
                        color: Color(0xFF6C5CE0))),
              ),
            ],
          ),
        ),
        const SizedBox(height: 20),
        const _SectionTitle(title: 'Categories', action: 'See all'),
        const SizedBox(height: 12),
        GridView.count(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          crossAxisCount: 3,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
          childAspectRatio: 0.9,
          children: const [
            _MallCat(emoji: '👗', label: 'Fashion', bg: Color(0xFFEDE8F8)),
            _MallCat(emoji: '📱', label: 'Electronics', bg: Color(0xFFE8F0FE)),
            _MallCat(emoji: '🏠', label: 'Home', bg: Color(0xFFEAF4D8)),
            _MallCat(emoji: '💄', label: 'Beauty', bg: Color(0xFFFFE8F0)),
            _MallCat(emoji: '🎮', label: 'Gaming', bg: Color(0xFFE8F4FD)),
            _MallCat(emoji: '🛒', label: 'Groceries', bg: Color(0xFFFFF3E0)),
          ],
        ),
        const SizedBox(height: 20),
        const _SectionTitle(title: 'Trending Now', action: 'See all'),
        const SizedBox(height: 12),
        SizedBox(
          height: 170,
          child: ListView(
            scrollDirection: Axis.horizontal,
            children: const [
              _TrendCard(
                  emoji: '📱',
                  name: 'iPhone 16 Pro',
                  price: r'$999',
                  tag: '🔥 Hot'),
              _TrendCard(
                  emoji: '👟',
                  name: 'Air Max 2025',
                  price: r'$189',
                  tag: '⚡ New'),
              _TrendCard(
                  emoji: '🎧',
                  name: 'AirPods Ultra',
                  price: r'$299',
                  tag: '💜 Top Pick'),
              _TrendCard(
                  emoji: '⌚',
                  name: 'Galaxy Watch 7',
                  price: r'$349',
                  tag: '🌟 Popular'),
            ],
          ),
        ),
      ],
    );
  }
}

class _MallCat extends StatelessWidget {
  final String emoji;
  final String label;
  final Color bg;
  const _MallCat({required this.emoji, required this.label, required this.bg});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration:
          BoxDecoration(color: bg, borderRadius: BorderRadius.circular(14)),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(emoji, style: const TextStyle(fontSize: 30)),
          const SizedBox(height: 6),
          Text(label,
              style: const TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w600,
                  color: Color(0xFF1C1C1C))),
        ],
      ),
    );
  }
}

class _TrendCard extends StatelessWidget {
  final String emoji;
  final String name;
  final String price;
  final String tag;
  const _TrendCard(
      {required this.emoji,
      required this.name,
      required this.price,
      required this.tag});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 136,
      margin: const EdgeInsets.only(right: 12),
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(18),
        boxShadow: const [
          BoxShadow(
              color: Color(0x0E000000), blurRadius: 10, offset: Offset(0, 3)),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(emoji, style: const TextStyle(fontSize: 36)),
          const Spacer(),
          Text(tag,
              style: const TextStyle(
                  fontSize: 10,
                  fontWeight: FontWeight.w600,
                  color: Color(0xFF9A9488))),
          const SizedBox(height: 2),
          Text(name,
              style: const TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w700,
                  color: Color(0xFF1C1C1C))),
          const SizedBox(height: 4),
          Text(price,
              style: const TextStyle(
                  fontSize: 15,
                  fontWeight: FontWeight.w800,
                  color: Color(0xFF6C5CE0))),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// TAB 2 — 50% Off Zone
// ─────────────────────────────────────────────────────────────────────────────
class _OffersBody extends StatelessWidget {
  const _OffersBody();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 4, 20, 28),
      children: [
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
          decoration: BoxDecoration(
            color: const Color(0xFFEAF4D8),
            borderRadius: BorderRadius.circular(18),
          ),
          child: const Row(
            children: [
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text('⚡ Flash Sale',
                        style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.w800,
                            color: Color(0xFF2E6B3E))),
                    SizedBox(height: 3),
                    Text('Limited time only!',
                        style:
                            TextStyle(fontSize: 12, color: Color(0xFF5A8A6A))),
                  ],
                ),
              ),
              _TimerBox('02'),
              Padding(
                padding: EdgeInsets.symmetric(horizontal: 4),
                child: Text(':',
                    style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.w900,
                        color: Color(0xFF2E6B3E))),
              ),
              _TimerBox('45'),
              Padding(
                padding: EdgeInsets.symmetric(horizontal: 4),
                child: Text(':',
                    style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.w900,
                        color: Color(0xFF2E6B3E))),
              ),
              _TimerBox('31'),
            ],
          ),
        ),
        const SizedBox(height: 20),
        const _SectionTitle(title: 'Best Deals', action: 'See all'),
        const SizedBox(height: 12),
        const _DealCard(
          emoji: '👟',
          name: 'Nike Air Force 1',
          original: r'$150',
          sale: r'$75',
          discount: '-50%',
          bg: Color(0xFFEAF4D8),
        ),
        const SizedBox(height: 12),
        const _DealCard(
          emoji: '🎧',
          name: 'Sony WH-1000XM5',
          original: r'$400',
          sale: r'$200',
          discount: '-50%',
          bg: Color(0xFFE4F0E4),
        ),
        const SizedBox(height: 12),
        const _DealCard(
          emoji: '💻',
          name: 'MacBook Air M3',
          original: r'$1299',
          sale: r'$649',
          discount: '-50%',
          bg: Color(0xFFDEEEDE),
        ),
        const SizedBox(height: 12),
        const _DealCard(
          emoji: '⌚',
          name: 'Apple Watch S10',
          original: r'$399',
          sale: r'$199',
          discount: '-50%',
          bg: Color(0xFFEAF4D8),
        ),
      ],
    );
  }
}

class _TimerBox extends StatelessWidget {
  final String value;
  const _TimerBox(this.value);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 42,
      height: 42,
      decoration: BoxDecoration(
        color: const Color(0xFF2E6B3E),
        borderRadius: BorderRadius.circular(10),
      ),
      alignment: Alignment.center,
      child: Text(value,
          style: const TextStyle(
              fontSize: 18, fontWeight: FontWeight.w900, color: Colors.white)),
    );
  }
}

class _DealCard extends StatelessWidget {
  final String emoji;
  final String name;
  final String original;
  final String sale;
  final String discount;
  final Color bg;
  const _DealCard(
      {required this.emoji,
      required this.name,
      required this.original,
      required this.sale,
      required this.discount,
      required this.bg});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration:
          BoxDecoration(color: bg, borderRadius: BorderRadius.circular(18)),
      child: Row(
        children: [
          Text(emoji, style: const TextStyle(fontSize: 38)),
          const SizedBox(width: 14),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name,
                    style: const TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w700,
                        color: Color(0xFF1C1C1C))),
                const SizedBox(height: 4),
                Row(
                  children: [
                    Text(original,
                        style: const TextStyle(
                            fontSize: 12,
                            color: Color(0xFF9A9488),
                            decoration: TextDecoration.lineThrough)),
                    const SizedBox(width: 8),
                    Text(sale,
                        style: const TextStyle(
                            fontSize: 15,
                            fontWeight: FontWeight.w800,
                            color: Color(0xFF2E6B3E))),
                  ],
                ),
              ],
            ),
          ),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
            decoration: BoxDecoration(
                color: const Color(0xFF2E6B3E),
                borderRadius: BorderRadius.circular(10)),
            child: Text(discount,
                style: const TextStyle(
                    fontSize: 12,
                    fontWeight: FontWeight.w800,
                    color: Colors.white)),
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// TAB 3 — Make a Print
// ─────────────────────────────────────────────────────────────────────────────
class _PrintBody extends StatelessWidget {
  const _PrintBody();

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.fromLTRB(20, 4, 20, 28),
      children: [
        Container(
          padding: const EdgeInsets.all(22),
          decoration: BoxDecoration(
            gradient: const LinearGradient(
                colors: [Color(0xFFF3DDB0), Color(0xFFECD196)]),
            borderRadius: BorderRadius.circular(18),
          ),
          child: Column(
            children: [
              const Text('🖼️', style: TextStyle(fontSize: 52)),
              const SizedBox(height: 8),
              const Text('Upload Your Photo',
                  style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.w800,
                      color: Color(0xFF5C4A1E))),
              const SizedBox(height: 4),
              const Text('Turn your memories into beautiful prints',
                  textAlign: TextAlign.center,
                  style: TextStyle(fontSize: 13, color: Color(0xFF8A6C2E))),
              const SizedBox(height: 16),
              Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 28, vertical: 12),
                decoration: BoxDecoration(
                    color: const Color(0xFF5C4A1E),
                    borderRadius: BorderRadius.circular(12)),
                child: const Text('+ Upload Now',
                    style: TextStyle(
                        fontSize: 14,
                        fontWeight: FontWeight.w700,
                        color: Colors.white)),
              ),
            ],
          ),
        ),
        const SizedBox(height: 20),
        const _SectionTitle(title: 'Print Types', action: 'See all'),
        const SizedBox(height: 12),
        GridView.count(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          crossAxisCount: 2,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
          childAspectRatio: 2.2,
          children: const [
            _PrintType(emoji: '🖼️', label: 'Photo Print', sub: r'From $1.99'),
            _PrintType(emoji: '🎨', label: 'Canvas', sub: r'From $24.99'),
            _PrintType(emoji: '📋', label: 'Poster', sub: r'From $9.99'),
            _PrintType(emoji: '📔', label: 'Photo Book', sub: r'From $19.99'),
          ],
        ),
        const SizedBox(height: 20),
        const _SectionTitle(title: 'Recent Orders'),
        const SizedBox(height: 12),
        const _OrderItem(
          emoji: '🖼️',
          name: 'Family Photo 4×6',
          date: 'Delivered Sep 3',
          status: '✅ Done',
        ),
        const SizedBox(height: 10),
        const _OrderItem(
          emoji: '🎨',
          name: 'Canvas 12×16',
          date: 'In transit · Sep 8',
          status: '🚚 Shipping',
        ),
        const SizedBox(height: 10),
        const _OrderItem(
          emoji: '📋',
          name: 'Birthday Poster A3',
          date: 'Processing · Sep 8',
          status: '🔄 Processing',
        ),
      ],
    );
  }
}

class _PrintType extends StatelessWidget {
  final String emoji;
  final String label;
  final String sub;
  const _PrintType(
      {required this.emoji, required this.label, required this.sub});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
      decoration: BoxDecoration(
        color: const Color(0xFFFFF8E8),
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: const Color(0xFFECD196)),
      ),
      child: Row(
        children: [
          Text(emoji, style: const TextStyle(fontSize: 28)),
          const SizedBox(width: 10),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(label,
                  style: const TextStyle(
                      fontSize: 13,
                      fontWeight: FontWeight.w700,
                      color: Color(0xFF5C4A1E))),
              const SizedBox(height: 2),
              Text(sub,
                  style:
                      const TextStyle(fontSize: 11, color: Color(0xFF8A6C2E))),
            ],
          ),
        ],
      ),
    );
  }
}

class _OrderItem extends StatelessWidget {
  final String emoji;
  final String name;
  final String date;
  final String status;
  const _OrderItem(
      {required this.emoji,
      required this.name,
      required this.date,
      required this.status});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(14),
        boxShadow: const [
          BoxShadow(
              color: Color(0x0A000000), blurRadius: 8, offset: Offset(0, 2)),
        ],
      ),
      child: Row(
        children: [
          Text(emoji, style: const TextStyle(fontSize: 28)),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(name,
                    style: const TextStyle(
                        fontSize: 13,
                        fontWeight: FontWeight.w700,
                        color: Color(0xFF1C1C1C))),
                const SizedBox(height: 2),
                Text(date,
                    style: const TextStyle(
                        fontSize: 11, color: Color(0xFF8A8A8A))),
              ],
            ),
          ),
          Text(status,
              style:
                  const TextStyle(fontSize: 11, fontWeight: FontWeight.w600)),
        ],
      ),
    );
  }
}
1
likes
160
points
305
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A customizable animated top app bar with a sliding notch-style tab indicator and per-tab gradient themes.

Repository (GitHub)
View/report issues

Topics

#widget #tabbar #navigation #animation

License

MIT (license)

Dependencies

flutter

More

Packages that depend on animated_notch_topbar