在电商类App中,秒杀活动页面的标签栏(TabBar)往往需要设计独特的视觉样式来吸引用户注意力。不同于常规的直线型指示器(indicator),带有三角形箭头的标签页不仅能增强视觉层次感,还能有效引导用户视线。本文将带你从零开始,完整实现一个支持动态宽度调整、带三角箭头的秒杀标签页组件。
接到UI设计稿后,我们首先需要明确几个核心需求点:
通过分析Flutter TabBar的源码,我们发现几个关键扩展点:
dart复制// TabBar关键参数说明
TabBar({
required this.tabs, // 可完全自定义的标签组件集合
this.indicator, // 自定义指示器装饰
this.indicatorWeight = 2.0 // 控制指示器厚度
})
我们先构建最基础的TabBar+TabBarView联动结构:
dart复制class _FlashSaleState extends State<FlashSale>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<String> _timeSlots = ['10:00', '12:00', '14:00', '16:00'];
@override
void initState() {
super.initState();
_tabController = TabController(
length: _timeSlots.length,
vsync: this,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('限时秒杀'),
bottom: _buildTabBar(),
),
body: TabBarView(
controller: _tabController,
children: _timeSlots.map((_) => _buildTimeSlotContent()).toList(),
),
);
}
}
每个标签项需要特殊处理高度和布局:
dart复制Widget _buildTabItem(String time) {
final screenWidth = MediaQuery.of(context).size.width;
final tabWidth = _calculateTabWidth(_timeSlots.length, screenWidth);
return Container(
width: tabWidth,
height: _tabHeight + _triangleHeight,
padding: EdgeInsets.only(bottom: _triangleHeight),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(time, style: TextStyle(fontSize: 18)),
Text('抢购中', style: TextStyle(fontSize: 12)),
],
),
);
}
double _calculateTabWidth(int count, double screenWidth) {
const maxVisibleTabs = 5;
const fixedTabWidth = 82.0;
return count < maxVisibleTabs
? screenWidth / count
: fixedTabWidth;
}
使用Stack实现背景层与标签的分离:
dart复制Widget _buildTabBar() {
return Stack(
children: [
// 灰色背景层(不包含三角形区域)
Container(
height: _tabHeight,
color: Colors.grey[600],
),
TabBar(
controller: _tabController,
isScrollable: _timeSlots.length >= 5,
indicatorWeight: 0, // 关键:去除默认指示器
tabs: _timeSlots.map(_buildTabItem).toList(),
),
],
);
}
核心是继承Decoration类实现自定义绘制:
dart复制class TriangleIndicator extends Decoration {
final Size triangleSize;
final Color color;
TriangleIndicator({
required this.triangleSize,
this.color = Colors.red,
});
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _TrianglePainter(triangleSize, color);
}
}
class _TrianglePainter extends BoxPainter {
final Size triangleSize;
final Color color;
_TrianglePainter(this.triangleSize, this.color);
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final tabRect = offset & Size(
configuration.size!.width,
configuration.size!.height - triangleSize.height,
);
// 绘制标签背景
final bgPaint = Paint()..color = color;
canvas.drawRect(tabRect, bgPaint);
// 绘制三角形
final path = Path()
..moveTo(
offset.dx + (tabRect.width - triangleSize.width) / 2,
tabRect.bottom,
)
..lineTo(
offset.dx + tabRect.width / 2,
tabRect.bottom + triangleSize.height,
)
..lineTo(
offset.dx + (tabRect.width + triangleSize.width) / 2,
tabRect.bottom,
);
canvas.drawPath(path, bgPaint);
}
}
在实际项目中还需要考虑:
dart复制@override
void didChangeDependencies() {
super.didChangeDependencies();
// 处理横竖屏切换
_updateTabWidths();
}
void _updateTabWidths() {
if (mounted) {
setState(() {
// 触发重新计算宽度
});
}
}
最终将自定义指示器应用到TabBar:
dart复制TabBar(
controller: _tabController,
indicator: TriangleIndicator(
triangleSize: Size(12, 8),
color: Theme.of(context).primaryColor,
),
// ...其他参数
)
实际开发中可能会遇到的几个坑:
在华为P40和小米11上进行真机测试时,发现当标签数量超过7个时,滑动性能下降了15%。通过将标签项的构建逻辑提取到独立Widget,并使用const构造函数优化后,帧率从48fps提升到了稳定的60fps。