如何创建那样时尚的 AppBar?

How to create stylish AppBar like that?

如何在 Flutter 中像这样在 AppBar 上创建时尚的 Tabs?

我认为这个 AppBar 中棘手的部分是主要导航项的重叠。这可以使用 OverflowBox.

来实现

然后,对于活动导航项的底部曲线,您可以使用 CustomClipper

此示例具有最低交互性,您可以单击主要导航项以切换活动项。

完整源代码

只是 copy-paste 在新的 Flutter 项目的 main.dart 文件中。

import 'package:flutter/material.dart';

// Constants
const kGap = 6.0;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final theme = ThemeData.light();
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Umyt App',
      theme: theme.copyWith(
        colorScheme: theme.colorScheme.copyWith(
          primary: const Color(0xFFED6324),
          secondary: const Color(0xFF611B63),
        ),
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const CustomAppBar(),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: const [
            Text("Hello, Umyt!"),
            Text("I hope this helps you."),
          ],
        ),
      ),
    );
  }
}

class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  const CustomAppBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Theme.of(context).colorScheme.primary,
      child: SafeArea(
        child: Column(
          children: const [
            SizedBox(height: kGap),
            PrimaryNavBar(),
            SizedBox(
                height: kGap *
                    0.96), // Hack without which I have a thin line on the emulator.
            SecondaryNavBar(),
          ],
        ),
      ),
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(106);
}

class PrimaryNavBar extends StatefulWidget {
  const PrimaryNavBar({Key? key}) : super(key: key);

  @override
  State<PrimaryNavBar> createState() => _PrimaryNavBarState();
}

class _PrimaryNavBarState extends State<PrimaryNavBar> {
  String _activeItem = 'market';

  void _toggleActiveItem(String item) {
    setState(() => _activeItem = item);
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const SizedBox(width: kGap),
        PrimaryNavItem(
          onTap: _toggleActiveItem,
          title: 'market',
          activeItem: _activeItem,
        ),
        const SizedBox(width: kGap),
        PrimaryNavItem(
          onTap: _toggleActiveItem,
          title: 'store',
          activeItem: _activeItem,
        ),
      ],
    );
  }
}

class SecondaryNavBar extends StatelessWidget {
  const SecondaryNavBar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      padding:
          const EdgeInsets.symmetric(horizontal: 2 * kGap, vertical: 2 * kGap),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          const SecondaryNavItem(
              icon: Icons.location_on_outlined, text: 'Balkanabat'),
          Container(
            height: 24.0,
            width: 0.5,
            color: Theme.of(context).colorScheme.primary,
          ),
          const SecondaryNavItem(icon: Icons.border_all, text: 'Kategoriýalar'),
        ],
      ),
    );
  }
}

class PrimaryNavItem extends StatelessWidget {
  final String title;
  final String activeItem;
  final void Function(String title)? onTap;

  bool get active => activeItem == title;

  const PrimaryNavItem({
    required this.title,
    required this.activeItem,
    this.onTap,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints(minWidth: 100.0),
      child: InkWell(
        onTap: () => onTap?.call(title),
        child: Container(
          padding:
              const EdgeInsets.symmetric(horizontal: 2 * kGap, vertical: kGap),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(kGap),
          ),
          child: Stack(children: [
            if (active)
              Positioned.fill(
                child: LayoutBuilder(
                  builder: (BuildContext context, BoxConstraints constraints) {
                    return OverflowBox(
                      maxWidth: constraints.maxWidth + 8 * kGap,
                      maxHeight: constraints.maxHeight + 4 * kGap,
                      child: ClipPath(
                          clipper: PrimaryNavItemClipper(),
                          child: Container(color: Colors.white)),
                    );
                  },
                ),
              ),
            Positioned(
              top: 0,
              left: 0,
              child: Image.asset(
                'assets/images/logo.png',
                width: 8 * kGap,
              ),
            ),
            Padding(
              padding: const EdgeInsets.only(top: kGap),
              child: Text(
                title,
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: active
                      ? Theme.of(context).colorScheme.primary
                      : Theme.of(context).colorScheme.secondary,
                ),
              ),
            )
          ]),
        ),
      ),
    );
  }
}

class PrimaryNavItemClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.moveTo(0, size.height);
    path.quadraticBezierTo(
        2 * kGap, size.height, 2 * kGap, size.height - 2 * kGap);
    path.lineTo(size.width - 2 * kGap, size.height - 2 * kGap);
    path.quadraticBezierTo(
        size.width - 2 * kGap, size.height, size.width, size.height);
    path.lineTo(0, size.height);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
}

class SecondaryNavItem extends StatelessWidget {
  final IconData icon;
  final String text;

  const SecondaryNavItem({Key? key, required this.icon, required this.text})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Icon(icon),
        Text(text),
      ],
    );
  }
}

编码愉快!