Skip to content

Commit c5cf6cd

Browse files
committed
chore:update Bottom Bar with animation indacator
1 parent 015a16b commit c5cf6cd

4 files changed

Lines changed: 534 additions & 50 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* // Software Name: OUDS Flutter
3+
* // SPDX-FileCopyrightText: Copyright (c) Orange SA
4+
* // SPDX-License-Identifier: MIT
5+
* //
6+
* // This software is distributed under the MIT license,
7+
* // the text of which is available at https://opensource.org/license/MIT/
8+
* // or see the "LICENSE" file for more details.
9+
* //
10+
* // Software description: Flutter library of reusable graphical components
11+
* //
12+
*/
13+
14+
import 'package:flutter/material.dart';
15+
import 'package:ouds_theme_contract/theme/tokens/components/ouds_bar_tokens.dart';
16+
17+
/// A custom painter for drawing an animated navigation bar indicator.
18+
///
19+
/// The indicator expands from the center of the tab outwards to its edges,
20+
/// creating a smooth animation effect when a tab becomes selected.
21+
class _OudsIndicatorPainter extends CustomPainter {
22+
/// The animation value (0.0 to 1.0) controlling the expansion from center.
23+
final double animationValue;
24+
25+
/// The color of the indicator line.
26+
final Color color;
27+
28+
/// The height (thickness) of the indicator line.
29+
final double thickness;
30+
31+
/// The width of the tab (used to determine expansion limits).
32+
final double tabWidth;
33+
34+
/// The border radius of the indicator.
35+
final double borderRadius;
36+
37+
_OudsIndicatorPainter({
38+
required this.animationValue,
39+
required this.color,
40+
required this.thickness,
41+
required this.tabWidth,
42+
required this.borderRadius,
43+
});
44+
45+
@override
46+
void paint(Canvas canvas, Size size) {
47+
// Calculate the expansion: starts from center and expands to edges
48+
final centerX = tabWidth / 2;
49+
final maxExpansion = tabWidth / 2;
50+
final currentExpansion = maxExpansion * animationValue;
51+
52+
// Starting point (left edge) and ending point (right edge) of the indicator
53+
final startX = centerX - currentExpansion;
54+
final endX = centerX + currentExpansion;
55+
final rectWidth = endX - startX;
56+
57+
// Only draw if width is positive
58+
if (rectWidth > 0) {
59+
final rect = Rect.fromLTWH(startX, 0, rectWidth, thickness);
60+
final rrect = RRect.fromRectAndRadius(
61+
rect,
62+
Radius.circular(borderRadius),
63+
);
64+
65+
final paint = Paint()
66+
..color = color
67+
..style = PaintingStyle.fill;
68+
69+
canvas.drawRRect(rrect, paint);
70+
}
71+
}
72+
73+
@override
74+
bool shouldRepaint(covariant _OudsIndicatorPainter oldDelegate) {
75+
return oldDelegate.animationValue != animationValue ||
76+
oldDelegate.color != color ||
77+
oldDelegate.thickness != thickness ||
78+
oldDelegate.tabWidth != tabWidth ||
79+
oldDelegate.borderRadius != borderRadius;
80+
}
81+
}
82+
83+
/// A widget that renders an animated indicator with expansion from center animation.
84+
class OudsAnimatedIndicator extends StatefulWidget {
85+
/// Whether the indicator should be visible and animated.
86+
final bool isSelected;
87+
88+
/// The color of the indicator.
89+
final Color color;
90+
91+
/// The height (thickness) of the indicator line.
92+
final double thickness;
93+
94+
/// The width of the tab containing this indicator.
95+
final double tabWidth;
96+
97+
/// The border radius of the indicator.
98+
final double borderRadius;
99+
100+
/// The duration of the animation.
101+
final Duration animationDuration;
102+
103+
const OudsAnimatedIndicator({
104+
super.key,
105+
required this.isSelected,
106+
required this.color,
107+
required this.thickness,
108+
required this.tabWidth,
109+
required this.borderRadius,
110+
this.animationDuration = const Duration(milliseconds: 300),
111+
});
112+
113+
@override
114+
State<OudsAnimatedIndicator> createState() => _OudsAnimatedIndicatorState();
115+
}
116+
117+
class _OudsAnimatedIndicatorState extends State<OudsAnimatedIndicator>
118+
with SingleTickerProviderStateMixin {
119+
late AnimationController _animationController;
120+
late Animation<double> _animation;
121+
122+
@override
123+
void initState() {
124+
super.initState();
125+
_animationController = AnimationController(
126+
duration: widget.animationDuration,
127+
vsync: this,
128+
);
129+
130+
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
131+
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
132+
);
133+
134+
// Start animation if initially selected
135+
if (widget.isSelected) {
136+
_animationController.forward();
137+
}
138+
}
139+
140+
@override
141+
void didUpdateWidget(covariant OudsAnimatedIndicator oldWidget) {
142+
super.didUpdateWidget(oldWidget);
143+
144+
if (widget.isSelected != oldWidget.isSelected) {
145+
if (widget.isSelected) {
146+
// Forward animation when selecting
147+
_animationController.forward();
148+
} else {
149+
// Reverse animation when deselecting
150+
_animationController.reverse();
151+
}
152+
}
153+
}
154+
155+
@override
156+
void dispose() {
157+
_animationController.dispose();
158+
super.dispose();
159+
}
160+
161+
@override
162+
Widget build(BuildContext context) {
163+
return SizedBox(
164+
height: widget.thickness,
165+
width: widget.tabWidth,
166+
child: AnimatedBuilder(
167+
animation: _animation,
168+
builder: (context, child) {
169+
return CustomPaint(
170+
painter: _OudsIndicatorPainter(
171+
animationValue: _animation.value,
172+
color: widget.color,
173+
thickness: widget.thickness,
174+
tabWidth: widget.tabWidth,
175+
borderRadius: widget.borderRadius,
176+
),
177+
);
178+
},
179+
),
180+
);
181+
}
182+
}
183+
184+
/// Extension to easily access indicator animation properties from bar tokens.
185+
extension OudsBarTokensIndicatorExtension on OudsBarTokens {
186+
/// Gets the animation duration for the indicator.
187+
Duration getIndicatorAnimationDuration() {
188+
return const Duration(milliseconds: 300);
189+
}
190+
}

