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..18fc651882b 100644 --- a/examples/lib/stories/widgets/widgets.dart +++ b/examples/lib/stories/widgets/widgets.dart @@ -51,6 +51,14 @@ 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 +79,14 @@ 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, diff --git a/packages/flame/lib/src/widgets/animation_widget.dart b/packages/flame/lib/src/widgets/animation_widget.dart index 7f584ccc58c..10d77fbbac2 100644 --- a/packages/flame/lib/src/widgets/animation_widget.dart +++ b/packages/flame/lib/src/widgets/animation_widget.dart @@ -34,6 +34,9 @@ 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 widget will fill the available space. + final Size? size; + const SpriteAnimationWidget({ required SpriteAnimation animation, required SpriteAnimationTicker animationTicker, @@ -43,6 +46,7 @@ class SpriteAnimationWidget extends StatefulWidget { this.loadingBuilder, this.onComplete, this.paint, + this.size, super.key, }) : _animationFuture = animation, _animationTicker = animationTicker; @@ -63,6 +67,7 @@ class SpriteAnimationWidget extends StatefulWidget { this.loadingBuilder, this.onComplete, this.paint, + this.size, String? package, super.key, }) : _animationFuture = SpriteAnimation.load( @@ -132,13 +137,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..a017c009e29 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 widget will fill the available space. + 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); + }); + }); }); }