Skip to content

Commit ca1ea87

Browse files
committed
chore: Add border to badge in bar
1 parent 26499cd commit ca1ea87

2 files changed

Lines changed: 160 additions & 22 deletions

File tree

ouds_core/lib/components/navigation/ouds_navigation_bar_item.dart

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:ouds_core/components/navigation/internal/ouds_navigation_bar_sta
2222
import 'package:ouds_core/components/navigation/internal/ouds_navigation_bar_status_modifier.dart';
2323
import 'package:ouds_theme_contract/ouds_theme.dart';
2424
import 'package:ouds_theme_contract/theme/tokens/components/ouds_bar_tokens.dart';
25+
import 'package:ouds_core/components/utilities/badge_border_utils.dart';
2526

2627
///
2728
/// An OUDS navigation bar item.
@@ -100,23 +101,27 @@ class OudsNavigationBarItem {
100101
excludeFromSemantics: true,
101102
assetName,
102103
fit: BoxFit.contain,
103-
height: 26, //sizeIcon.iconDecorativeExtraSmall,
104-
width: 26, //sizeIcon.iconDecorativeExtraSmall,
104+
height: 26,
105+
width: 26,
105106
colorFilter: ColorFilter.mode(
106107
modifier.getTextIconItemColor(controlState, isSelected),
107108
BlendMode.srcIn,
108109
),
109110
);
110111

111-
return badge != null
112-
? OudsBadge.count(
113-
semanticsLabel: badge.contentDescription,
114-
label: badge.count.toString(),
115-
status: Negative(),
116-
size: badge.hasCount ? OudsBadgeSize.medium : OudsBadgeSize.xsmall,
117-
child: widgetIcon,
118-
)
119-
: widgetIcon;
112+
if (badge == null) return widgetIcon;
113+
114+
return buildBadgeWithBorder(
115+
context: context,
116+
hasCount: badge.hasCount,
117+
child: OudsBadge.count(
118+
semanticsLabel: badge.contentDescription,
119+
label: badge.count.toString(),
120+
status: Negative(),
121+
size: badge.hasCount ? OudsBadgeSize.medium : OudsBadgeSize.xsmall,
122+
child: widgetIcon,
123+
),
124+
);
120125
}
121126

122127
/// Builds the top indicator shown above the icon when the destination is selected.
@@ -256,27 +261,30 @@ class OudsNavigationBarItem {
256261
excludeFromSemantics: true,
257262
assetName,
258263
fit: BoxFit.contain,
259-
height: 26, //sizeIcon.iconDecorativeExtraSmall,
260-
width: 26, //sizeIcon.iconDecorativeExtraSmall,
264+
height: 26,
265+
width: 26,
261266
colorFilter: ColorFilter.mode(
262267
modifier.getTextIconItemColor(controlState, isSelected),
263268
BlendMode.srcIn,
264269
),
265270
);
266271