ouds_core/lib/components/navigation/ouds_navigation_bar_item.dart

Lines changed: 77 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:flutter/material.dart';
1717
import 'package:flutter_svg/flutter_svg.dart';
1818
import 'package:ouds_core/components/badge/ouds_badge.dart';
1919
import 'package:ouds_core/components/common/ouds_icon_status.dart';
20+
import 'package:ouds_core/components/navigation/internal/ouds_navigation_bar_indicator_animation.dart';
2021
import 'package:ouds_core/components/navigation/internal/ouds_navigation_bar_state.dart';
2122
import 'package:ouds_core/components/navigation/internal/ouds_navigation_bar_status_modifier.dart';
2223
import 'package:ouds_theme_contract/ouds_theme.dart';
@@ -102,10 +103,7 @@ class OudsNavigationBarItem {
102103
height: 26, //sizeIcon.iconDecorativeExtraSmall,
103104
width: 26, //sizeIcon.iconDecorativeExtraSmall,
104105
colorFilter: ColorFilter.mode(
105-
modifier.getTextIconItemColor(
106-
controlState,
107-
isSelected,
108-
),
106+
modifier.getTextIconItemColor(controlState, isSelected),
109107
BlendMode.srcIn,
110108
),
111109
);
@@ -122,19 +120,30 @@ class OudsNavigationBarItem {
122120
}
123121

