在 Flutter 开发中,TabBarView 和 PageView 是常见的多页面切换组件。然而,默认的 TabBarView 有一些局限性,比如当 Tab 标签长度不一时,指示器长度不好丝滑的动态调整到固定长度;PageView又没有平滑的指示器过渡动画效果。为了克服这些限制,我们可以自定义组件,结合 PageView 来实现更灵活的布局和动画效果。
本文将详细介绍如何通过自定义组件解决这些问题,并实现一个支持动态指示器长度和滑动动画效果的 PageView
1. 问题分析
1.1 TabBar 的局限
在默认情况下,Flutter 的 TabBar 使用了固定长度的指示器,指示器会均匀分布在每个 Tab 下方。如果我们遇到以下场景:
• 标签长度不一致
• 希望指示器与标签的长度相同
• 希望指示器具有动画过渡效果
那么默认的 TabBar 就无法满足需求。因此,我们需要寻找一种更加灵活的解决方案。
1.2 为什么使用 PageView
相比 TabBarView,PageView 允许我们更加灵活地控制每个页面的过渡效果。使用 PageView 可以实现更平滑的页面切换,并且可以手动控制指示器的动画效果。
2. 自定义 CustomCommonTabBar 组件的实现
为了实现自定义指示器的长度和动画效果,我们需要对 TabBar 进行封装,并通过 PageView 实现更灵活的动画控制。我们将创建一个 CustomCommonTabBar 组件,它能够根据标签的实际长度动态调整指示器的宽度,并在标签切换时,添加指示器的平滑过渡动画。
以下是 CustomCommonTabBar 组件的完整实现:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../common/const.dart';
import '../common/res/dimens.dart';
import '../common/res/fontFamily.dart';
typedef ChildWidgetBuilder = Widget Function(
BuildContext context,
int index,
bool isSelected,
);
class CustomCommonTabBar extends StatefulWidget {
final int currentIndex;
final PageController controller;
final ValueChanged<int>? onItemChange;
final List<String> tabs;
final double? tabPadding;
final Widget? suffix;
final double? bottomPadding;
final ChildWidgetBuilder? itemBuilder;
final double? selectedFontSize;
final double? unSelectedFontSize;
const CustomCommonTabBar({
super.key,
required this.currentIndex,
required this.controller,
required this.tabs,
this.tabPadding,
this.onItemChange,
this.suffix,
this.bottomPadding,
this.itemBuilder,
this.selectedFontSize,
this.unSelectedFontSize,
});
@override
State<CustomCommonTabBar> createState() => _CustomCommonTabBarState();
}
class _CustomCommonTabBarState extends State<CustomCommonTabBar> {
int currTabIndex = 0;
double indicatorPosition = 0.0;
double indicatorWidth = 16.0; // 默认下划线宽度
final List<GlobalKey> _tabKeys = [];
@override
void initState() {
super.initState();
currTabIndex = widget.currentIndex;
// 初始化每个 Tab 的 Key
_tabKeys.addAll(List.generate(widget.tabs.length, (_) => GlobalKey()));
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateIndicator();
});
widget.controller.addListener(() {
int index = widget.controller.page?.round() ?? 0;
if (currTabIndex != index) {
currTabIndex = index;
if (widget.onItemChange != null) {
widget.onItemChange!(currTabIndex);
}
setState(() {
_updateIndicator();
});
}
});
}
// 更新下划线的位置和宽度
void _updateIndicator() {
if (_tabKeys[currTabIndex].currentContext != null) {
final RenderBox renderBox = _tabKeys[currTabIndex]
.currentContext!
.findRenderObject() as RenderBox;
final position = renderBox.localToGlobal(Offset.zero);
setState(() {
indicatorPosition =
position.dx + (renderBox.size.width - indicatorWidth) / 2;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.only(bottom: widget.bottomPadding ?? 0.0),
child: Stack(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
AppConfig.horizontal.horizontalSpace,
...List.generate(widget.tabs.length, (index) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
tabItem(name: widget.tabs[index], index: index),
if (widget.tabs.length - 1 != index)
widget.tabPadding == null
? Container(
width: 40.h,
)
: Container(
width: widget.tabPadding!.h,
)
],
);
}).toList(),
Expanded(
child: widget.suffix ?? const SizedBox(),
),
AppConfig.horizontal.horizontalSpace,
],
),
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
left: indicatorPosition,
bottom: 0,
child: Container(
width: indicatorWidth, // 根据每个 Tab 设置的宽度
height: 3.0,
color: Theme.of(context).colorScheme.error,
),
),
],
),
);
}
Widget tabItem({required String name, required int index}) {
bool isSelected = index == currTabIndex;
return InkWell(
key: _tabKeys[index], // 为每个 Tab 设定 Key
onTap: () {
if (currTabIndex != index) {
setState(() {
currTabIndex = index;
_updateIndicator();
if (widget.onItemChange != null) {
widget.onItemChange!(currTabIndex);
}
widget.controller.jumpToPage(currTabIndex);
});
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(
bottom: 8.0,
),
child: Text(
name,
style: TextStyle(
color: isSelected
? Theme.of(context).textTheme.titleLarge!.color!
: Theme.of(context).textTheme.titleSmall!.color!,
fontSize: isSelected
? (widget.selectedFontSize ?? Dimens.font_sp20)
: (widget.unSelectedFontSize ?? Dimens.font_sp18),
fontWeight: isSelected ? Dimens.boldFont : FontWeight.normal,
fontFamily: isSelected ? FontFamily.Medium : FontFamily.Regular,
),
),
),
],
),
);
}
}
3. 如何使用 CustomCommonTabBar
接下来,我们展示如何在页面中使用这个自定义组件,并处理指示器动画。
Widget tab() {
return CustomCommonTabBar(
currentIndex: currentIndex,
controller: PageController(),
tabs: [
'Hong Kong Dollar Remittance',
'Dollar Remittance',
],
onItemChange: (index) {
if (mounted) {
setState(() {
currentIndex = index;
getRemittanceInformation();
});
}
},
);
}
4. 自定义指示器动画效果
在上述组件中,我们通过 AnimatedPositioned 让指示器在页面切换时实现平滑的过渡动画。AnimatedPositioned 是 Flutter 中强大的动画工具,它可以根据属性的变化自动处理过渡动画。
每当 PageController 监听到页面切换时,指示器的宽度和位置都会被重新计算,利用动画来确保过渡效果更加顺滑。
5.总结
通过自定义 CustomCommonTabBar,我们解决了默认 TabBar 指示器长度固定、动画效果不够灵活的问题。使用 PageView 和 GlobalKey,我们可以精确控制指示器的宽度,并实现平滑的过渡动画。