import 'dart:math'; import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; /// Tab 指示器:过渡中宽度拉伸效果(规范文档 13.2) /// /// 只触发 repaint,不触发 widget rebuild,不会引起嵌套 layout。 /// 使用方式:直接作为 TabBar 的 indicator 属性传入,不需要包 AnimatedBuilder。 class StretchTabIndicator extends Decoration { const StretchTabIndicator({ required this.controller, required this.color, this.height = 2.5, this.borderRadius = 1.25, }); final TabController controller; final Color color; final double height; final double borderRadius; @override BoxPainter createBoxPainter([VoidCallback? onChanged]) => _StretchPainter(this, onChanged); @override bool operator ==(Object other) => other is StretchTabIndicator && other.controller == controller && other.color == color; @override int get hashCode => Object.hash(controller, color); } class _StretchPainter extends BoxPainter { _StretchPainter(this.decoration, VoidCallback? onChanged) : super(onChanged); final StretchTabIndicator decoration; // 无需自己监听 controller:TabBar 内部的 _IndicatorPainter 已经通过 // `super(repaint: controller.animation)` 在每次 offset 变化时触发重绘。 // 我们在 paint() 里直接读取 controller.offset 即可,不需要 addListener。 @override void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) { final stretch = 1.0 + 0.3 * sin(decoration.controller.offset.abs() * pi); final baseW = cfg.size!.width; final stretchW = baseW * stretch; final dx = (stretchW - baseW) / 2; final rect = Rect.fromLTWH( offset.dx - dx, offset.dy + cfg.size!.height - decoration.height, stretchW, decoration.height, ); canvas.drawRRect( RRect.fromRectAndRadius(rect, Radius.circular(decoration.borderRadius)), Paint()..color = decoration.color, ); } } /// 封装好的 AppTabBar,直接使用 StretchTabIndicator,不需要包 AnimatedBuilder。 /// 实现 [PreferredSizeWidget],可作为 [AppBar.bottom] 使用。 class AppTabBar extends StatelessWidget implements PreferredSizeWidget { const AppTabBar({ super.key, required this.controller, required this.tabs, this.isScrollable = false, this.labelColor, this.unselectedLabelColor, this.labelStyle, this.unselectedLabelStyle, this.dividerColor = Colors.transparent, }); final TabController controller; final List tabs; final bool isScrollable; final Color? labelColor; final Color? unselectedLabelColor; final TextStyle? labelStyle; final TextStyle? unselectedLabelStyle; final Color dividerColor; /// 与 Material [TabBar] 默认高度一致(非滚动、Decoration 类指示条) @override Size get preferredSize => const Size.fromHeight(46); @override Widget build(BuildContext context) { return TabBar( controller: controller, tabs: tabs, isScrollable: isScrollable, indicator: StretchTabIndicator( controller: controller, color: AppColors.brand, ), indicatorSize: TabBarIndicatorSize.label, dividerColor: dividerColor, labelColor: labelColor ?? AppColors.brand, unselectedLabelColor: unselectedLabelColor ?? Theme.of(context).colorScheme.onSurface.withAlpha(153), labelStyle: labelStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), unselectedLabelStyle: unselectedLabelStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ); } }