1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'icon_button.dart';
6/// @docImport 'navigation_rail.dart';
7/// @docImport 'text_button.dart';
8/// @docImport 'text_theme.dart';
9library;
10
11import 'dart:math' as math;
12
13import 'package:flutter/rendering.dart';
14import 'package:flutter/widgets.dart';
15
16import 'badge_theme.dart';
17import 'color_scheme.dart';
18import 'theme.dart';
19
20/// A Material Design "badge".
21///
22/// A badge's [label] conveys a small amount of information about its
23/// [child], like a count or status. If the label is null then this is
24/// a "small" badge that's displayed as a [smallSize] diameter filled
25/// circle. Otherwise this is a [StadiumBorder] shaped "large" badge
26/// with height [largeSize].
27///
28/// Badges are typically used to decorate the icon within a
29/// [BottomNavigationBarItem] or a [NavigationRailDestination]
30/// or a button's icon, as in [TextButton.icon]. The badge's default
31/// configuration is intended to work well with a default sized (24)
32/// [Icon].
33///
34/// {@tool dartpad}
35/// This example shows how to create a [Badge] with label and count
36/// wrapped on an icon in an [IconButton].
37///
38/// ** See code in examples/api/lib/material/badge/badge.0.dart **
39/// {@end-tool}
40class Badge extends StatelessWidget {
41 /// Create a Badge that stacks [label] on top of [child].
42 ///
43 /// If [label] is null then just a filled circle is displayed. Otherwise
44 /// the [label] is displayed within a [StadiumBorder] shaped area.
45 const Badge({
46 super.key,
47 this.backgroundColor,
48 this.textColor,
49 this.smallSize,
50 this.largeSize,
51 this.textStyle,
52 this.padding,
53 this.alignment,
54 this.offset,
55 this.label,
56 this.isLabelVisible = true,
57 this.child,
58 });
59
60 /// Convenience constructor for creating a badge with a numeric
61 /// label with 1-3 digits based on [count].
62 ///
63 /// Initializes [label] with a [Text] widget that contains [count].
64 /// If [count] is greater than 999, then the label is '999+'.
65 Badge.count({
66 super.key,
67 this.backgroundColor,
68 this.textColor,
69 this.smallSize,
70 this.largeSize,
71 this.textStyle,
72 this.padding,
73 this.alignment,
74 this.offset,
75 required int count,
76 this.isLabelVisible = true,
77 this.child,
78 }) : label = Text(count > 999 ? '999+' : '$count');
79
80 /// The badge's fill color.
81 ///
82 /// Defaults to the [BadgeTheme]'s background color, or
83 /// [ColorScheme.error] if the theme value is null.
84 final Color? backgroundColor;
85
86 /// The color of the badge's [label] text.
87 ///
88 /// This color overrides the color of the label's [textStyle].
89 ///
90 /// Defaults to the [BadgeTheme]'s foreground color, or
91 /// [ColorScheme.onError] if the theme value is null.
92 final Color? textColor;
93
94 /// The diameter of the badge if [label] is null.
95 ///
96 /// Defaults to the [BadgeTheme]'s small size, or 6 if the theme value
97 /// is null.
98 final double? smallSize;
99
100 /// The badge's height if [label] is non-null.
101 ///
102 /// Defaults to the [BadgeTheme]'s large size, or 16 if the theme value
103 /// is null. If the default value is overridden then it may be useful to
104 /// also override [padding] and [alignment].
105 final double? largeSize;
106
107 /// The [DefaultTextStyle] for the badge's label.
108 ///
109 /// The text style's color is overwritten by the [textColor].
110 ///
111 /// This value is only used if [label] is non-null.
112 ///
113 /// Defaults to the [BadgeTheme]'s text style, or the overall theme's
114 /// [TextTheme.labelSmall] if the badge theme's value is null. If
115 /// the default text style is overridden then it may be useful to
116 /// also override [largeSize], [padding], and [alignment].
117 final TextStyle? textStyle;
118
119 /// The padding added to the badge's label.
120 ///
121 /// This value is only used if [label] is non-null.
122 ///
123 /// Defaults to the [BadgeTheme]'s padding, or 4 pixels on the
124 /// left and right if the theme's value is null.
125 final EdgeInsetsGeometry? padding;
126
127 /// Combined with [offset] to determine the location of the [label]
128 /// relative to the [child].
129 ///
130 /// The alignment positions the label in the same way a child of an
131 /// [Align] widget is positioned, except that, the alignment is
132 /// resolved as if the label was a [largeSize] square and [offset]
133 /// is added to the result.
134 ///
135 /// This value is only used if [label] is non-null.
136 ///
137 /// Defaults to the [BadgeTheme]'s alignment, or
138 /// [AlignmentDirectional.topEnd] if the theme's value is null.
139 final AlignmentGeometry? alignment;
140
141 /// Combined with [alignment] to determine the location of the [label]
142 /// relative to the [child].
143 ///
144 /// This value is only used if [label] is non-null.
145 ///
146 /// Defaults to the [BadgeTheme]'s offset, or
147 /// if the theme's value is null then `Offset(4, -4)` for
148 /// [TextDirection.ltr] or `Offset(-4, -4)` for [TextDirection.rtl].
149 final Offset? offset;
150
151 /// The badge's content, typically a [Text] widget that contains 1 to 4
152 /// characters.
153 ///
154 /// If the label is null then this is a "small" badge that's
155 /// displayed as a [smallSize] diameter filled circle. Otherwise
156 /// this is a [StadiumBorder] shaped "large" badge with height [largeSize].
157 final Widget? label;
158
159 /// If false, the badge's [label] is not included.
160 ///
161 /// This flag is true by default. It's intended to make it convenient
162 /// to create a badge that's only shown under certain conditions.
163 final bool isLabelVisible;
164
165 /// The widget that the badge is stacked on top of.
166 ///
167 /// Typically this is an default sized [Icon] that's part of a
168 /// [BottomNavigationBarItem] or a [NavigationRailDestination].
169 final Widget? child;
170
171 @override
172 Widget build(BuildContext context) {
173 if (!isLabelVisible) {
174 return child ?? const SizedBox();
175 }
176
177 final BadgeThemeData badgeTheme = BadgeTheme.of(context);
178 final BadgeThemeData defaults = _BadgeDefaultsM3(context);
179 final Decoration effectiveDecoration = ShapeDecoration(
180 color: backgroundColor ?? badgeTheme.backgroundColor ?? defaults.backgroundColor!,
181 shape: const StadiumBorder(),
182 );
183 final double effectiveWidthOffset;
184 final Widget badge;
185 final bool hasLabel = label != null;
186 if (hasLabel) {
187 final double minSize = effectiveWidthOffset = largeSize ?? badgeTheme.largeSize ?? defaults.largeSize!;
188 badge = DefaultTextStyle(
189 style: (textStyle ?? badgeTheme.textStyle ?? defaults.textStyle!).copyWith(
190 color: textColor ?? badgeTheme.textColor ?? defaults.textColor!,
191 ),
192 child: _IntrinsicHorizontalStadium(
193 minSize: minSize,
194 child: Container(
195 clipBehavior: Clip.antiAlias,
196 decoration: effectiveDecoration,
197 padding: padding ?? badgeTheme.padding ?? defaults.padding!,
198 alignment: Alignment.center,
199 child: label,
200 ),
201 ),
202 );
203 } else {
204 final double effectiveSmallSize = effectiveWidthOffset = smallSize ?? badgeTheme.smallSize ?? defaults.smallSize!;
205 badge = Container(
206 width: effectiveSmallSize,
207 height: effectiveSmallSize,
208 clipBehavior: Clip.antiAlias,
209 decoration: effectiveDecoration,
210 );
211 }
212
213 if (child == null) {
214 return badge;
215 }
216
217 final AlignmentGeometry effectiveAlignment = alignment ?? badgeTheme.alignment ?? defaults.alignment!;
218 final TextDirection textDirection = Directionality.of(context);
219 final Offset defaultOffset = textDirection == TextDirection.ltr ? const Offset(4, -4) : const Offset(-4, -4);
220 // Adds a offset const Offset(0, 8) to avoiding breaking customers after
221 // the offset calculation changes.
222 // See https://github.com/flutter/flutter/pull/146853.
223 final Offset effectiveOffset = (offset ?? badgeTheme.offset ?? defaultOffset) + const Offset(0, 8);
224
225 return
226 Stack(
227 clipBehavior: Clip.none,
228 children: <Widget>[
229 child!,
230 Positioned.fill(
231 child: _Badge(
232 alignment: effectiveAlignment,
233 offset: hasLabel ? effectiveOffset : Offset.zero,
234 hasLabel: hasLabel,
235 widthOffset: effectiveWidthOffset,
236 textDirection: textDirection,
237 child: badge,
238 ),
239 ),
240 ],
241 );
242 }
243}
244
245class _Badge extends SingleChildRenderObjectWidget {
246 const _Badge({
247 required this.alignment,
248 required this.offset,
249 required this.widthOffset,
250 required this.textDirection,
251 required this.hasLabel,
252 super.child, // the badge
253 });
254
255 final AlignmentGeometry alignment;
256 final Offset offset;
257 final double widthOffset;
258 final TextDirection textDirection;
259 final bool hasLabel;
260
261 @override
262 _RenderBadge createRenderObject(BuildContext context) {
263 return _RenderBadge(
264 alignment: alignment,
265 widthOffset: widthOffset,
266 hasLabel: hasLabel,
267 offset: offset,
268 textDirection: Directionality.maybeOf(context),
269 );
270 }
271
272 @override
273 void updateRenderObject(BuildContext context, _RenderBadge renderObject) {
274 renderObject
275 ..alignment = alignment
276 ..offset = offset
277 ..widthOffset = widthOffset
278 ..hasLabel = hasLabel
279 ..textDirection = Directionality.maybeOf(context);
280 }
281
282 @override
283 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
284 super.debugFillProperties(properties);
285 properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
286 properties.add(DiagnosticsProperty<Offset>('offset', offset));
287 }
288}
289
290class _RenderBadge extends RenderAligningShiftedBox {
291 _RenderBadge({
292 super.textDirection,
293 super.alignment,
294 required Offset offset,
295 required bool hasLabel,
296 required double widthOffset,
297 }) : _offset = offset,
298 _hasLabel = hasLabel,
299 _widthOffset = widthOffset;
300
301 Offset get offset => _offset;
302 Offset _offset;
303 set offset(Offset value) {
304 if (_offset == value) {
305 return;
306 }
307 _offset = value;
308 markNeedsLayout();
309 }
310
311 bool get hasLabel => _hasLabel;
312 bool _hasLabel;
313 set hasLabel(bool value) {
314 if (_hasLabel == value) {
315 return;
316 }
317 _hasLabel = value;
318 markNeedsLayout();
319 }
320
321 double get widthOffset => _widthOffset;
322 double _widthOffset;
323 set widthOffset(double value) {
324 if (_widthOffset == value) {
325 return;
326 }
327 _widthOffset = value;
328 markNeedsLayout();
329 }
330
331 @override
332 void performLayout() {
333 final BoxConstraints constraints = this.constraints;
334 assert(constraints.hasBoundedWidth);
335 assert(constraints.hasBoundedHeight);
336 size = constraints.biggest;
337
338 child!.layout(const BoxConstraints(), parentUsesSize: true);
339 final double badgeSize = child!.size.height;
340 final Alignment resolvedAlignment = alignment.resolve(textDirection);
341 final BoxParentData childParentData = child!.parentData! as BoxParentData;
342 Offset badgeLocation = offset + resolvedAlignment.alongOffset(Offset(size.width - widthOffset, size.height));
343 if (hasLabel) {
344 // Adjust for label height.
345 badgeLocation = badgeLocation - Offset(0, badgeSize / 2);
346 }
347 childParentData.offset = badgeLocation;
348 }
349}
350
351/// A widget size itself to the smallest horizontal stadium rect that can still
352/// fit the child's intrinsic size.
353///
354/// A horizontal stadium means a rect that has width >= height.
355///
356/// Uses [minSize] to set the min size of width and height.
357class _IntrinsicHorizontalStadium extends SingleChildRenderObjectWidget {
358 const _IntrinsicHorizontalStadium({super.child, required this.minSize});
359 final double minSize;
360
361 @override
362 _RenderIntrinsicHorizontalStadium createRenderObject(BuildContext context) {
363 return _RenderIntrinsicHorizontalStadium(minSize: minSize);
364 }
365}
366
367class _RenderIntrinsicHorizontalStadium extends RenderProxyBox {
368 _RenderIntrinsicHorizontalStadium({
369 RenderBox? child,
370 required double minSize,
371 }) : _minSize = minSize,
372 super(child);
373
374 double get minSize => _minSize;
375 double _minSize;
376 set minSize(double value) {
377 if (_minSize == value) {
378 return;
379 }
380 _minSize = value;
381 markNeedsLayout();
382 }
383
384 @override
385 double computeMinIntrinsicWidth(double height) {
386 return getMaxIntrinsicWidth(height);
387 }
388
389 @override
390 double computeMaxIntrinsicWidth(double height) {
391 return math.max(getMaxIntrinsicHeight(double.infinity), super.computeMaxIntrinsicWidth(height));
392 }
393
394 @override
395 double computeMinIntrinsicHeight(double width) {
396 return getMaxIntrinsicHeight(width);
397 }
398
399 @override
400 double computeMaxIntrinsicHeight(double width) {
401 return math.max(minSize, super.computeMaxIntrinsicHeight(width));
402 }
403
404 BoxConstraints _childConstraints(RenderBox child, BoxConstraints constraints) {
405 final double childHeight = math.max(minSize, child.getMaxIntrinsicHeight(constraints.maxWidth));
406 final double childWidth = child.getMaxIntrinsicWidth(constraints.maxHeight);
407 return constraints.tighten(width: math.max(childWidth, childHeight), height: childHeight);
408 }
409
410 Size _computeSize({required ChildLayouter layoutChild, required BoxConstraints constraints}) {
411 final RenderBox child = this.child!;
412 final Size childSize = layoutChild(child, _childConstraints(child, constraints));
413 if (childSize.height > childSize.width) {
414 return Size(childSize.height, childSize.height);
415 }
416 return childSize;
417 }
418
419 @override
420 @protected
421 Size computeDryLayout(covariant BoxConstraints constraints) {
422 return _computeSize(
423 layoutChild: ChildLayoutHelper.dryLayoutChild,
424 constraints: constraints,
425 );
426 }
427
428 @override
429 double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
430 final RenderBox child = this.child!;
431 return child.getDryBaseline(_childConstraints(child, constraints), baseline);
432 }
433
434 @override
435 void performLayout() {
436 size = _computeSize(
437 layoutChild: ChildLayoutHelper.layoutChild,
438 constraints: constraints,
439 );
440 }
441}
442
443
444// BEGIN GENERATED TOKEN PROPERTIES - Badge
445
446// Do not edit by hand. The code between the "BEGIN GENERATED" and
447// "END GENERATED" comments are generated from data in the Material
448// Design token database by the script:
449// dev/tools/gen_defaults/bin/gen_defaults.dart.
450
451class _BadgeDefaultsM3 extends BadgeThemeData {
452 _BadgeDefaultsM3(this.context) : super(
453 smallSize: 6.0,
454 largeSize: 16.0,
455 padding: const EdgeInsets.symmetric(horizontal: 4),
456 alignment: AlignmentDirectional.topEnd,
457 );
458
459 final BuildContext context;
460 late final ThemeData _theme = Theme.of(context);
461 late final ColorScheme _colors = _theme.colorScheme;
462
463 @override
464 Color? get backgroundColor => _colors.error;
465
466 @override
467 Color? get textColor => _colors.onError;
468
469 @override
470 TextStyle? get textStyle => Theme.of(context).textTheme.labelSmall;
471}
472
473// END GENERATED TOKEN PROPERTIES - Badge
474

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com