| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- 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<Widget> 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),
- );
- }
- }
|