From 3273429eaabcb4bc5c47655be58465fb86d3bae8 Mon Sep 17 00:00:00 2001 From: Vitaliy Hnidenko <82770717+sawel24@users.noreply.github.com> Date: Thu, 6 Apr 2023 18:28:18 +0300 Subject: [PATCH 1/8] feat: add triangle badge shape (#107) * feat: add triangle badge shape * Add widget tests for triangle badge shape --- example/.flutter-plugins-dependencies | 2 +- example/lib/alarm_app.dart | 29 ++++++-- lib/src/badge.dart | 3 +- lib/src/badge_shape.dart | 5 ++ .../triangle_badge_shape_painter.dart | 72 +++++++++++++++++++ lib/src/utils/drawing_utils.dart | 8 +++ test/badges_test.dart | 71 ++++++++++++++++++ 7 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 lib/src/painters/triangle_badge_shape_painter.dart diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 1fbe401..4dadac9 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/Users/yadaniyil/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/Users/yadaniyil/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]}],"date_created":"2023-03-24 12:03:15.082040","version":"3.7.6"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/Users/vitalii/flutter3/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/Users/vitalii/flutter3/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]}],"date_created":"2023-04-03 12:55:06.152967","version":"3.7.0"} \ No newline at end of file diff --git a/example/lib/alarm_app.dart b/example/lib/alarm_app.dart index 80e3322..5c6f913 100644 --- a/example/lib/alarm_app.dart +++ b/example/lib/alarm_app.dart @@ -17,18 +17,33 @@ class _AlarmAppState extends State { @override Widget build(BuildContext context) { return badges.Badge( - badgeStyle: badges.BadgeStyle(padding: EdgeInsets.all(7)), + badgeStyle: badges.BadgeStyle( + borderSide: BorderSide(color: Colors.white, width: 2), + shape: badges.BadgeShape.triangle, + badgeGradient: badges.BadgeGradient.linear( + colors: [ + Colors.red, + Colors.orange, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), badgeAnimation: badges.BadgeAnimation.fade( animationDuration: Duration(seconds: 1), loopAnimation: _isLooped, ), - // onTap: () { - // setState(() => _isLooped = !_isLooped); - // }, ignorePointer: false, - // toAnimate: false, - badgeContent: - Text(counter.toString(), style: TextStyle(color: Colors.white)), + badgeContent: Container( + width: 20, + height: 20, + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + '!', + style: TextStyle(color: Colors.white), + ), + )), position: badges.BadgePosition.topEnd(top: -12), child: GestureDetector( onTap: () { diff --git a/lib/src/badge.dart b/lib/src/badge.dart index 7a13e8b..70ee966 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -157,7 +157,8 @@ class BadgeState extends State with TickerProviderStateMixin { borderRadius: widget.badgeStyle.borderRadius, ); final isCustomShape = widget.badgeStyle.shape == BadgeShape.twitter || - widget.badgeStyle.shape == BadgeShape.instagram; + widget.badgeStyle.shape == BadgeShape.instagram || + widget.badgeStyle.shape == BadgeShape.triangle; final gradientBorder = widget.badgeStyle.borderGradient != null ? BadgeBorderGradient( diff --git a/lib/src/badge_shape.dart b/lib/src/badge_shape.dart index 4788701..3aacf91 100644 --- a/lib/src/badge_shape.dart +++ b/lib/src/badge_shape.dart @@ -1,4 +1,5 @@ import 'package:badges/badges.dart' as badges; +import 'package:badges/src/painters/triangle_badge_shape_painter.dart'; import 'package:flutter/material.dart'; /// Set of shapes that you can use for your [badges.Badge] widget. @@ -15,6 +16,10 @@ enum BadgeShape { /// * [RoundedRectangleBorder] square, + /// To make the triangle badge . + /// See [TriangleBadgeShapePainter] for more details. + triangle, + /// To make the twitter badge . /// See [TwitterBadgeShapePainter] for more details. twitter, diff --git a/lib/src/painters/triangle_badge_shape_painter.dart b/lib/src/painters/triangle_badge_shape_painter.dart new file mode 100644 index 0000000..a7e5454 --- /dev/null +++ b/lib/src/painters/triangle_badge_shape_painter.dart @@ -0,0 +1,72 @@ +import 'package:badges/badges.dart'; +import 'package:badges/src/utils/gradient_utils.dart'; +import 'package:flutter/material.dart'; + +class TriangleBadgeShapePainter extends CustomPainter { + Color? color; + BadgeGradient? badgeGradient; + BadgeGradient? borderGradient; + BorderSide? borderSide; + + TriangleBadgeShapePainter({ + Key? key, + this.color = Colors.blue, + this.badgeGradient, + this.borderGradient, + this.borderSide, + }); + + @override + void paint(Canvas canvas, Size size) { + final width = size.width; + final height = size.height; + + Path path = Path(); + Paint paint = Paint(); + Paint paintBorder = Paint(); + + if (badgeGradient != null) { + paint.shader = GradientUtils.getGradientShader( + badgeGradient: badgeGradient!, + width: width, + height: height, + ); + } + paintBorder + ..color = borderSide?.color ?? Colors.white + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeWidth = borderSide?.width ?? 0; + + if (borderGradient != null) { + paintBorder.shader = GradientUtils.getGradientShader( + badgeGradient: borderGradient!, + width: width, + height: height, + ); + } + path + ..moveTo(width * 0.132, height * 0.888) + ..arcToPoint(Offset(width * 0.075, height * 0.772), + radius: Radius.circular(height * 0.09)) + ..lineTo(width * 0.428, height * 0.156) + ..arcToPoint(Offset(width * 0.582, height * 0.156), + radius: Radius.circular(height * 0.09)) + ..lineTo(width * 0.928, height * 0.756) + ..arcToPoint(Offset(width * 0.868, height * 0.888), + radius: Radius.circular(height * 0.09)) + ..lineTo(width * 0.132, height * 0.888); + path.close(); + + paint.color = color!; + canvas.drawPath(path, paint); + if (borderSide != BorderSide.none) { + canvas.drawPath(path, paintBorder); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/src/utils/drawing_utils.dart b/lib/src/utils/drawing_utils.dart index a896794..fa051af 100644 --- a/lib/src/utils/drawing_utils.dart +++ b/lib/src/utils/drawing_utils.dart @@ -1,5 +1,6 @@ import 'package:badges/badges.dart'; import 'package:badges/src/painters/instagram_badge_shape_painter.dart'; +import 'package:badges/src/painters/triangle_badge_shape_painter.dart'; import 'package:badges/src/painters/twitter_badge_shape_painter.dart'; import 'package:flutter/material.dart'; @@ -26,6 +27,13 @@ class DrawingUtils { borderSide: borderSide, borderGradient: borderGradient, ); + case BadgeShape.triangle: + return TriangleBadgeShapePainter( + color: color, + badgeGradient: badgeGradient, + borderSide: borderSide, + borderGradient: borderGradient, + ); case BadgeShape.square: case BadgeShape.circle: break; diff --git a/test/badges_test.dart b/test/badges_test.dart index fbf6bbd..3bd661f 100644 --- a/test/badges_test.dart +++ b/test/badges_test.dart @@ -2,6 +2,7 @@ import 'package:badges/badges.dart' as badges; import 'package:badges/src/badge_border_gradient.dart'; import 'package:badges/src/badge_gradient_type.dart'; import 'package:badges/src/painters/instagram_badge_shape_painter.dart'; +import 'package:badges/src/painters/triangle_badge_shape_painter.dart'; import 'package:badges/src/painters/twitter_badge_shape_painter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -334,6 +335,76 @@ void main() { expect(painter.shouldRepaint(painter), true); }); }); + + group('Triangle shape', () { + const triangleBadge = badges.Badge( + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.triangle, + badgeColor: Colors.green, + badgeGradient: + badges.BadgeGradient.radial(colors: [Colors.black, Colors.green]), + borderGradient: + badges.BadgeGradient.linear(colors: [Colors.red, Colors.yellow]), + borderSide: BorderSide(width: 2), + ), + ); + + testWidgets('Triangle badge should render correctly', (tester) async { + await tester.pumpWidget(_wrapWithMaterialApp(triangleBadge)); + expect(find.byType(badges.Badge), findsOneWidget); + expect(find.byType(CustomPaint), findsWidgets); + }); + + testWidgets('Triangle badge color should match', (tester) async { + await tester.pumpWidget(_wrapWithMaterialApp(triangleBadge)); + + final customPaint = + tester.widgetList(find.byType(CustomPaint)).last; + final painter = customPaint.painter as TriangleBadgeShapePainter; + expect(painter.color, Colors.green); + }); + + testWidgets('Triangle badge gradient should match', (tester) async { + await tester.pumpWidget(_wrapWithMaterialApp(triangleBadge)); + final customPaint = + tester.widgetList(find.byType(CustomPaint)).last; + final painter = customPaint.painter as TriangleBadgeShapePainter; + expect(painter.badgeGradient?.gradientType, BadgeGradientType.radial); + expect(painter.badgeGradient?.colors.first, Colors.black); + expect(painter.badgeGradient?.colors.last, Colors.green); + expect(painter.badgeGradient?.colors.length, 2); + }); + + testWidgets('Triangle badge border gradient should match', + (tester) async { + await tester.pumpWidget(_wrapWithMaterialApp(triangleBadge)); + final customPaint = + tester.widgetList(find.byType(CustomPaint)).last; + final painter = customPaint.painter as TriangleBadgeShapePainter; + expect(painter.borderGradient?.gradientType, BadgeGradientType.linear); + expect(painter.borderGradient?.colors.first, Colors.red); + expect(painter.borderGradient?.colors.last, Colors.yellow); + expect(painter.borderGradient?.colors.length, 2); + }); + + testWidgets('Triangle badge border should match', (tester) async { + await tester.pumpWidget(_wrapWithMaterialApp(triangleBadge)); + final customPaint = + tester.widgetList(find.byType(CustomPaint)).last; + final painter = customPaint.painter as TriangleBadgeShapePainter; + expect(painter.borderSide?.width, 2); + expect(painter.borderSide?.style, BorderStyle.solid); + expect(painter.borderSide?.strokeAlign, BorderSide.strokeAlignInside); + }); + + testWidgets('Triangle badge repaint should match', (tester) async { + await tester.pumpWidget(_wrapWithMaterialApp(triangleBadge)); + final customPaint = + tester.widgetList(find.byType(CustomPaint)).last; + final painter = customPaint.painter as TriangleBadgeShapePainter; + expect(painter.shouldRepaint(painter), true); + }); + }); }); group('Badge tests', () { From fec7f9fc43bc0f6a4ad9d7804c823135eaf8f9de Mon Sep 17 00:00:00 2001 From: Vitaliy Hnidenko <82770717+sawel24@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:39:39 +0300 Subject: [PATCH 2/8] fix: set all custom shapes to fixed-size proportions (#108) * Update custom painter for all custom shapes to set fixed-size proportions * Return default value of child content for verified account widgets * Resize the shape depending on the child widget in the same proportions * Fix git check errors * Resize all types of shapes depending on the size child widget * Add method for calculate default badge content padding value * Change method for calculate default badge content padding value * Add unit tests for calculateBadgeContentPadding method --- example/lib/alarm_app.dart | 14 +- lib/src/badge.dart | 122 +++++++++++------- lib/src/badge_style.dart | 4 +- .../instagram_badge_shape_painter.dart | 64 ++++----- .../triangle_badge_shape_painter.dart | 34 +++-- .../painters/twitter_badge_shape_painter.dart | 48 ++++--- test/badges_test.dart | 65 +++++++++- 7 files changed, 229 insertions(+), 122 deletions(-) diff --git a/example/lib/alarm_app.dart b/example/lib/alarm_app.dart index 5c6f913..c5db1e2 100644 --- a/example/lib/alarm_app.dart +++ b/example/lib/alarm_app.dart @@ -34,16 +34,10 @@ class _AlarmAppState extends State { loopAnimation: _isLooped, ), ignorePointer: false, - badgeContent: Container( - width: 20, - height: 20, - child: Padding( - padding: const EdgeInsets.only(left: 8), - child: Text( - '!', - style: TextStyle(color: Colors.white), - ), - )), + badgeContent: Text( + '!', + style: TextStyle(color: Colors.white), + ), position: badges.BadgePosition.topEnd(top: -12), child: GestureDetector( onTap: () { diff --git a/lib/src/badge.dart b/lib/src/badge.dart index 70ee966..843db20 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -144,6 +144,21 @@ class BadgeState extends State with TickerProviderStateMixin { return _appearanceController.value; } + EdgeInsets calculateBadgeContentPadding( + Widget? badgeContent, + BadgeShape shape, + ) { + final isTextContent = badgeContent is Text; + final isTriangleShape = shape == BadgeShape.triangle; + if (isTriangleShape) { + return const EdgeInsets.symmetric(horizontal: 10.0); + } else if (isTextContent) { + return const EdgeInsets.symmetric(horizontal: 8.0); + } else { + return const EdgeInsets.symmetric(horizontal: 5.0); + } + } + Widget _getBadge() { final border = widget.badgeStyle.shape == BadgeShape.circle ? CircleBorder( @@ -174,52 +189,69 @@ class BadgeState extends State with TickerProviderStateMixin { builder: (context, child) { return Opacity( opacity: _getOpacity(), - child: isCustomShape - ? CustomPaint( - painter: DrawingUtils.drawBadgeShape( - shape: widget.badgeStyle.shape, - color: widget.badgeStyle.badgeColor, - badgeGradient: widget.badgeStyle.badgeGradient, - borderGradient: widget.badgeStyle.borderGradient, - borderSide: widget.badgeStyle.borderSide, - ), - child: Padding( - padding: widget.badgeStyle.padding, - child: widget.badgeContent, - ), - ) - : Material( - shape: border, - elevation: widget.badgeStyle.elevation, - // Without this Colors.transparent will be ignored - type: MaterialType.transparency, - child: AnimatedContainer( - curve: widget.badgeAnimation.colorChangeAnimationCurve, - duration: widget.badgeAnimation.toAnimate - ? widget.badgeAnimation.colorChangeAnimationDuration - : Duration.zero, - decoration: widget.badgeStyle.shape == BadgeShape.circle - ? BoxDecoration( - color: widget.badgeStyle.badgeColor, - border: gradientBorder, - gradient: - widget.badgeStyle.badgeGradient?.gradient(), - shape: BoxShape.circle, - ) - : BoxDecoration( - color: widget.badgeStyle.badgeColor, - gradient: - widget.badgeStyle.badgeGradient?.gradient(), - shape: BoxShape.rectangle, - borderRadius: widget.badgeStyle.borderRadius, - border: gradientBorder, + child: UnconstrainedBox( + child: IntrinsicWidth( + child: AspectRatio( + aspectRatio: + widget.badgeStyle.shape == BadgeShape.square ? 1.5 : 1.0, + child: isCustomShape + ? CustomPaint( + painter: DrawingUtils.drawBadgeShape( + shape: widget.badgeStyle.shape, + color: widget.badgeStyle.badgeColor, + badgeGradient: widget.badgeStyle.badgeGradient, + borderGradient: widget.badgeStyle.borderGradient, + borderSide: widget.badgeStyle.borderSide, + ), + child: Padding( + padding: widget.badgeStyle.padding ?? + calculateBadgeContentPadding( + widget.badgeContent, + widget.badgeStyle.shape, + ), + child: Center(child: widget.badgeContent), + ), + ) + : Material( + shape: border, + elevation: widget.badgeStyle.elevation, + // Without this Colors.transparent will be ignored + type: MaterialType.transparency, + child: AnimatedContainer( + curve: + widget.badgeAnimation.colorChangeAnimationCurve, + duration: widget.badgeAnimation.toAnimate + ? widget + .badgeAnimation.colorChangeAnimationDuration + : Duration.zero, + decoration: widget.badgeStyle.shape == + BadgeShape.circle + ? BoxDecoration( + color: widget.badgeStyle.badgeColor, + border: gradientBorder, + gradient: widget.badgeStyle.badgeGradient + ?.gradient(), + shape: BoxShape.circle, + ) + : BoxDecoration( + color: widget.badgeStyle.badgeColor, + gradient: widget.badgeStyle.badgeGradient + ?.gradient(), + shape: BoxShape.rectangle, + borderRadius: + widget.badgeStyle.borderRadius, + border: gradientBorder, + ), + child: Padding( + padding: widget.badgeStyle.padding ?? + const EdgeInsets.symmetric(horizontal: 5.0), + child: Center(child: widget.badgeContent), ), - child: Padding( - padding: widget.badgeStyle.padding, - child: widget.badgeContent, - ), - ), - ), + ), + ), + ), + ), + ), ); }, ); diff --git a/lib/src/badge_style.dart b/lib/src/badge_style.dart index 55486aa..1aad24c 100644 --- a/lib/src/badge_style.dart +++ b/lib/src/badge_style.dart @@ -31,7 +31,7 @@ class BadgeStyle { /// Specifies padding for [badgeContent]. /// The default value is EdgeInsets.all(5.0). - final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? padding; const BadgeStyle({ this.shape = BadgeShape.circle, @@ -41,6 +41,6 @@ class BadgeStyle { this.elevation = 2, this.badgeGradient, this.borderGradient, - this.padding = const EdgeInsets.all(5.0), + this.padding, }); } diff --git a/lib/src/painters/instagram_badge_shape_painter.dart b/lib/src/painters/instagram_badge_shape_painter.dart index 51560fa..65a16db 100644 --- a/lib/src/painters/instagram_badge_shape_painter.dart +++ b/lib/src/painters/instagram_badge_shape_painter.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:badges/badges.dart'; import 'package:badges/src/utils/gradient_utils.dart'; import 'package:flutter/material.dart'; @@ -21,6 +23,10 @@ class InstagramBadgeShapePainter extends CustomPainter { final width = size.width; final height = size.height; + final double maxSize = math.max(width, height); + + canvas.clipRect(Offset.zero & Size(maxSize, maxSize)); + Path path = Path(); Paint paint = Paint(); Paint paintBorder = Paint(); @@ -28,8 +34,8 @@ class InstagramBadgeShapePainter extends CustomPainter { if (badgeGradient != null) { paint.shader = GradientUtils.getGradientShader( badgeGradient: badgeGradient!, - width: width, - height: height, + width: maxSize, + height: maxSize, ); } paintBorder @@ -41,36 +47,36 @@ class InstagramBadgeShapePainter extends CustomPainter { if (borderGradient != null) { paintBorder.shader = GradientUtils.getGradientShader( badgeGradient: borderGradient!, - width: width, - height: height, + width: maxSize, + height: maxSize, ); } - path.moveTo(width * 0.14, height * 0.14); - path.lineTo(width * 0.3, height * 0.14); - path.lineTo(width * 0.385, 0); - path.lineTo(width * 0.515, height * 0.08); - path.lineTo(width * 0.627, height * 0.012); - path.lineTo(width * 0.7, height * 0.134); - path.lineTo(width * 0.867, height * 0.134); - path.lineTo(width * 0.867, height * 0.3); - path.lineTo(width, height * 0.38); - path.lineTo(width * 0.922, height * 0.505); - path.lineTo(width * 0.995, height * 0.629); - path.lineTo(width * 0.866, height * 0.706); - path.lineTo(width * 0.866, height * 0.868); - path.lineTo(width * 0.697, height * 0.868); - path.lineTo(width * 0.618, height * 0.996); - path.lineTo(width * 0.5, height * 0.924); - path.lineTo(width * 0.379, height * 0.996); - path.lineTo(width * 0.302, height * 0.868); - path.lineTo(width * 0.14, height * 0.868); - path.lineTo(width * 0.14, height * 0.702); - path.lineTo(width * 0.004, height * 0.618); - path.lineTo(width * 0.08, height * 0.494); - path.lineTo(width * 0.012, height * 0.379); - path.lineTo(width * 0.14, height * 0.306); - path.lineTo(width * 0.14, height * 0.14); + path.moveTo(maxSize * 0.14, maxSize * 0.14); + path.lineTo(maxSize * 0.3, maxSize * 0.14); + path.lineTo(maxSize * 0.385, 0); + path.lineTo(maxSize * 0.515, maxSize * 0.08); + path.lineTo(maxSize * 0.627, maxSize * 0.012); + path.lineTo(maxSize * 0.7, maxSize * 0.134); + path.lineTo(maxSize * 0.867, maxSize * 0.134); + path.lineTo(maxSize * 0.867, maxSize * 0.3); + path.lineTo(maxSize, maxSize * 0.38); + path.lineTo(maxSize * 0.922, maxSize * 0.505); + path.lineTo(maxSize * 0.995, maxSize * 0.629); + path.lineTo(maxSize * 0.866, maxSize * 0.706); + path.lineTo(maxSize * 0.866, maxSize * 0.868); + path.lineTo(maxSize * 0.697, maxSize * 0.868); + path.lineTo(maxSize * 0.618, maxSize * 0.996); + path.lineTo(maxSize * 0.5, maxSize * 0.924); + path.lineTo(maxSize * 0.379, maxSize * 0.996); + path.lineTo(maxSize * 0.302, maxSize * 0.868); + path.lineTo(maxSize * 0.14, maxSize * 0.868); + path.lineTo(maxSize * 0.14, maxSize * 0.702); + path.lineTo(maxSize * 0.004, maxSize * 0.618); + path.lineTo(maxSize * 0.08, maxSize * 0.494); + path.lineTo(maxSize * 0.012, maxSize * 0.379); + path.lineTo(maxSize * 0.14, maxSize * 0.306); + path.lineTo(maxSize * 0.14, maxSize * 0.14); paint.color = color!; canvas.drawPath(path, paint); diff --git a/lib/src/painters/triangle_badge_shape_painter.dart b/lib/src/painters/triangle_badge_shape_painter.dart index a7e5454..37ce8bb 100644 --- a/lib/src/painters/triangle_badge_shape_painter.dart +++ b/lib/src/painters/triangle_badge_shape_painter.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:badges/badges.dart'; import 'package:badges/src/utils/gradient_utils.dart'; import 'package:flutter/material.dart'; @@ -21,6 +23,10 @@ class TriangleBadgeShapePainter extends CustomPainter { final width = size.width; final height = size.height; + final double maxSize = math.max(width, height); + + canvas.clipRect(Offset.zero & Size(maxSize, maxSize)); + Path path = Path(); Paint paint = Paint(); Paint paintBorder = Paint(); @@ -28,8 +34,8 @@ class TriangleBadgeShapePainter extends CustomPainter { if (badgeGradient != null) { paint.shader = GradientUtils.getGradientShader( badgeGradient: badgeGradient!, - width: width, - height: height, + width: maxSize, + height: maxSize, ); } paintBorder @@ -41,21 +47,21 @@ class TriangleBadgeShapePainter extends CustomPainter { if (borderGradient != null) { paintBorder.shader = GradientUtils.getGradientShader( badgeGradient: borderGradient!, - width: width, - height: height, + width: maxSize, + height: maxSize, ); } path - ..moveTo(width * 0.132, height * 0.888) - ..arcToPoint(Offset(width * 0.075, height * 0.772), - radius: Radius.circular(height * 0.09)) - ..lineTo(width * 0.428, height * 0.156) - ..arcToPoint(Offset(width * 0.582, height * 0.156), - radius: Radius.circular(height * 0.09)) - ..lineTo(width * 0.928, height * 0.756) - ..arcToPoint(Offset(width * 0.868, height * 0.888), - radius: Radius.circular(height * 0.09)) - ..lineTo(width * 0.132, height * 0.888); + ..moveTo(maxSize * 0.132, maxSize * 0.888) + ..arcToPoint(Offset(maxSize * 0.075, maxSize * 0.772), + radius: Radius.circular(maxSize * 0.09)) + ..lineTo(maxSize * 0.428, maxSize * 0.156) + ..arcToPoint(Offset(maxSize * 0.582, maxSize * 0.156), + radius: Radius.circular(maxSize * 0.09)) + ..lineTo(maxSize * 0.928, maxSize * 0.756) + ..arcToPoint(Offset(maxSize * 0.868, maxSize * 0.888), + radius: Radius.circular(maxSize * 0.09)) + ..lineTo(maxSize * 0.132, maxSize * 0.888); path.close(); paint.color = color!; diff --git a/lib/src/painters/twitter_badge_shape_painter.dart b/lib/src/painters/twitter_badge_shape_painter.dart index 2b02642..1b05422 100644 --- a/lib/src/painters/twitter_badge_shape_painter.dart +++ b/lib/src/painters/twitter_badge_shape_painter.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:badges/src/badge_gradient.dart'; import 'package:badges/src/utils/gradient_utils.dart'; import 'package:flutter/material.dart'; @@ -21,6 +23,10 @@ class TwitterBadgeShapePainter extends CustomPainter { final width = size.width; final height = size.height; + final double maxSize = math.max(width, height); + + canvas.clipRect(Offset.zero & Size(maxSize, maxSize)); + Path path = Path(); Paint paint = Paint(); Paint paintBorder = Paint(); @@ -28,8 +34,8 @@ class TwitterBadgeShapePainter extends CustomPainter { if (badgeGradient != null) { paint.shader = GradientUtils.getGradientShader( badgeGradient: badgeGradient!, - width: width, - height: height, + width: maxSize, + height: maxSize, ); } paintBorder @@ -41,28 +47,28 @@ class TwitterBadgeShapePainter extends CustomPainter { if (borderGradient != null) { paintBorder.shader = GradientUtils.getGradientShader( badgeGradient: borderGradient!, - width: width, - height: height, + width: maxSize, + height: maxSize, ); } - path.moveTo(width * 0.357, height * 0.156); - path.arcToPoint(Offset(width * 0.643, height * 0.156), - radius: Radius.circular(height * 0.157)); - path.arcToPoint(Offset(width * 0.847, height * 0.396), - radius: Radius.circular(height * 0.165)); - path.arcToPoint(Offset(width * 0.857, height * 0.666), - radius: Radius.circular(height * 0.170)); - path.arcToPoint(Offset(width * 0.643, height * 0.844), - radius: Radius.circular(height * 0.163)); - path.arcToPoint(Offset(width * 0.357, height * 0.844), - radius: Radius.circular(height * 0.157)); - path.arcToPoint(Offset(width * 0.145, height * 0.665), - radius: Radius.circular(height * 0.163)); - path.arcToPoint(Offset(width * 0.154, height * 0.372), - radius: Radius.circular(height * 0.170)); - path.arcToPoint(Offset(width * 0.357, height * 0.156), - radius: Radius.circular(height * 0.163)); + path.moveTo(maxSize * 0.357, maxSize * 0.156); + path.arcToPoint(Offset(maxSize * 0.643, maxSize * 0.156), + radius: Radius.circular(maxSize * 0.157)); + path.arcToPoint(Offset(maxSize * 0.847, maxSize * 0.396), + radius: Radius.circular(maxSize * 0.165)); + path.arcToPoint(Offset(maxSize * 0.857, maxSize * 0.666), + radius: Radius.circular(maxSize * 0.170)); + path.arcToPoint(Offset(maxSize * 0.643, maxSize * 0.844), + radius: Radius.circular(maxSize * 0.163)); + path.arcToPoint(Offset(maxSize * 0.357, maxSize * 0.844), + radius: Radius.circular(maxSize * 0.157)); + path.arcToPoint(Offset(maxSize * 0.145, maxSize * 0.665), + radius: Radius.circular(maxSize * 0.163)); + path.arcToPoint(Offset(maxSize * 0.154, maxSize * 0.372), + radius: Radius.circular(maxSize * 0.170)); + path.arcToPoint(Offset(maxSize * 0.357, maxSize * 0.156), + radius: Radius.circular(maxSize * 0.163)); paint.color = color!; canvas.drawPath(path, paint); diff --git a/test/badges_test.dart b/test/badges_test.dart index 3bd661f..74c4f4d 100644 --- a/test/badges_test.dart +++ b/test/badges_test.dart @@ -1,6 +1,7 @@ import 'package:badges/badges.dart' as badges; import 'package:badges/src/badge_border_gradient.dart'; import 'package:badges/src/badge_gradient_type.dart'; +import 'package:badges/src/badge_shape.dart'; import 'package:badges/src/painters/instagram_badge_shape_painter.dart'; import 'package:badges/src/painters/triangle_badge_shape_painter.dart'; import 'package:badges/src/painters/twitter_badge_shape_painter.dart'; @@ -18,6 +19,68 @@ import 'test_widget_screen.dart'; import 'utils_tests.dart'; void main() { + group('Unit tests', () { + test('Calculate padding in triangle badge with text', () async { + const badge = badges.Badge( + badgeContent: Text('!'), + badgeStyle: badges.BadgeStyle(shape: BadgeShape.triangle), + ); + final badgeState = badge.createState(); + final edgeInsets = badgeState.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle.shape, + ); + expect(edgeInsets.top, 0); + expect(edgeInsets.bottom, 0); + expect(edgeInsets.left, 10); + expect(edgeInsets.right, 10); + }); + test('Calculate padding in triangle badge with icon', () async { + const badge = badges.Badge( + badgeContent: Icon(Icons.check, size: 10), + badgeStyle: badges.BadgeStyle(shape: BadgeShape.triangle), + ); + final badgeState = badge.createState(); + final edgeInsets = badgeState.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle.shape, + ); + expect(edgeInsets.top, 0); + expect(edgeInsets.bottom, 0); + expect(edgeInsets.left, 10); + expect(edgeInsets.right, 10); + }); + test('Calculate padding in instagram badge with icon', () async { + const badge = badges.Badge( + badgeContent: Icon(Icons.check, size: 10), + badgeStyle: badges.BadgeStyle(shape: BadgeShape.instagram), + ); + final badgeState = badge.createState(); + final edgeInsets = badgeState.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle.shape, + ); + expect(edgeInsets.top, 0); + expect(edgeInsets.bottom, 0); + expect(edgeInsets.left, 5); + expect(edgeInsets.right, 5); + }); + test('Calculate padding in instagram badge with text', () async { + const badge = badges.Badge( + badgeContent: Text('test'), + badgeStyle: badges.BadgeStyle(shape: BadgeShape.instagram), + ); + final badgeState = badge.createState(); + final edgeInsets = badgeState.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle.shape, + ); + expect(edgeInsets.top, 0); + expect(edgeInsets.bottom, 0); + expect(edgeInsets.left, 8); + expect(edgeInsets.right, 8); + }); + }); group('Badge Position tests', () { Widget getBadge(badges.BadgePosition position) { return badges.Badge( @@ -536,7 +599,7 @@ void main() { expect(badgeWidget.badgeStyle.elevation, 2); expect(badgeWidget.badgeStyle.badgeGradient, null); expect(badgeWidget.badgeStyle.borderGradient, null); - expect(badgeWidget.badgeStyle.padding, const EdgeInsets.all(5.0)); + expect(badgeWidget.badgeStyle.padding, null); // Animation expect(badgeWidget.badgeAnimation.toAnimate, true); From 0ec99ea8816749d320cf08b8c9598f27303c955b Mon Sep 17 00:00:00 2001 From: Daniyil Yakovlev Date: Tue, 11 Apr 2023 18:56:13 +0300 Subject: [PATCH 3/8] Version bump to 3.1.0 --- CHANGELOG.md | 4 ++++ README.md | 3 ++- example/.flutter-plugins-dependencies | 2 +- pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e3e21..f3a8e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.1.0] - [April 11, 2023] +* Add BadgeShape.triangle +* Fix custom shapes deformation depending on the badge content + ## [3.0.3] - [March 24, 2023] * Fix transparent color for the badge diff --git a/README.md b/README.md index 1135214..a0669e3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ In your pubspec.yaml ```yaml dependencies: - badges: ^3.0.3 + badges: ^3.1.0 ``` Attention! In Flutter 3.7 the Badge widget was introduced in the Material library, so to escape the ambiguous imports you need to import the package like this: ```dart @@ -104,6 +104,7 @@ From left to right:
2) BadgeShape.square 3) BadgeShape.twitter 4) BadgeShape.instagram +5) BadgeShape.triangle

--- diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 4dadac9..a844e10 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/Users/vitalii/flutter3/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/Users/vitalii/flutter3/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]}],"date_created":"2023-04-03 12:55:06.152967","version":"3.7.0"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/Users/yadaniyil/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/Users/yadaniyil/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]}],"date_created":"2023-04-11 18:51:47.132592","version":"3.7.9"} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 3724bbb..8da0e40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: badges description: A package for creating badges. Badges can be used for an additional marker for any widget, e.g. show a number of items in a shopping cart. -version: 3.0.3 +version: 3.1.0 repository: https://github.com/yako-dev/flutter_badges issue_tracker: https://github.com/yako-dev/flutter_badges/issues From 71e36f0e28097c8d326800a9d426ce66d765b515 Mon Sep 17 00:00:00 2001 From: sawel24 Date: Thu, 13 Apr 2023 19:16:54 +0300 Subject: [PATCH 4/8] fix: display of square badge with small content size --- lib/src/badge.dart | 132 ++++++++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 50 deletions(-) diff --git a/lib/src/badge.dart b/lib/src/badge.dart index 843db20..d9f928c 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -62,10 +62,22 @@ class BadgeState extends State with TickerProviderStateMixin { late AnimationController _appearanceController; late Animation _animation; bool enableLoopAnimation = false; + double? textSize; @override void initState() { super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (widget.badgeContent is Text) { + final text = widget.badgeContent as Text; + final size = _textSize(text.data!, text.style); + setState(() { + if (size.height > size.width) { + textSize = size.height; + } + }); + } + }); enableLoopAnimation = widget.badgeAnimation.animationDuration.inMilliseconds > 0; _animationController = AnimationController( @@ -97,6 +109,15 @@ class BadgeState extends State with TickerProviderStateMixin { } } + Size _textSize(String text, TextStyle? style) { + final TextPainter textPainter = TextPainter( + text: TextSpan(text: text, style: style), + maxLines: 1, + textDirection: TextDirection.ltr) + ..layout(minWidth: 0, maxWidth: double.infinity); + return textPainter.size; + } + @override Widget build(BuildContext context) { if (widget.child == null) { @@ -174,6 +195,7 @@ class BadgeState extends State with TickerProviderStateMixin { final isCustomShape = widget.badgeStyle.shape == BadgeShape.twitter || widget.badgeStyle.shape == BadgeShape.instagram || widget.badgeStyle.shape == BadgeShape.triangle; + final isSquareShape = widget.badgeStyle.shape == BadgeShape.square; final gradientBorder = widget.badgeStyle.borderGradient != null ? BadgeBorderGradient( @@ -189,69 +211,79 @@ class BadgeState extends State with TickerProviderStateMixin { builder: (context, child) { return Opacity( opacity: _getOpacity(), - child: UnconstrainedBox( - child: IntrinsicWidth( - child: AspectRatio( - aspectRatio: - widget.badgeStyle.shape == BadgeShape.square ? 1.5 : 1.0, - child: isCustomShape - ? CustomPaint( - painter: DrawingUtils.drawBadgeShape( - shape: widget.badgeStyle.shape, - color: widget.badgeStyle.badgeColor, - badgeGradient: widget.badgeStyle.badgeGradient, - borderGradient: widget.badgeStyle.borderGradient, - borderSide: widget.badgeStyle.borderSide, - ), + child: isCustomShape + ? CustomPaint( + painter: DrawingUtils.drawBadgeShape( + shape: widget.badgeStyle.shape, + color: widget.badgeStyle.badgeColor, + badgeGradient: widget.badgeStyle.badgeGradient, + borderGradient: widget.badgeStyle.borderGradient, + borderSide: widget.badgeStyle.borderSide, + ), + child: UnconstrainedBox( + child: IntrinsicWidth( + child: AspectRatio( + aspectRatio: 1.0, child: Padding( padding: widget.badgeStyle.padding ?? calculateBadgeContentPadding( widget.badgeContent, widget.badgeStyle.shape, ), - child: Center(child: widget.badgeContent), + child: Center( + child: widget.badgeContent, + ), ), - ) - : Material( - shape: border, - elevation: widget.badgeStyle.elevation, - // Without this Colors.transparent will be ignored - type: MaterialType.transparency, - child: AnimatedContainer( - curve: - widget.badgeAnimation.colorChangeAnimationCurve, - duration: widget.badgeAnimation.toAnimate - ? widget - .badgeAnimation.colorChangeAnimationDuration - : Duration.zero, - decoration: widget.badgeStyle.shape == - BadgeShape.circle - ? BoxDecoration( - color: widget.badgeStyle.badgeColor, - border: gradientBorder, - gradient: widget.badgeStyle.badgeGradient - ?.gradient(), - shape: BoxShape.circle, - ) - : BoxDecoration( - color: widget.badgeStyle.badgeColor, - gradient: widget.badgeStyle.badgeGradient - ?.gradient(), - shape: BoxShape.rectangle, - borderRadius: - widget.badgeStyle.borderRadius, - border: gradientBorder, - ), + ), + ), + ), + ) + : Material( + shape: border, + elevation: widget.badgeStyle.elevation, + // Without this Colors.transparent will be ignored + type: MaterialType.transparency, + child: AnimatedContainer( + curve: widget.badgeAnimation.colorChangeAnimationCurve, + duration: widget.badgeAnimation.toAnimate + ? widget.badgeAnimation.colorChangeAnimationDuration + : Duration.zero, + decoration: widget.badgeStyle.shape == BadgeShape.circle + ? BoxDecoration( + color: widget.badgeStyle.badgeColor, + border: gradientBorder, + gradient: + widget.badgeStyle.badgeGradient?.gradient(), + shape: BoxShape.circle, + ) + : BoxDecoration( + color: widget.badgeStyle.badgeColor, + gradient: + widget.badgeStyle.badgeGradient?.gradient(), + shape: BoxShape.rectangle, + borderRadius: widget.badgeStyle.borderRadius, + border: gradientBorder, + ), + child: UnconstrainedBox( + child: IntrinsicWidth( + child: AspectRatio( + aspectRatio: isSquareShape ? 1.5 : 1.0, child: Padding( padding: widget.badgeStyle.padding ?? const EdgeInsets.symmetric(horizontal: 5.0), - child: Center(child: widget.badgeContent), + child: SizedBox( + height: isSquareShape ? textSize : null, + width: isSquareShape ? textSize : null, + child: Center( + child: widget.badgeContent, + ), + ), ), ), ), - ), - ), - ), + ), + ), + ), ); }, ); From 83c9dc43fc170e95e39980bc269a3109ac4e66b4 Mon Sep 17 00:00:00 2001 From: sawel24 Date: Thu, 13 Apr 2023 19:34:48 +0300 Subject: [PATCH 5/8] fix: change calculation size for text --- lib/src/badge.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/badge.dart b/lib/src/badge.dart index d9f928c..e3789e9 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -73,7 +73,7 @@ class BadgeState extends State with TickerProviderStateMixin { final size = _textSize(text.data!, text.style); setState(() { if (size.height > size.width) { - textSize = size.height; + textSize = (widget.badgeStyle.padding?.vertical ?? 0) + size.height; } }); } From 8db47c74d9183b81a11f175cc1b1c0e25760a9e9 Mon Sep 17 00:00:00 2001 From: sawel24 Date: Fri, 14 Apr 2023 12:58:19 +0300 Subject: [PATCH 6/8] fix: resize calculation for text when it changes --- lib/src/badge.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/badge.dart b/lib/src/badge.dart index e3789e9..3a16c1b 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -326,6 +326,20 @@ class BadgeState extends State with TickerProviderStateMixin { @override void didUpdateWidget(Badge oldWidget) { super.didUpdateWidget(oldWidget); + if (widget.badgeContent is Text && oldWidget.badgeContent is Text) { + final newText = widget.badgeContent as Text; + final oldText = oldWidget.badgeContent as Text; + final size = _textSize(newText.data!, newText.style); + if (newText.data != oldText.data) { + setState(() { + if (size.height > size.width) { + textSize = (widget.badgeStyle.padding?.vertical ?? 0) + size.height; + } else { + textSize = null; + } + }); + } + } if (widget.badgeAnimation.toAnimate) { if (widget.badgeStyle.badgeColor != oldWidget.badgeStyle.badgeColor && widget.showBadge) { From e026b9b0b4627f806b949c85371ee9dfc3ce573b Mon Sep 17 00:00:00 2001 From: sawel24 Date: Thu, 20 Apr 2023 15:01:45 +0300 Subject: [PATCH 7/8] fix: update display of content of different sizes for different badge shapes, also update unit tests --- example/lib/alarm_app.dart | 1 + lib/src/badge.dart | 129 +++++++++------------ lib/src/utils/calculation_utils.dart | 40 ++++++- test/badges_test.dart | 63 ---------- test/utils_tests.dart | 167 +++++++++++++++++++++++---- 5 files changed, 239 insertions(+), 161 deletions(-) diff --git a/example/lib/alarm_app.dart b/example/lib/alarm_app.dart index c5db1e2..7f29f6f 100644 --- a/example/lib/alarm_app.dart +++ b/example/lib/alarm_app.dart @@ -18,6 +18,7 @@ class _AlarmAppState extends State { Widget build(BuildContext context) { return badges.Badge( badgeStyle: badges.BadgeStyle( + padding: EdgeInsets.zero, borderSide: BorderSide(color: Colors.white, width: 2), shape: badges.BadgeShape.triangle, badgeGradient: badges.BadgeGradient.linear( diff --git a/lib/src/badge.dart b/lib/src/badge.dart index 3a16c1b..fac1946 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -3,6 +3,7 @@ import 'package:badges/src/badge_border_gradient.dart'; import 'package:badges/src/utils/calculation_utils.dart'; import 'package:badges/src/utils/drawing_utils.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; class Badge extends StatefulWidget { const Badge({ @@ -63,21 +64,14 @@ class BadgeState extends State with TickerProviderStateMixin { late Animation _animation; bool enableLoopAnimation = false; double? textSize; + final GlobalKey _key = GlobalKey(); + double? _widgetSize; @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (widget.badgeContent is Text) { - final text = widget.badgeContent as Text; - final size = _textSize(text.data!, text.style); - setState(() { - if (size.height > size.width) { - textSize = (widget.badgeStyle.padding?.vertical ?? 0) + size.height; - } - }); - } - }); + SchedulerBinding.instance + .addPostFrameCallback((_) => scaleWidgetSize(_key, badge: widget)); enableLoopAnimation = widget.badgeAnimation.animationDuration.inMilliseconds > 0; _animationController = AnimationController( @@ -109,13 +103,27 @@ class BadgeState extends State with TickerProviderStateMixin { } } - Size _textSize(String text, TextStyle? style) { - final TextPainter textPainter = TextPainter( - text: TextSpan(text: text, style: style), - maxLines: 1, - textDirection: TextDirection.ltr) - ..layout(minWidth: 0, maxWidth: double.infinity); - return textPainter.size; + void scaleWidgetSize(GlobalKey key, {required Badge badge, Badge? oldBadge}) { + double newSize = 0; + + if (badge.badgeContent is Text) { + final newText = badge.badgeContent as Text; + final size = + CalculationUtils.calculateSizeOfText(newText.data!, newText.style); + newSize = size.width >= size.height + ? size.width * 1.1764 + : size.height * 1.1764; + } else if (badge.badgeContent is Icon) { + newSize = (badge.badgeContent as Icon).size ?? 0; + } else { + final RenderBox? childBox = + _key.currentContext?.findRenderObject() as RenderBox?; + if (childBox != null) { + newSize = childBox.size.height; + } + } + newSize *= badge.badgeStyle.shape == BadgeShape.triangle ? 1.7 : 1; + setState(() => _widgetSize = newSize); } @override @@ -134,7 +142,8 @@ class BadgeState extends State with TickerProviderStateMixin { widget.onTap == null ? widget.child! : Padding( - padding: CalculationUtils.calculatePadding(widget.position), + padding: CalculationUtils.calculatePaddingByPosition( + widget.position), child: widget.child!, ), BadgePositioned( @@ -165,21 +174,6 @@ class BadgeState extends State with TickerProviderStateMixin { return _appearanceController.value; } - EdgeInsets calculateBadgeContentPadding( - Widget? badgeContent, - BadgeShape shape, - ) { - final isTextContent = badgeContent is Text; - final isTriangleShape = shape == BadgeShape.triangle; - if (isTriangleShape) { - return const EdgeInsets.symmetric(horizontal: 10.0); - } else if (isTextContent) { - return const EdgeInsets.symmetric(horizontal: 8.0); - } else { - return const EdgeInsets.symmetric(horizontal: 5.0); - } - } - Widget _getBadge() { final border = widget.badgeStyle.shape == BadgeShape.circle ? CircleBorder( @@ -220,20 +214,17 @@ class BadgeState extends State with TickerProviderStateMixin { borderGradient: widget.badgeStyle.borderGradient, borderSide: widget.badgeStyle.borderSide, ), - child: UnconstrainedBox( - child: IntrinsicWidth( - child: AspectRatio( - aspectRatio: 1.0, - child: Padding( - padding: widget.badgeStyle.padding ?? - calculateBadgeContentPadding( - widget.badgeContent, - widget.badgeStyle.shape, - ), - child: Center( - child: widget.badgeContent, - ), - ), + child: Padding( + padding: CalculationUtils.calculateBadgeContentPadding( + widget.badgeContent, + widget.badgeStyle, + ), + child: SizedBox( + width: _widgetSize, + height: _widgetSize, + child: Center( + key: _key, + child: widget.badgeContent, ), ), ), @@ -264,21 +255,17 @@ class BadgeState extends State with TickerProviderStateMixin { borderRadius: widget.badgeStyle.borderRadius, border: gradientBorder, ), - child: UnconstrainedBox( - child: IntrinsicWidth( - child: AspectRatio( - aspectRatio: isSquareShape ? 1.5 : 1.0, - child: Padding( - padding: widget.badgeStyle.padding ?? - const EdgeInsets.symmetric(horizontal: 5.0), - child: SizedBox( - height: isSquareShape ? textSize : null, - width: isSquareShape ? textSize : null, - child: Center( - child: widget.badgeContent, - ), - ), - ), + child: Padding( + padding: CalculationUtils.calculateBadgeContentPadding( + widget.badgeContent, + widget.badgeStyle, + ), + child: SizedBox( + width: _widgetSize, + height: isSquareShape ? null : _widgetSize, + child: Center( + key: _key, + child: widget.badgeContent, ), ), ), @@ -326,20 +313,8 @@ class BadgeState extends State with TickerProviderStateMixin { @override void didUpdateWidget(Badge oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.badgeContent is Text && oldWidget.badgeContent is Text) { - final newText = widget.badgeContent as Text; - final oldText = oldWidget.badgeContent as Text; - final size = _textSize(newText.data!, newText.style); - if (newText.data != oldText.data) { - setState(() { - if (size.height > size.width) { - textSize = (widget.badgeStyle.padding?.vertical ?? 0) + size.height; - } else { - textSize = null; - } - }); - } - } + SchedulerBinding.instance.addPostFrameCallback( + (_) => scaleWidgetSize(_key, badge: widget, oldBadge: oldWidget)); if (widget.badgeAnimation.toAnimate) { if (widget.badgeStyle.badgeColor != oldWidget.badgeStyle.badgeColor && widget.showBadge) { diff --git a/lib/src/utils/calculation_utils.dart b/lib/src/utils/calculation_utils.dart index b47f259..1e67604 100644 --- a/lib/src/utils/calculation_utils.dart +++ b/lib/src/utils/calculation_utils.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; @@ -26,7 +28,7 @@ class CalculationUtils { /// When the onTap is specified, we need to add some padding /// to make the full badge tappable. - static EdgeInsets calculatePadding(BadgePosition? position) { + static EdgeInsets calculatePaddingByPosition(BadgePosition? position) { if (position == null) { return const EdgeInsets.only(top: 8, right: 10); } @@ -81,4 +83,40 @@ class CalculationUtils { } return Offset(width, height); } + + static Size calculateSizeOfText(String text, TextStyle? style) { + final TextPainter textPainter = TextPainter( + text: TextSpan(text: text, style: style), + maxLines: 1, + textDirection: TextDirection.ltr) + ..layout(minWidth: 0, maxWidth: double.infinity); + return textPainter.size; + } + + static EdgeInsetsGeometry calculateBadgeContentPadding( + Widget? badgeContent, + BadgeStyle style, + ) { + final isTextContent = badgeContent is Text; + final padding = style.padding as EdgeInsets?; + + final isNotCustomShape = + style.shape == BadgeShape.circle || style.shape == BadgeShape.square; + + if (isTextContent && isNotCustomShape) { + return padding ?? EdgeInsets.zero; + } else if (!isNotCustomShape) { + if (padding == null) return const EdgeInsets.all(5); + final top = padding.top; + final bottom = padding.bottom; + final left = padding.left; + final right = padding.right; + final horizontalMax = math.max(left, right); + final verticalMax = math.max(top, bottom); + final maxPadding = math.max(horizontalMax, verticalMax); + + return EdgeInsets.all(maxPadding); + } + return padding ?? const EdgeInsets.all(5); + } } diff --git a/test/badges_test.dart b/test/badges_test.dart index 74c4f4d..6102f9d 100644 --- a/test/badges_test.dart +++ b/test/badges_test.dart @@ -1,7 +1,6 @@ import 'package:badges/badges.dart' as badges; import 'package:badges/src/badge_border_gradient.dart'; import 'package:badges/src/badge_gradient_type.dart'; -import 'package:badges/src/badge_shape.dart'; import 'package:badges/src/painters/instagram_badge_shape_painter.dart'; import 'package:badges/src/painters/triangle_badge_shape_painter.dart'; import 'package:badges/src/painters/twitter_badge_shape_painter.dart'; @@ -19,68 +18,6 @@ import 'test_widget_screen.dart'; import 'utils_tests.dart'; void main() { - group('Unit tests', () { - test('Calculate padding in triangle badge with text', () async { - const badge = badges.Badge( - badgeContent: Text('!'), - badgeStyle: badges.BadgeStyle(shape: BadgeShape.triangle), - ); - final badgeState = badge.createState(); - final edgeInsets = badgeState.calculateBadgeContentPadding( - badge.badgeContent, - badge.badgeStyle.shape, - ); - expect(edgeInsets.top, 0); - expect(edgeInsets.bottom, 0); - expect(edgeInsets.left, 10); - expect(edgeInsets.right, 10); - }); - test('Calculate padding in triangle badge with icon', () async { - const badge = badges.Badge( - badgeContent: Icon(Icons.check, size: 10), - badgeStyle: badges.BadgeStyle(shape: BadgeShape.triangle), - ); - final badgeState = badge.createState(); - final edgeInsets = badgeState.calculateBadgeContentPadding( - badge.badgeContent, - badge.badgeStyle.shape, - ); - expect(edgeInsets.top, 0); - expect(edgeInsets.bottom, 0); - expect(edgeInsets.left, 10); - expect(edgeInsets.right, 10); - }); - test('Calculate padding in instagram badge with icon', () async { - const badge = badges.Badge( - badgeContent: Icon(Icons.check, size: 10), - badgeStyle: badges.BadgeStyle(shape: BadgeShape.instagram), - ); - final badgeState = badge.createState(); - final edgeInsets = badgeState.calculateBadgeContentPadding( - badge.badgeContent, - badge.badgeStyle.shape, - ); - expect(edgeInsets.top, 0); - expect(edgeInsets.bottom, 0); - expect(edgeInsets.left, 5); - expect(edgeInsets.right, 5); - }); - test('Calculate padding in instagram badge with text', () async { - const badge = badges.Badge( - badgeContent: Text('test'), - badgeStyle: badges.BadgeStyle(shape: BadgeShape.instagram), - ); - final badgeState = badge.createState(); - final edgeInsets = badgeState.calculateBadgeContentPadding( - badge.badgeContent, - badge.badgeStyle.shape, - ); - expect(edgeInsets.top, 0); - expect(edgeInsets.bottom, 0); - expect(edgeInsets.left, 8); - expect(edgeInsets.right, 8); - }); - }); group('Badge Position tests', () { Widget getBadge(badges.BadgePosition position) { return badges.Badge( diff --git a/test/utils_tests.dart b/test/utils_tests.dart index 9ad8e98..cdf7440 100644 --- a/test/utils_tests.dart +++ b/test/utils_tests.dart @@ -1,4 +1,4 @@ -import 'package:badges/badges.dart'; +import 'package:badges/badges.dart' as badges; import 'package:badges/src/painters/instagram_badge_shape_painter.dart'; import 'package:badges/src/painters/twitter_badge_shape_painter.dart'; import 'package:badges/src/utils/calculation_utils.dart'; @@ -9,7 +9,8 @@ import 'package:flutter_test/flutter_test.dart'; void testUtils() { group('CalculationUtils', () { test('Passing null', () async { - final BadgePosition position = CalculationUtils.calculatePosition(null); + final badges.BadgePosition position = + CalculationUtils.calculatePosition(null); expect(position.top, 0); expect(position.end, 0); expect(position.start, null); @@ -19,7 +20,8 @@ void testUtils() { test('Null values', () async { final position = CalculationUtils.calculatePosition( - BadgePosition.custom(top: null, end: null, bottom: null, start: null), + badges.BadgePosition.custom( + top: null, end: null, bottom: null, start: null), ); expect(position.top, null); expect(position.end, null); @@ -30,7 +32,8 @@ void testUtils() { test('Negative values', () async { final position = CalculationUtils.calculatePosition( - BadgePosition.custom(top: -10, end: -10, bottom: -10, start: -10)); + badges.BadgePosition.custom( + top: -10, end: -10, bottom: -10, start: -10)); expect(position.top, 0); expect(position.end, 0); expect(position.bottom, 0); @@ -40,7 +43,7 @@ void testUtils() { test('Normal values', () async { final position = CalculationUtils.calculatePosition( - BadgePosition.custom(top: 15, end: 15, bottom: 15, start: 15), + badges.BadgePosition.custom(top: 15, end: 15, bottom: 15, start: 15), ); expect(position.top, 15); expect(position.end, 15); @@ -51,7 +54,7 @@ void testUtils() { group('CalculationUtils.calculatePadding', () { test('Passing null', () async { - final padding = CalculationUtils.calculatePadding(null); + final padding = CalculationUtils.calculatePaddingByPosition(null); expect(padding.top, 8); expect(padding.right, 10); expect(padding.bottom, 0); @@ -59,8 +62,8 @@ void testUtils() { }); test('isCenter = true', () async { - final padding = CalculationUtils.calculatePadding( - BadgePosition.custom(isCenter: true, top: -10, end: 20), + final padding = CalculationUtils.calculatePaddingByPosition( + badges.BadgePosition.custom(isCenter: true, top: -10, end: 20), ); expect(padding.top, 0); expect(padding.right, 0); @@ -69,8 +72,9 @@ void testUtils() { }); test('Null values', () async { - final padding = CalculationUtils.calculatePadding( - BadgePosition.custom(top: null, end: null, bottom: null, start: null), + final padding = CalculationUtils.calculatePaddingByPosition( + badges.BadgePosition.custom( + top: null, end: null, bottom: null, start: null), ); expect(padding.top, 0); expect(padding.left, 0); @@ -79,8 +83,8 @@ void testUtils() { }); test('Top and start values', () async { - final padding = CalculationUtils.calculatePadding( - BadgePosition.custom(top: -5, end: -5, bottom: -5, start: -5), + final padding = CalculationUtils.calculatePaddingByPosition( + badges.BadgePosition.custom(top: -5, end: -5, bottom: -5, start: -5), ); expect(padding.top, 5); expect(padding.left, 5); @@ -90,8 +94,8 @@ void testUtils() { test('Without top and start values and negative end bottom values', () async { - final padding = CalculationUtils.calculatePadding( - BadgePosition.custom(end: -5, bottom: -5), + final padding = CalculationUtils.calculatePaddingByPosition( + badges.BadgePosition.custom(end: -5, bottom: -5), ); expect(padding.top, 0); expect(padding.left, 0); @@ -100,8 +104,8 @@ void testUtils() { }); test('Without top and start values and normal end bottom values', () async { - final padding = CalculationUtils.calculatePadding( - BadgePosition.custom(end: 5, bottom: 5), + final padding = CalculationUtils.calculatePaddingByPosition( + badges.BadgePosition.custom(end: 5, bottom: 5), ); expect(padding.top, 0); expect(padding.left, 0); @@ -180,26 +184,149 @@ void testUtils() { group('DrawingUtils.drawBadgeShape', () { test('Instagram badge shape painter should match', () async { final getCustomPainter = - DrawingUtils.drawBadgeShape(shape: BadgeShape.instagram); + DrawingUtils.drawBadgeShape(shape: badges.BadgeShape.instagram); expect(getCustomPainter.runtimeType, InstagramBadgeShapePainter); }); test('Twitter badge shape painter should match', () async { final getCustomPainter = - DrawingUtils.drawBadgeShape(shape: BadgeShape.twitter); + DrawingUtils.drawBadgeShape(shape: badges.BadgeShape.twitter); expect(getCustomPainter.runtimeType, TwitterBadgeShapePainter); }); test('Circle badge shape painter should be null', () async { final getCustomPainter = - DrawingUtils.drawBadgeShape(shape: BadgeShape.circle); + DrawingUtils.drawBadgeShape(shape: badges.BadgeShape.circle); expect(getCustomPainter, null); }); test('Square badge shape painter should be null', () async { final getCustomPainter = - DrawingUtils.drawBadgeShape(shape: BadgeShape.square); + DrawingUtils.drawBadgeShape(shape: badges.BadgeShape.square); expect(getCustomPainter, null); }); }); + + group('Text size calculation', () { + const text = 'H'; + const textStyle = TextStyle(fontSize: 16, fontWeight: FontWeight.bold); + + test('Returns correct size for custom parameters', () { + final size = CalculationUtils.calculateSizeOfText(text, textStyle); + expect(size.width, 16); + expect(size.height, 16); + }); + + test('Returns correct size for empty text', () { + final size = CalculationUtils.calculateSizeOfText('', textStyle); + expect(size.width, 0); + expect(size.height, 16); + }); + + test('Returns correct size for null style', () { + final size = CalculationUtils.calculateSizeOfText(text, null); + expect(size.width, 14); + expect(size.height, 14); + }); + }); + + group('Content padding calculation', () { + test('Padding in custom badge shape with text and null padding', () async { + const badge = badges.Badge( + badgeContent: Text('!'), + badgeStyle: badges.BadgeStyle(shape: badges.BadgeShape.triangle), + ); + final edgeInsets = CalculationUtils.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle, + ) as EdgeInsets; + expect(edgeInsets.top, 5); + expect(edgeInsets.bottom, 5); + expect(edgeInsets.left, 5); + expect(edgeInsets.right, 5); + }); + test('Padding in custom badge shape with text and padding value', () async { + const badge = badges.Badge( + badgeContent: Icon(Icons.check, size: 10), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.triangle, + padding: EdgeInsets.only(left: 2, right: 4, top: 6, bottom: 8)), + ); + final edgeInsets = CalculationUtils.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle, + ) as EdgeInsets; + expect(edgeInsets.top, 8); + expect(edgeInsets.bottom, 8); + expect(edgeInsets.left, 8); + expect(edgeInsets.right, 8); + }); + + test('Padding in default badge shape with text and null padding', () async { + const badge = badges.Badge( + badgeContent: Text('H'), + badgeStyle: badges.BadgeStyle(shape: badges.BadgeShape.circle), + ); + final edgeInsets = CalculationUtils.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle, + ) as EdgeInsets; + expect(edgeInsets.top, 0); + expect(edgeInsets.bottom, 0); + expect(edgeInsets.left, 0); + expect(edgeInsets.right, 0); + }); + + test('Padding in default badge shape with text and padding value', + () async { + const badge = badges.Badge( + badgeContent: Text('H'), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.circle, + padding: EdgeInsets.only(left: 1, right: 2, top: 3, bottom: 4)), + ); + final edgeInsets = CalculationUtils.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle, + ) as EdgeInsets; + expect(edgeInsets.top, 3); + expect(edgeInsets.bottom, 4); + expect(edgeInsets.left, 1); + expect(edgeInsets.right, 2); + }); + + test('Padding in default badge shape with icon and padding value', + () async { + const badge = badges.Badge( + badgeContent: Icon(Icons.add), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.circle, + padding: EdgeInsets.only(left: 1, right: 2, top: 3, bottom: 4)), + ); + final edgeInsets = CalculationUtils.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle, + ) as EdgeInsets; + expect(edgeInsets.top, 3); + expect(edgeInsets.bottom, 4); + expect(edgeInsets.left, 1); + expect(edgeInsets.right, 2); + }); + + test('Padding in default badge shape with icon and null padding ', + () async { + const badge = badges.Badge( + badgeContent: Icon(Icons.add), + badgeStyle: badges.BadgeStyle(shape: badges.BadgeShape.circle), + ); + final edgeInsets = CalculationUtils.calculateBadgeContentPadding( + badge.badgeContent, + badge.badgeStyle, + ) as EdgeInsets; + expect(edgeInsets.top, 5); + expect(edgeInsets.bottom, 5); + expect(edgeInsets.left, 5); + expect(edgeInsets.right, 5); + }); + }); } From 535a2d78c59923072e35e6a520131b6b8e5649d3 Mon Sep 17 00:00:00 2001 From: sawel24 Date: Fri, 21 Apr 2023 00:33:13 +0300 Subject: [PATCH 8/8] fix: update calculation badge content size of context --- lib/src/badge.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/badge.dart b/lib/src/badge.dart index fac1946..ae69536 100644 --- a/lib/src/badge.dart +++ b/lib/src/badge.dart @@ -119,7 +119,8 @@ class BadgeState extends State with TickerProviderStateMixin { final RenderBox? childBox = _key.currentContext?.findRenderObject() as RenderBox?; if (childBox != null) { - newSize = childBox.size.height; + Size size = childBox.size; + newSize = size.height >= size.width ? size.height : size.width; } } newSize *= badge.badgeStyle.shape == BadgeShape.triangle ? 1.7 : 1;