Flutter自定义选项卡指示器与设计不匹配

移动开发 2026-07-09

下面是我想实现的效果:

Req. First Image

Req. Second Image

截图来自iPhone 15 Pro:

Output Image

Output Image

我一直在研究如何创建一个自定义容器,我希望构建一个看起来与第一张截图相似的小部件。我正在使用一个Container,配合AnimatedPositioned和 Row来实现相同的设计。

代码:

final mainTabs = ["Text1", "Text2"];
Container(
  height: 40,
  color: AppColors.primaryDark,
  padding: const EdgeInsets.symmetric(horizontal: 5),
  child: LayoutBuilder(
    builder: (context, constraints) {
      final tabWidth = constraints.maxWidth / mainTabs.length;
      return Stack(
        children: [
          AnimatedPositioned(
            duration: const Duration(milliseconds: 250),
            curve: Curves.easeInOut,
            left: tabWidth * mainTab,
            width: tabWidth,
            top: 5,
            bottom: 0,
            child: Container(
              margin: const EdgeInsets.symmetric(horizontal: 2),
              decoration: const BoxDecoration(
                color: AppColors.extraDarkYellow,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(14),
                  topRight: Radius.circular(14),
                ),
              ),
            ),
          ),

          Row(
            children: List.generate(mainTabs.length, (index) {
              final isSelected = mainTab == index;
              return SizedBox(
                width: tabWidth,
                child: GestureDetector(
                  onTap: () {
                    setState(() {
                      mainTab = index;
                      subTab = 0;
                    });
                  },
                  child: Padding(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 10,
                    ),
                    child: Center(
                      child: Text(
                        mainTabs[index],
                        style: AppTextStyles.regular(15).copyWith(
                          height: 1.0,
                          color: isSelected
                              ? Colors.black
                              : Colors.white,
                        ),
                      ),
                    ),
                  ),
                ),
              );
            }),
          ),
        ],
      );
    },
  ),
)

如何实现与第一张截图所示相同的设计?

解决方案

我用一个小型的Flutter示例来尝试复现你的UI,并在选中的标签指示器上使用 CustomPainter,而不仅仅使用 BorderRadius

import 'package:flutter/material.dart';

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

class AppColors {
  static const primaryDark = Color(0xFF1E2A36);
  static const extraDarkYellow = Color(0xFFE0B23C);
}

class AppTextStyles {
  static TextStyle regular(double size) {
    return TextStyle(
      fontSize: size,
      fontWeight: FontWeight.w500,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.white,
        body: Center(
          child: Padding(
            padding: EdgeInsets.all(24),
            child: MainTabs(),
          ),
        ),
      ),
    );
  }
}

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

  @override
  State<MainTabs> createState() => _MainTabsState();
}

class _MainTabsState extends State<MainTabs> {
  int mainTab = 0;

  final mainTabs = ["Text1", "Text2"];

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 44,
      color: AppColors.primaryDark,
      padding: const EdgeInsets.symmetric(horizontal: 5),
      child: LayoutBuilder(
        builder: (context, constraints) {
          final tabWidth = constraints.maxWidth / mainTabs.length;

          return Stack(
            children: [
              AnimatedPositioned(
                duration: const Duration(milliseconds: 250),
                curve: Curves.easeInOut,
                left: tabWidth * mainTab,
                top: 6,
                bottom: 0,
                width: tabWidth,
                child: CustomPaint(
                  painter: SelectedTabPainter(
                    color: AppColors.extraDarkYellow,
                  ),
                ),
              ),
              Row(
                children: List.generate(mainTabs.length, (index) {
                  final isSelected = mainTab == index;

                  return SizedBox(
                    width: tabWidth,
                    height: 44,
                    child: InkWell(
                      onTap: () {
                        setState(() {
                          mainTab = index;
                        });
                      },
                      child: Center(
                        child: Text(
                          mainTabs[index],
                          style: AppTextStyles.regular(15).copyWith(
                            height: 1.0,
                            color: isSelected ? Colors.black : Colors.white,
                          ),
                        ),
                      ),
                    ),
                  );
                }),
              ),
            ],
          );
        },
      ),
    );
  }
}

class SelectedTabPainter extends CustomPainter {
  final Color color;

  SelectedTabPainter({required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;

    const radius = 14.0;

    final path = Path();

    path.moveTo(0, size.height);
    path.lineTo(0, radius);
    path.quadraticBezierTo(0, 0, radius, 0);
    path.lineTo(size.width - radius, 0);
    path.quadraticBezierTo(size.width, 0, size.width, radius);
    path.lineTo(size.width, size.height);
    path.close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant SelectedTabPainter oldDelegate) {
    return oldDelegate.color != color;
  }
}

关键部分是 BoxDecorationBorderRadius.only(topLeft, topRight) 只能创建一个简单的圆角矩形。如果你希望选中的标签与更自定义的设计相匹配,使用 CustomPainter 绘制形状会提供更好的控制。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章