I uses Container
's color to make indicator-like rather than using TabBar
's indicator as I've to implement some animation to the Container
.
When TabController
index is changing, setState
is called in the listener. Tries scroll/slide on the TabBar
, the TabBar
isn't properly changing the index, as listener
doesn't listen to animation for TabBar
.
I've tried using tabcontroller.animation.addListener
method, but there isn't any workaround for me to control the scroll movement.
Attached video below demonstrates tapping and scroll/slide applied on the TabBar
.
Code:
class TabTest extends StatefulWidget {
@override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController _tabController;
late List<AnimationController> _animationControllers;
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
_animationControllers = List.generate(
4,
(i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
reverseDuration: Duration(milliseconds: 350),
));
}
@override
Widget build(BuildContext context) {
List<IconData> _tabIconData = [
Icons.card_giftcard,
Icons.confirmation_num_outlined,
Icons.emoji_events_outlined,
Icons.wine_bar_outlined,
];
List<String> _tabLabel = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
Widget _tab({
required IconData iconData,
required String label,
required bool isSelectedIndex,
// required double widthAnimation,
// required heightAnimation,
}) {
const _tabTextStyle = TextStyle(
fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.only(bottom: 2.0),
height: 55,
width: double.infinity, //_animContainerWidth - widthAnimation,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelectedIndex ? Colors.black : Colors.transparent,
width: 2.0,
),
),
),
child: Tab(
iconMargin: EdgeInsets.only(bottom: 5.0),
icon: Icon(iconData, color: Colors.black),
child: Text(label, style: _tabTextStyle),
),
);
}
List<Widget> _animationGenerator() {
return List.generate(
4,
(index) => ClipRRect(
child: AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, _) {
final value = _animationControllers[index].value;
final angle = math.sin(value * math.pi * 2) * math.pi * 0.04;
return Transform.rotate(
angle: angle,
child: _tab(
iconData: _tabIconData[index],
label: _tabLabel[index],
isSelectedIndex: _tabController.index == index,
));
}),
),
);
}
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(100),
child: AppBar(
iconTheme: Theme.of(context).iconTheme,
title: Text(
'Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(20),
child: Container(
child: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _animationGenerator(),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.white,
spreadRadius: 5.0,
offset: Offset(0, 3))
],
),
),
),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(
4,
(index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
setState(() {}); // To refresh color for Container bottom Border
_animationControllers[_tabController.previousIndex].reverse();
} else {
_animationControllers[_tabController.index].forward();
}
}
@override
void dispose() {
super.dispose();
_tabController.removeListener(_listener);
}
}
CodePudding user response:
this is a solution with a CustomPaint
widget driven by TabController.animation
:
class TabTest extends StatefulWidget {
@override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController _tabController;
late List<AnimationController> _animationControllers;
@override
void initState() {
super.initState();
// timeDilation = 10;
_tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
_animationControllers = List.generate(4, (i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
));
}
@override
Widget build(BuildContext context) {
List<IconData> _tabIconData = [
Icons.card_giftcard,
Icons.confirmation_num_outlined,
Icons.emoji_events_outlined,
Icons.wine_bar_outlined,
];
List<String> _tabLabel = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
List<Color> _tabColor = [
Color(0xffaa0000),
Color(0xff00aa00),
Color(0xff0000aa),
Colors.black,
];
Widget _tab({
required IconData iconData,
required String label,
required Color color,
required int index,
required Animation<double>? animation,
}) {
const _tabTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return CustomPaint(
painter: TabPainter(
animation: animation!,
index: index,
color: color,
),
child: SizedBox(
width: double.infinity,
child: Tab(
iconMargin: EdgeInsets.only(bottom: 5.0),
icon: Icon(iconData, color: Colors.black),
child: Text(label, style: _tabTextStyle),
),
),
);
}
List<Widget> _animationGenerator() {
return List.generate(
4,
(index) => AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, _) {
final value = _animationControllers[index].value;
final angle = sin(value * pi * 3) * pi * 0.04;
return Transform.rotate(
angle: angle,
child: _tab(
iconData: _tabIconData[index],
label: _tabLabel[index],
color: _tabColor[index],
index: index,
animation: _tabController.animation,
));
}),
);
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text('Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _animationGenerator(),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(4, (index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
_animationControllers[_tabController.previousIndex].value = 0;
} else {
_animationControllers[_tabController.index].forward();
}
}
@override
void dispose() {
super.dispose();
_tabController
..removeListener(_listener)
..dispose();
_animationControllers.forEach((ac) => ac.dispose());
}
}
class TabPainter extends CustomPainter {
final Animation<double> animation;
final int index;
final Color color;
final tabPaint = Paint();
TabPainter({
required this.animation,
required this.index,
required this.color,
});
@override
void paint(ui.Canvas canvas, ui.Size size) {
// timeDilation = 10;
if ((animation.value - index).abs() < 1) {
final rect = Offset.zero & size;
canvas.clipRect(rect);
canvas.translate(size.width * (animation.value - index), 0);
final tabRect = Alignment.bottomCenter.inscribe(Size(size.width, 3), rect);
canvas.drawRect(tabRect, tabPaint..color = color);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}