app_tab_bar.dart 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import 'dart:math';
  2. import 'package:flutter/material.dart';
  3. import '../../../core/theme/app_colors.dart';
  4. /// Tab 指示器:过渡中宽度拉伸效果(规范文档 13.2)
  5. ///
  6. /// 只触发 repaint,不触发 widget rebuild,不会引起嵌套 layout。
  7. /// 使用方式:直接作为 TabBar 的 indicator 属性传入,不需要包 AnimatedBuilder。
  8. class StretchTabIndicator extends Decoration {
  9. const StretchTabIndicator({
  10. required this.controller,
  11. required this.color,
  12. this.height = 2.5,
  13. this.borderRadius = 1.25,
  14. });
  15. final TabController controller;
  16. final Color color;
  17. final double height;
  18. final double borderRadius;
  19. @override
  20. BoxPainter createBoxPainter([VoidCallback? onChanged]) =>
  21. _StretchPainter(this, onChanged);
  22. @override
  23. bool operator ==(Object other) =>
  24. other is StretchTabIndicator &&
  25. other.controller == controller &&
  26. other.color == color;
  27. @override
  28. int get hashCode => Object.hash(controller, color);
  29. }
  30. class _StretchPainter extends BoxPainter {
  31. _StretchPainter(this.decoration, VoidCallback? onChanged) : super(onChanged);
  32. final StretchTabIndicator decoration;
  33. // 无需自己监听 controller:TabBar 内部的 _IndicatorPainter 已经通过
  34. // `super(repaint: controller.animation)` 在每次 offset 变化时触发重绘。
  35. // 我们在 paint() 里直接读取 controller.offset 即可,不需要 addListener。
  36. @override
  37. void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
  38. final stretch =
  39. 1.0 + 0.3 * sin(decoration.controller.offset.abs() * pi);
  40. final baseW = cfg.size!.width;
  41. final stretchW = baseW * stretch;
  42. final dx = (stretchW - baseW) / 2;
  43. final rect = Rect.fromLTWH(
  44. offset.dx - dx,
  45. offset.dy + cfg.size!.height - decoration.height,
  46. stretchW,
  47. decoration.height,
  48. );
  49. canvas.drawRRect(
  50. RRect.fromRectAndRadius(rect, Radius.circular(decoration.borderRadius)),
  51. Paint()..color = decoration.color,
  52. );
  53. }
  54. }
  55. /// 封装好的 AppTabBar,直接使用 StretchTabIndicator,不需要包 AnimatedBuilder。
  56. /// 实现 [PreferredSizeWidget],可作为 [AppBar.bottom] 使用。
  57. class AppTabBar extends StatelessWidget implements PreferredSizeWidget {
  58. const AppTabBar({
  59. super.key,
  60. required this.controller,
  61. required this.tabs,
  62. this.isScrollable = false,
  63. this.labelColor,
  64. this.unselectedLabelColor,
  65. this.labelStyle,
  66. this.unselectedLabelStyle,
  67. this.dividerColor = Colors.transparent,
  68. });
  69. final TabController controller;
  70. final List<Widget> tabs;
  71. final bool isScrollable;
  72. final Color? labelColor;
  73. final Color? unselectedLabelColor;
  74. final TextStyle? labelStyle;
  75. final TextStyle? unselectedLabelStyle;
  76. final Color dividerColor;
  77. /// 与 Material [TabBar] 默认高度一致(非滚动、Decoration 类指示条)
  78. @override
  79. Size get preferredSize => const Size.fromHeight(46);
  80. @override
  81. Widget build(BuildContext context) {
  82. return TabBar(
  83. controller: controller,
  84. tabs: tabs,
  85. isScrollable: isScrollable,
  86. indicator: StretchTabIndicator(
  87. controller: controller,
  88. color: AppColors.brand,
  89. ),
  90. indicatorSize: TabBarIndicatorSize.label,
  91. dividerColor: dividerColor,
  92. labelColor: labelColor ?? AppColors.brand,
  93. unselectedLabelColor: unselectedLabelColor ??
  94. Theme.of(context).colorScheme.onSurface.withAlpha(153),
  95. labelStyle: labelStyle ??
  96. const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
  97. unselectedLabelStyle: unselectedLabelStyle ??
  98. const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
  99. );
  100. }
  101. }