124122
/// Builds the top indicator shown above the icon when the destination is selected.
125-
Container _buildTopIndicatorBar(BuildContext context, OudsBarTokens bar, bool isSelected, OudsNavigationBarControlState controlState) {
126-
final navigationBarStatusModifier = OudsNavigationBarStatusModifier(context);
123+
/// Uses an animated indicator that expands from the center when selected and collapses when deselected.
124+
/// Returns SizedBox.shrink() when not selected to avoid taking space.
125+
Widget _buildTopIndicatorBar(
126+
BuildContext context,
127+
OudsBarTokens bar,
128+
bool isSelected,
129+
OudsNavigationBarControlState controlState,
130+
) {
131+
// Don't show indicator when not selected to avoid taking space
132+
if (!isSelected) {
133+
return SizedBox.shrink();
134+
}
127135

128-
return Container(
129-
height: bar.sizeHeightActiveIndicatorCustom, // thickness of the bar
130-
width: bar.sizeWidthActiveIndicatorCustomTop, // width of the bar (adjust)
131-
decoration: BoxDecoration(
132-
color: isSelected ? navigationBarStatusModifier.getIndicatorBarColor(controlState) : Colors.transparent,
133-
borderRadius: BorderRadius.horizontal(
134-
left: Radius.circular(bar.borderRadiusActiveIndicatorCustomTop),
135-
right: Radius.circular(bar.borderRadiusActiveIndicatorCustomTop),
136-
),
137-
),
136+
final navigationBarStatusModifier = OudsNavigationBarStatusModifier(
137+
context,
138+
);
139+
140+
return OudsAnimatedIndicator(
141+
isSelected: isSelected,
142+
color: navigationBarStatusModifier.getIndicatorBarColor(controlState),
143+
thickness: bar.sizeHeightActiveIndicatorCustom,
144+
tabWidth: bar.sizeWidthActiveIndicatorCustomTop,
145+
borderRadius: bar.borderRadiusActiveIndicatorCustomTop,
146+
animationDuration: const Duration(milliseconds: 300),
138147
);
139148
}
140149

@@ -164,10 +173,24 @@ class OudsNavigationBarItem {
164173
Flexible(
165174
child: NavigationDestination(
166175
label: label,
167-
icon: _buildBadgeIconNavigationDestination(context, icon, modifier, controlState, badge, isSelected: isSelected),
168-
selectedIcon: _buildBadgeIconNavigationDestination(context, icon, modifier, controlState, badge, isSelected: isSelected),
176+
icon: _buildBadgeIconNavigationDestination(
177+
context,
178+
icon,
179+
modifier,
180+
controlState,
181+
badge,
182+
isSelected: isSelected,
183+
),
184+
selectedIcon: _buildBadgeIconNavigationDestination(
185+
context,
186+
icon,
187+
modifier,
188+
controlState,
189+
badge,
190+
isSelected: isSelected,
191+
),
169192
),
170-
)
193+
),
171194
],
172195
);
173196
}
@@ -191,8 +214,22 @@ class OudsNavigationBarItem {
191214

192215
return BottomNavigationBarItem(
193216
label: label,
194-
icon: _buildBadgeIconBottomNavigationBarItem(context, icon, modifier, controlState, badge, isSelected: isSelected),
195-
activeIcon: _buildBadgeIconBottomNavigationBarItem(context, icon, modifier, controlState, badge, isSelected: isSelected),
217+
icon: _buildBadgeIconBottomNavigationBarItem(
218+
context,
219+
icon,
220+
modifier,
221+
controlState,
222+
badge,
223+
isSelected: isSelected,
224+
),
225+
activeIcon: _buildBadgeIconBottomNavigationBarItem(
226+
context,
227+
icon,
228+
modifier,
229+
controlState,
230+
badge,
231+
isSelected: isSelected,
232+
),
196233
);
197234
}
198235

@@ -227,39 +264,29 @@ class OudsNavigationBarItem {
227264
height: 26, //sizeIcon.iconDecorativeExtraSmall,
228265
width: 26, //sizeIcon.iconDecorativeExtraSmall,
229266
colorFilter: ColorFilter.mode(
230-
modifier.getTextIconItemColor(
231-
controlState,
232-
isSelected,
233-
),
267+
modifier.getTextIconItemColor(controlState, isSelected),
234268
BlendMode.srcIn,
235269
),
236270
);
237271

238-
return badge != null
239-
? Column(
240-
children: [
241-
_buildTopIndicatorBar(context, bar, isSelected, controlState),
242-
SizedBox(
243-
height: 2,
244-
),
245-
OudsBadge.count(
246-
semanticsLabel: badge.contentDescription,
247-
label: badge.count.toString(),
248-
status: Negative(),
249-
size: badge.hasCount ? OudsBadgeSize.medium : OudsBadgeSize.xsmall,
250-
child: widgetIcon,
251-
),
252-
],
253-
)
254-
: Column(
255-
children: [
256-
_buildTopIndicatorBar(context, bar, isSelected, controlState),
257-
SizedBox(
258-
height: 2,
259-
),
260-
widgetIcon,
261-
],
262-
);
272+
// Build the children list based on selection state
273+
final children = <Widget>[
274+
_buildTopIndicatorBar(context, bar, isSelected, controlState),
275+
if (isSelected) const SizedBox(height: 2),
276+
badge != null
277+
? OudsBadge.count(
278+
semanticsLabel: badge.contentDescription,
279+
label: badge.count.toString(),
280+
status: Negative(),
281+
size: badge.hasCount
282+
? OudsBadgeSize.medium
283+
: OudsBadgeSize.xsmall,
284+
child: widgetIcon,
285+
)
286+
: widgetIcon,
287+
];
288+
289+
return Column(children: children);
263290
}
264291
}
265292

0 commit comments

Comments
 (0)