267-
// Build the children list based on selection state
268272
final children = <Widget>[
269273
_buildTopIndicatorBar(context, bar, isSelected, controlState),
270274
const SizedBox(height: 2),
271275
badge != null
272-
? OudsBadge.count(
273-
semanticsLabel: badge.contentDescription,
274-
label: badge.count.toString(),
275-
status: Negative(),
276-
size: badge.hasCount
277-
? OudsBadgeSize.medium
278-
: OudsBadgeSize.xsmall,
279-
child: widgetIcon,
276+
? buildBadgeWithBorder(
277+
context: context,
278+
hasCount: badge.hasCount,
279+
child: OudsBadge.count(
280+
semanticsLabel: badge.contentDescription,
281+
label: badge.count.toString(),
282+
status: Negative(),
283+
size: badge.hasCount
284+
? OudsBadgeSize.medium
285+
: OudsBadgeSize.xsmall,
286+
child: widgetIcon,
287+
),
280288
)
281289
: widgetIcon,
282290
];
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
/// @nodoc
15+
library;
16+
17+
import 'package:flutter/material.dart';
18+
import 'package:ouds_theme_contract/ouds_theme.dart';
19+
20+
/// Wraps [child] in a [Stack] and draws a 1-px circular border ring around
21+
/// the [OudsBadge] indicator.
22+
///
23+
/// The ring is rendered via [CustomPaint] — the badge itself is never altered.
24+
/// Its position and radius adapt automatically to any icon size or
25+
/// accessibility text-scale factor.
26+
///
27+
/// **Parameters**
28+
///
29+
/// - [context] — resolves badge-size tokens and the border colour from the
30+
/// active OUDS theme.
31+
/// - [child] — the badge widget to wrap (typically an [OudsBadge]).
32+
/// - [hasCount]`true` for a numeric count badge (medium, 16 dp),
33+
/// `false` for a plain dot badge (xsmall, 8 dp).
34+
///
35+
/// **Example — dot badge (xsmall)**
36+
/// ```dart
37+
/// buildBadgeWithBorder(
38+
/// context: context,
39+
/// hasCount: false,
40+
/// child: myBadgeWidget,
41+
/// );
42+
/// ```
43+
///
44+
/// **Example — count badge (medium)**
45+
/// ```dart
46+
/// buildBadgeWithBorder(
47+
/// context: context,
48+
/// hasCount: true,
49+
/// child: myBadgeWidget,
50+
/// );
51+
/// ```
52+
Widget buildBadgeWithBorder({
53+
required BuildContext context,
54+
required Widget child,
55+
required bool hasCount,
56+
}) {
57+
final bar = OudsTheme.of(context).componentsTokens(context).bar;
58+
final badgeTokens = OudsTheme.of(context).componentsTokens(context).badge;
59+
60+
final badgeRadius =
61+
MediaQuery.textScalerOf(
62+
context,
63+
).scale(hasCount ? badgeTokens.sizeMedium : badgeTokens.sizeXsmall) /
64+
2;
65+
66+
return Stack(
67+
clipBehavior: Clip.none,
68+
children: [
69+
child,
70+
Positioned.fill(
71+
child: IgnorePointer(
72+
child: CustomPaint(
73+
painter: _BadgeBorderPainter(
74+
badgeRadius: badgeRadius,
75+
hasCount: hasCount,
76+
borderColor: bar.colorBorderBadge,
77+
),
78+
),
79+
),
80+
),
81+
],
82+
);
83+
}
84+
85+
/// Paints the circular border ring around the badge indicator.
86+
///
87+
/// The ring centre is derived from Flutter Badge's layout algorithm
88+
/// ([_RenderBadge.performLayout]):
89+
///
90+
/// - **Dot** (`!hasCount`): `centre = (width − r, r)`
91+
/// - **Count** (`hasCount`): `centre = (width − r + 4, 4)`
92+
/// where `4` is the LTR effective offset applied to labelled badges.
93+
///
94+
/// The draw radius is set to `badgeRadius + strokeWidth / 2` so that the
95+
/// inner edge of the stroke touches the badge boundary with no gap.
96+
class _BadgeBorderPainter extends CustomPainter {
97+
const _BadgeBorderPainter({
98+
required this.badgeRadius,
99+
required this.hasCount,
100+
required this.borderColor,
101+
});
102+
103+
final double badgeRadius;
104+
final bool hasCount;
105+
final Color borderColor;
106+
107+
@override
108+
void paint(Canvas canvas, Size size) {
109+
final double cx = hasCount
110+
? size.width - badgeRadius + 4
111+
: size.width - badgeRadius;
112+
final double cy = hasCount ? 4.0 : badgeRadius;
113+
114+
const double strokeWidth = 1.0;
115+
canvas.drawCircle(
116+
Offset(cx, cy),
117+
badgeRadius + strokeWidth / 2,
118+
Paint()
119+
..color = borderColor
120+
..style = PaintingStyle.stroke
121+
..strokeWidth = strokeWidth,
122+
);
123+
}
124+
125+
@override
126+
bool shouldRepaint(covariant _BadgeBorderPainter old) =>
127+
old.badgeRadius != badgeRadius ||
128+
old.hasCount != hasCount ||
129+
old.borderColor != borderColor;
130+
}

0 commit comments

Comments
 (0)