From 69ab6cd5cbb5d2ed6a8fdbde92b8389bec5c6657 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Sun, 22 Mar 2026 18:19:58 -0300 Subject: [PATCH 1/4] feat: Adding size to the sprite width and sprite animation widget --- .../lib/src/widgets/animation_widget.dart | 15 ++- .../flame/lib/src/widgets/sprite_widget.dart | 14 ++- .../widgets/sprite_animation_widget_test.dart | 91 +++++++++++++++++++ .../test/widgets/sprite_widget_test.dart | 71 +++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) diff --git a/packages/flame/lib/src/widgets/animation_widget.dart b/packages/flame/lib/src/widgets/animation_widget.dart index 7f584ccc58c..def13865ee7 100644 --- a/packages/flame/lib/src/widgets/animation_widget.dart +++ b/packages/flame/lib/src/widgets/animation_widget.dart @@ -34,6 +34,10 @@ class SpriteAnimationWidget extends StatefulWidget { /// When omitted the default paint from the [Sprite] class will be used. final Paint? paint; + /// The size of the widget. If null, the size will be derived from the + /// animation. + final Size? size; + const SpriteAnimationWidget({ required SpriteAnimation animation, required SpriteAnimationTicker animationTicker, @@ -43,6 +47,7 @@ class SpriteAnimationWidget extends StatefulWidget { this.loadingBuilder, this.onComplete, this.paint, + this.size, super.key, }) : _animationFuture = animation, _animationTicker = animationTicker; @@ -63,6 +68,7 @@ class SpriteAnimationWidget extends StatefulWidget { this.loadingBuilder, this.onComplete, this.paint, + this.size, String? package, super.key, }) : _animationFuture = SpriteAnimation.load( @@ -132,13 +138,20 @@ class _SpriteAnimationWidgetState extends State { final ticker = _animationTicker ?? spriteAnimation.createTicker(); ticker.completed.then((_) => widget.onComplete?.call()); - return InternalSpriteAnimationWidget( + final internalWidget = InternalSpriteAnimationWidget( animation: spriteAnimation, animationTicker: ticker, anchor: widget.anchor, playing: widget.playing, paint: widget.paint, ); + if (widget.size != null) { + return SizedBox.fromSize( + size: widget.size, + child: internalWidget, + ); + } + return internalWidget; }, errorBuilder: widget.errorBuilder, loadingBuilder: widget.loadingBuilder, diff --git a/packages/flame/lib/src/widgets/sprite_widget.dart b/packages/flame/lib/src/widgets/sprite_widget.dart index 2c3a3172d1c..db123c9d8d4 100644 --- a/packages/flame/lib/src/widgets/sprite_widget.dart +++ b/packages/flame/lib/src/widgets/sprite_widget.dart @@ -32,6 +32,9 @@ class SpriteWidget extends StatefulWidget { /// If the Sprite should be rasterized or not. final bool rasterize; + /// The size of the widget. If null, the size will be derived from the sprite. + final Size? size; + final FutureOr _spriteFuture; /// renders the [sprite] as a Widget. @@ -45,6 +48,7 @@ class SpriteWidget extends StatefulWidget { this.loadingBuilder, this.paint, this.rasterize = false, + this.size, super.key, }) : _spriteFuture = sprite; @@ -65,6 +69,7 @@ class SpriteWidget extends StatefulWidget { this.loadingBuilder, this.paint, this.rasterize = false, + this.size, String? package, super.key, }) : _spriteFuture = Sprite.load( @@ -141,12 +146,19 @@ class _SpriteWidgetState extends State { return BaseFutureBuilder( future: _spriteFuture, builder: (_, sprite) { - return InternalSpriteWidget( + final internalWidget = InternalSpriteWidget( sprite: sprite, anchor: widget.anchor, angle: widget.angle, paint: widget.paint, ); + if (widget.size != null) { + return SizedBox.fromSize( + size: widget.size, + child: internalWidget, + ); + } + return internalWidget; }, errorBuilder: widget.errorBuilder, loadingBuilder: widget.loadingBuilder, diff --git a/packages/flame/test/widgets/sprite_animation_widget_test.dart b/packages/flame/test/widgets/sprite_animation_widget_test.dart index 94ce450141a..0579801661a 100644 --- a/packages/flame/test/widgets/sprite_animation_widget_test.dart +++ b/packages/flame/test/widgets/sprite_animation_widget_test.dart @@ -528,5 +528,96 @@ Future main() async { }); }); }); + + group('size parameter', () { + testWidgets('when null, does not wrap in SizedBox', (tester) async { + final sprite1 = Sprite(image); + final sprite2 = Sprite(image); + final spriteAnimation = SpriteAnimation.spriteList( + [sprite1, sprite2], + stepTime: 0.1, + ); + + await tester.pumpWidget( + SpriteAnimationWidget( + animation: spriteAnimation, + animationTicker: spriteAnimation.createTicker(), + ), + ); + + // Check that InternalSpriteAnimationWidget is not wrapped in a SizedBox + final internalWidgetFinder = find.byType(InternalSpriteAnimationWidget); + final sizedBoxAncestor = find.ancestor( + of: internalWidgetFinder, + matching: find.byType(SizedBox), + ); + expect(sizedBoxAncestor, findsNothing); + }); + + testWidgets('when provided, wraps in SizedBox with correct size', ( + tester, + ) async { + final sprite1 = Sprite(image); + final sprite2 = Sprite(image); + final spriteAnimation = SpriteAnimation.spriteList( + [sprite1, sprite2], + stepTime: 0.1, + ); + const customSize = Size(100, 200); + + await tester.pumpWidget( + SpriteAnimationWidget( + animation: spriteAnimation, + animationTicker: spriteAnimation.createTicker(), + size: customSize, + ), + ); + + // Find SizedBox that is an ancestor of InternalSpriteAnimationWidget + final internalWidgetFinder = find.byType(InternalSpriteAnimationWidget); + final sizedBoxFinder = find.ancestor( + of: internalWidgetFinder, + matching: find.byType(SizedBox), + ); + expect(sizedBoxFinder, findsOneWidget); + + final sizedBox = tester.widget(sizedBoxFinder); + expect(sizedBox.width, customSize.width); + expect(sizedBox.height, customSize.height); + }); + + testWidgets('asset constructor respects size parameter', (tester) async { + const imagePath = 'test_path_size_animation'; + Flame.images.add(imagePath, image); + const customSize = Size(150, 250); + final spriteAnimationData = SpriteAnimationData.sequenced( + amount: 2, + stepTime: 1, + textureSize: Vector2(10, 10), + ); + + await tester.pumpWidget( + SpriteAnimationWidget.asset( + path: imagePath, + data: spriteAnimationData, + size: customSize, + ), + ); + + await tester.pump(); + + // Find SizedBox that is an ancestor of InternalSpriteAnimationWidget + final internalWidgetFinder = find.byType(InternalSpriteAnimationWidget); + final sizedBoxFinder = find.ancestor( + of: internalWidgetFinder, + matching: find.byType(SizedBox), + ); + expect(sizedBoxFinder, findsOneWidget); + + final sizedBox = tester.widget(sizedBoxFinder); + expect(sizedBox.width, customSize.width); + expect(sizedBox.height, customSize.height); + }); + }); }); } diff --git a/packages/flame/test/widgets/sprite_widget_test.dart b/packages/flame/test/widgets/sprite_widget_test.dart index ccdb7f22e60..2aaf7d97a5e 100644 --- a/packages/flame/test/widgets/sprite_widget_test.dart +++ b/packages/flame/test/widgets/sprite_widget_test.dart @@ -109,5 +109,76 @@ Future main() async { expect(internalSpriteWidgetFinder.sprite.srcPosition, Vector2(10, 10)); }); }); + + group('size parameter', () { + testWidgets('when null, does not wrap in SizedBox', (tester) async { + final sprite = Sprite(image); + + await tester.pumpWidget(SpriteWidget(sprite: sprite)); + await tester.pump(); + + // Check that InternalSpriteWidget is not wrapped in a SizedBox + final internalWidgetFinder = find.byType(InternalSpriteWidget); + final sizedBoxAncestor = find.ancestor( + of: internalWidgetFinder, + matching: find.byType(SizedBox), + ); + expect(sizedBoxAncestor, findsNothing); + }); + + testWidgets('when provided, wraps in SizedBox with correct size', ( + tester, + ) async { + final sprite = Sprite(image); + const customSize = Size(100, 200); + + await tester.pumpWidget( + SpriteWidget( + sprite: sprite, + size: customSize, + ), + ); + await tester.pump(); + + // Find SizedBox that is an ancestor of InternalSpriteWidget + final internalWidgetFinder = find.byType(InternalSpriteWidget); + final sizedBoxFinder = find.ancestor( + of: internalWidgetFinder, + matching: find.byType(SizedBox), + ); + expect(sizedBoxFinder, findsOneWidget); + + final sizedBox = tester.widget(sizedBoxFinder); + expect(sizedBox.width, customSize.width); + expect(sizedBox.height, customSize.height); + }); + + testWidgets('asset constructor respects size parameter', (tester) async { + const imagePath = 'test_path_size'; + Flame.images.add(imagePath, image); + const customSize = Size(150, 250); + + await tester.pumpWidget( + SpriteWidget.asset( + path: imagePath, + size: customSize, + ), + ); + + await tester.pump(); + + // Find SizedBox that is an ancestor of InternalSpriteWidget + final internalWidgetFinder = find.byType(InternalSpriteWidget); + final sizedBoxFinder = find.ancestor( + of: internalWidgetFinder, + matching: find.byType(SizedBox), + ); + expect(sizedBoxFinder, findsOneWidget); + + final sizedBox = tester.widget(sizedBoxFinder); + expect(sizedBox.width, customSize.width); + expect(sizedBox.height, customSize.height); + }); + }); }); } From 727aab900fa408cbb460a398e016d6230c86e01c Mon Sep 17 00:00:00 2001 From: Erick Date: Sun, 22 Mar 2026 18:22:20 -0300 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Erick --- packages/flame/lib/src/widgets/animation_widget.dart | 3 +-- packages/flame/lib/src/widgets/sprite_widget.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/flame/lib/src/widgets/animation_widget.dart b/packages/flame/lib/src/widgets/animation_widget.dart index def13865ee7..10d77fbbac2 100644 --- a/packages/flame/lib/src/widgets/animation_widget.dart +++ b/packages/flame/lib/src/widgets/animation_widget.dart @@ -34,8 +34,7 @@ class SpriteAnimationWidget extends StatefulWidget { /// When omitted the default paint from the [Sprite] class will be used. final Paint? paint; - /// The size of the widget. If null, the size will be derived from the - /// animation. + /// The size of the widget. If null, the widget will fill the available space. final Size? size; const SpriteAnimationWidget({ diff --git a/packages/flame/lib/src/widgets/sprite_widget.dart b/packages/flame/lib/src/widgets/sprite_widget.dart index db123c9d8d4..a017c009e29 100644 --- a/packages/flame/lib/src/widgets/sprite_widget.dart +++ b/packages/flame/lib/src/widgets/sprite_widget.dart @@ -32,7 +32,7 @@ class SpriteWidget extends StatefulWidget { /// If the Sprite should be rasterized or not. final bool rasterize; - /// The size of the widget. If null, the size will be derived from the sprite. + /// The size of the widget. If null, the widget will fill the available space. final Size? size; final FutureOr _spriteFuture; From 0cc92f0bcf9a8c5bea201925c18d0258e245ed16 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Sun, 22 Mar 2026 20:52:35 -0300 Subject: [PATCH 3/4] adding examples --- .../sprite_animation_widget_example.dart | 29 ++++++++++++++++++- .../widgets/sprite_widget_example.dart | 25 ++++++++++++++++ examples/lib/stories/widgets/widgets.dart | 18 ++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/examples/lib/stories/widgets/sprite_animation_widget_example.dart b/examples/lib/stories/widgets/sprite_animation_widget_example.dart index ed1e4e66571..cb2f55c9313 100644 --- a/examples/lib/stories/widgets/sprite_animation_widget_example.dart +++ b/examples/lib/stories/widgets/sprite_animation_widget_example.dart @@ -7,7 +7,7 @@ import 'package:flutter/widgets.dart'; final anchorOptions = Anchor.values.map((e) => e.name).toList(); Widget spriteAnimationWidgetBuilder(DashbookContext ctx) { - return Container( + return SizedBox( width: ctx.numberProperty('container width', 400), height: ctx.numberProperty('container height', 200), child: SpriteAnimationWidget.asset( @@ -32,3 +32,30 @@ Widget spriteAnimationWidgetBuilder(DashbookContext ctx) { ), ); } + +Widget spriteAnimationWithSizeWidgetBuilder(DashbookContext ctx) { + return SpriteAnimationWidget.asset( + size: Size( + ctx.numberProperty('width', 400), + ctx.numberProperty('height', 200), + ), + path: 'bomb_ptero.png', + data: SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.2, + textureSize: Vector2(48, 32), + ), + playing: ctx.boolProperty('playing', true), + anchor: Anchor.valueOf( + ctx.listProperty('anchor', 'center', anchorOptions), + ), + paint: + paintList[paintChoices.indexOf( + ctx.listProperty( + 'paint', + 'none', + paintChoices, + ), + )], + ); +} diff --git a/examples/lib/stories/widgets/sprite_widget_example.dart b/examples/lib/stories/widgets/sprite_widget_example.dart index b962882d027..5ce1e04d185 100644 --- a/examples/lib/stories/widgets/sprite_widget_example.dart +++ b/examples/lib/stories/widgets/sprite_widget_example.dart @@ -29,3 +29,28 @@ Widget spriteWidgetBuilder(DashbookContext ctx) { ), ); } + +Widget spriteWidgetWithSizeBuilder(DashbookContext ctx) { + return DecoratedBox( + decoration: BoxDecoration(border: Border.all(color: Colors.amber)), + child: SpriteWidget.asset( + size: Size( + ctx.numberProperty('width', 400), + ctx.numberProperty('height', 200), + ), + path: 'shield.png', + angle: pi / 180 * ctx.numberProperty('angle (deg)', 0), + anchor: Anchor.valueOf( + ctx.listProperty('anchor', 'center', anchorOptions), + ), + paint: + paintList[paintChoices.indexOf( + ctx.listProperty( + 'paint', + 'none', + paintChoices, + ), + )], + ), + ); +} diff --git a/examples/lib/stories/widgets/widgets.dart b/examples/lib/stories/widgets/widgets.dart index 547eec053f1..e4c995042d5 100644 --- a/examples/lib/stories/widgets/widgets.dart +++ b/examples/lib/stories/widgets/widgets.dart @@ -51,6 +51,15 @@ void addWidgetsStories(Dashbook dashbook) { on the pen icon. ''', ) + ..add( + 'Sprite Widget (with size)', + spriteWidgetWithSizeBuilder, + codeLink: baseLink('widgets/sprite_widget_example.dart'), + info: ''' + Similar as the default example, but using a fixed size directly in the + widget. + ''', + ) ..add( 'Sprite Widget (section of image)', partialSpriteWidgetBuilder, @@ -71,6 +80,15 @@ void addWidgetsStories(Dashbook dashbook) { the settings on the pen icon. ''', ) + ..add( + 'Sprite Animation Widget (with size)', + spriteAnimationWithSizeWidgetBuilder, + codeLink: baseLink('widgets/sprite_animation_widget_example.dart'), + info: ''' + Similar as the default example, but using a fixed size directly in the + widget. + ''', + ) ..add( 'CustomPainterComponent', customPainterBuilder, From dcdcc96ed2c5c1df89a849529b4f7ac3fd082cfb Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 23 Mar 2026 13:54:26 -0300 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Lukas Klingsbo Co-authored-by: Erick --- examples/lib/stories/widgets/widgets.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/lib/stories/widgets/widgets.dart b/examples/lib/stories/widgets/widgets.dart index e4c995042d5..18fc651882b 100644 --- a/examples/lib/stories/widgets/widgets.dart +++ b/examples/lib/stories/widgets/widgets.dart @@ -56,8 +56,7 @@ void addWidgetsStories(Dashbook dashbook) { spriteWidgetWithSizeBuilder, codeLink: baseLink('widgets/sprite_widget_example.dart'), info: ''' - Similar as the default example, but using a fixed size directly in the - widget. +Similar as the default example, but using a fixed size directly in the widget. ''', ) ..add( @@ -85,8 +84,7 @@ void addWidgetsStories(Dashbook dashbook) { spriteAnimationWithSizeWidgetBuilder, codeLink: baseLink('widgets/sprite_animation_widget_example.dart'), info: ''' - Similar as the default example, but using a fixed size directly in the - widget. +Similar as the default example, but using a fixed size directly in the widget. ''', ) ..add(