I'm currently working on a custom Navigation Bar which utilizes the CustomPainter class. The static navigation is already done and looks totally fine.
It looks like this: enter image description here
My problem is that I'm trying to apply an animation to it, which doesn't work properly. To be more specific, there is a curve on the Navigation Bar, which should move to the clicked tab. The animation gets forwarded and all but it just does not behave as I like. The curve just doesn't stop where I'd like it to be positioned.
These are the important parts of my code:
custom_nav_bar.dart
class CustomBottomNavigationBar extends StatefulWidget {
const CustomBottomNavigationBar({Key? key}) : super(key: key);
@override
_CustomBottomNavigationBarState createState() => _CustomBottomNavigationBarState();
}
class _CustomBottomNavigationBarState extends State<CustomBottomNavigationBar>
with TickerProviderStateMixin {
final CustomValueNotifier<int> _pageIndex = CustomValueNotifier<int>(1);
late AnimationController _curveController;
late AnimationController _navbarController;
late Animation<double> _navSecondRowAnimation;
late Animation<double> _curveAnimation;
@override
initState() {
super.initState();
_curveController = AnimationController(
duration: durations.kSlidingCurveDuration,
vsync: this,
);
_navbarController = AnimationController(
duration: durations.kSlidingNavbarDuration,
vsync: this,
);
_navSecondRowAnimation = CurvedAnimation(
parent: _navbarController,
curve: Curves.fastLinearToSlowEaseIn,
);
_curveAnimation = Tween(begin: .0, end: 1.0).animate(
CurvedAnimation(parent: _curveController, curve: Curves.ease),
);
// * Used to trigger the elevation of each navbar item when pressed
_pageIndex.addListener(() => setState(() {}));
}
...
RepaintBoundary(
child: CustomPaint(
willChange: true,
painter: NavbarPainter(
tabListenable: _pageIndex,
listenable: _curveController.view,
),
child: SizedBox(
width: context.mediaQuery.size.width,
height: dimens.kNavbarHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <CustomBottomNavigationBarItem>[
CustomBottomNavigationBarItem(
text: context.l10n!.navbarLabelFav.toUpperCase(),
svgIconPath: Assets.lib.src.res.images.icons.nav.heart,
selected: _pageIndex.value == 0,
onTap: () => _setIndex(0),
),
CustomBottomNavigationBarItem(
text: context.l10n!.navbarLabelHome.toUpperCase(),
svgIconPath: Assets.lib.src.res.images.icons.nav.search,
selected: _pageIndex.value == 1,
onTap: () => _setIndex(1),
),
CustomBottomNavigationBarItem(
text: context.l10n!.navbarLabelMore.toUpperCase(),
svgIconPath: Assets.lib.src.res.images.icons.nav.more,
selected: _pageIndex.value == 2,
onTap: () => _setIndex(2),
),
],
),
),
),
),
...
_setIndex(int selectedIndex) {
if (_pageIndex.value == selectedIndex) return;
_pageIndex.value = selectedIndex;
_curveController.reset();
_curveController.forward();
if (_pageIndex.value == 2) {
_openSecondRow();
return;
}
_closeSecondRow();
}
navbar_painter.dart
class NavbarPainter extends CustomPainter {
final CustomValueNotifier<int> tabListenable;
// final Animation<double> _offset;
final Animation<double> listenable;
NavbarPainter({required this.tabListenable, required this.listenable})
// : _offset = Tween(begin: .0, end: 8.0).animate(listenable),
super(repaint: listenable);
...
@override
paint(Canvas canvas, Size size) {
...
_initBezierPointsForCurve(Size size);
...
}
@override
bool shouldRepaint(NavbarPainter oldDelegate) {
print('shouldRepaint() called');
return oldDelegate.listenable.value != listenable.value;
}
...
_initBezierPointsForCurve(Size size) {
// * Only used on first init
if (bezierControlPoints == null || bezierEndPoints == null) {
_initBezierPointsForCenterCurve(size);
return;
}
// * Used after first init
// ? Formula: value * width / 24
// _calcDeltaMultiplier() -> -2, -1, 1, 2 depending on the tab
final curveDeltaDx = _calcDeltaMultiplier() * listenable.value * 8 * size.width / 24;
// ? Always references the prev curve
// * Transform the points gradually with _listenable.value until they reach the new tab
curveStartingPoint = Offset(curveStartingPoint!.dx + curveDeltaDx, .0);
bezierControlPoints?.updateAll(
(key, value) => Offset(value.dx + curveDeltaDx, value.dy),
);
bezierEndPoints?.updateAll(
(key, value) => Offset(value.dx + curveDeltaDx, value.dy),
);
}
// * How many tabs are between the cur & prev
int _calcDeltaMultiplier() => tabListenable.value - (tabListenable.previousValue ?? 0);
...
}
This is what happens, when I click on the right tab: The Curve is outside of the Navigation Bar When I again click the center tab it looks like this: enter image description here
Also the behaviour doesn't seem to be consistent for some reason...
To give more insights into the calculation, here is an example for the calculation of the starting point of the curve:
- 7.0 * size.width / 24 -> starting Point of the center curve
- (7.0 + 8.0) * size.width / 24 -> starting Point of the right curve
- (7.0 - 8.0) * size.width / 24 -> starting Point of the left curve
The calculation for all other points is the same besides that the starting value is different. I am adding/subtracting 8.0 * size.width / 24
instead which is the same since I don't want to remember the starting values. Additionally I'm multiplying it by the animations value [0, 1] to achieve the transition. The method _calcDeltaMultiplier() is used to get the multiplication factor to determine in which direction the curve should move and also how many tabs it should move to the left or right.
Actually all these points work as expected when I am not using an animation. But the User Experience is just better with the animation.
Has anybody an idea on how to solve this? Can't seem to make any progress after countless days of trying.