From 8c3d283b8b1ce74e44f5276e839ba87e1c17f738 Mon Sep 17 00:00:00 2001 From: Gabriel Terwesten Date: Sun, 21 May 2023 19:37:19 +0200 Subject: [PATCH] feat: add extension-based widget API (#41) --- packages/fleet/example/lib/hike_graph.dart | 93 +++----- .../example/lib/interactive_box_fleet.dart | 20 +- .../example/lib/interactive_box_flutter.dart | 20 +- .../lib/simple_declarative_animation.dart | 29 +-- .../lib/simple_imperative_animation.dart | 44 ++-- packages/fleet/example/lib/stack.dart | 30 +-- packages/fleet/lib/fleet.dart | 1 + packages/fleet/lib/src/widget_extension.dart | 202 ++++++++++++++++++ 8 files changed, 287 insertions(+), 152 deletions(-) create mode 100644 packages/fleet/lib/src/widget_extension.dart diff --git a/packages/fleet/example/lib/hike_graph.dart b/packages/fleet/example/lib/hike_graph.dart index 68b09d5..5b02ac4 100644 --- a/packages/fleet/example/lib/hike_graph.dart +++ b/packages/fleet/example/lib/hike_graph.dart @@ -30,40 +30,34 @@ class _PageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SegmentedButton( - segments: const [ - ButtonSegment( - value: Observation.heartRate, - label: Text('Heart Rate'), - ), - ButtonSegment( - value: Observation.pace, - label: Text('Pace'), - ), - ], - selected: {_observation}, - onSelectionChanged: (selection) { - setState(() { - _observation = selection.single; - }); - }, - ), - const SizedBox(height: 10), - SizedBox( - width: 400, - height: 200, - child: HikeGraph( - hike: hike, - observation: _observation, + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SegmentedButton( + segments: const [ + ButtonSegment( + value: Observation.heartRate, + label: Text('Heart Rate'), ), - ) - ], - ), - ), + ButtonSegment( + value: Observation.pace, + label: Text('Pace'), + ), + ], + selected: {_observation}, + onSelectionChanged: (selection) { + setState(() { + _observation = selection.single; + }); + }, + ), + const SizedBox(height: 10), + HikeGraph( + hike: hike, + observation: _observation, + ).size(width: 400, height: 200) + ], + ).center(), ); } } @@ -72,15 +66,6 @@ AnimationSpec ripple(int index) { return Curves.bounceOut.animation().delay(Duration(milliseconds: 30 * index)); } -extension on Widget { - Widget animation(AnimationSpec animation) { - return Animated( - animation: animation, - child: this, - ); - } -} - class HikeGraph extends StatelessWidget { const HikeGraph({ super.key, @@ -144,22 +129,14 @@ class GraphCapsule extends StatelessWidget { @override Widget build(BuildContext context) { final relativeRange = range.relativeTo(overallRange); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - AContainer( - height: height * relativeRange.magnitude, - width: 24, - decoration: ShapeDecoration( - shape: const StadiumBorder(), - color: color, - ), - ), - ASizedBox( - height: height * relativeRange.min, - ) - ], - ); + return AContainer( + height: height * relativeRange.magnitude, + width: 24, + decoration: ShapeDecoration( + shape: const StadiumBorder(), + color: color, + ), + ).translate(Offset(0, -height * relativeRange.min)); } } diff --git a/packages/fleet/example/lib/interactive_box_fleet.dart b/packages/fleet/example/lib/interactive_box_fleet.dart index a75012b..d101596 100644 --- a/packages/fleet/example/lib/interactive_box_fleet.dart +++ b/packages/fleet/example/lib/interactive_box_fleet.dart @@ -52,21 +52,11 @@ class _MyHomePageState extends State with AnimatingStateMixin { _color = _distanceColorTween.begin!; }); }, - child: AAlign( - alignment: _alignment, - child: SizedBox.square( - dimension: 400, - child: AColoredBox( - color: _color, - child: const Center( - child: Text( - 'Drag me!', - style: TextStyle(color: Colors.white), - ), - ), - ), - ), - ), + child: const Text('Drag me!', style: TextStyle(color: Colors.white)) + .center() + .boxColor(_color) + .square(400) + .align(_alignment), ), ); } diff --git a/packages/fleet/example/lib/interactive_box_flutter.dart b/packages/fleet/example/lib/interactive_box_flutter.dart index ebd82a8..d61636b 100644 --- a/packages/fleet/example/lib/interactive_box_flutter.dart +++ b/packages/fleet/example/lib/interactive_box_flutter.dart @@ -81,21 +81,11 @@ class _MyHomePageState extends State _colorTween.begin = _color; _controller.forward(from: 0); }, - child: Align( - alignment: _alignment, - child: SizedBox.square( - dimension: 400, - child: ColoredBox( - color: _color, - child: const Center( - child: Text( - 'Drag me!', - style: TextStyle(color: Colors.white), - ), - ), - ), - ), - ), + child: const Text('Drag me!', style: TextStyle(color: Colors.white)) + .center() + .boxColor(_color) + .square(400) + .align(_alignment), ), ); } diff --git a/packages/fleet/example/lib/simple_declarative_animation.dart b/packages/fleet/example/lib/simple_declarative_animation.dart index e94af9a..8d30f6b 100644 --- a/packages/fleet/example/lib/simple_declarative_animation.dart +++ b/packages/fleet/example/lib/simple_declarative_animation.dart @@ -30,25 +30,16 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Animated( - animation: Curves.ease.animation(1.s), - value: _expanded, - child: ASizedBox.fromSize( - size: _expanded ? const Size.square(400) : const Size.square(200), - child: AColoredBox( - color: _expanded ? Colors.green : Colors.blue, - child: Center( - child: TextButton( - onPressed: () => setState(() => _expanded = !_expanded), - style: TextButton.styleFrom(foregroundColor: Colors.white), - child: const Text('Toggle'), - ), - ), - ), - ), - ), - ), + body: TextButton( + onPressed: () => setState(() => _expanded = !_expanded), + style: TextButton.styleFrom(foregroundColor: Colors.white), + child: const Text('Toggle'), + ) + .center() + .boxColor(_expanded ? Colors.green : Colors.blue) + .sizeWith(_expanded ? const Size.square(400) : const Size.square(200)) + .animation(Curves.ease.animation(1.s), value: _expanded) + .center(), ); } } diff --git a/packages/fleet/example/lib/simple_imperative_animation.dart b/packages/fleet/example/lib/simple_imperative_animation.dart index d904c31..cf62cde 100644 --- a/packages/fleet/example/lib/simple_imperative_animation.dart +++ b/packages/fleet/example/lib/simple_imperative_animation.dart @@ -49,32 +49,26 @@ class _MyHomePageState extends State with AnimatingStateMixin { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: ASizedBox.fromSize( - size: _size, - child: AColoredBox( - color: _color, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: _collapse, - style: TextButton.styleFrom(foregroundColor: Colors.white), - child: const Text('Collapsed'), - ), - const SizedBox(height: 16), - TextButton( - style: TextButton.styleFrom(foregroundColor: Colors.white), - onPressed: _expand, - child: const Text('Expanded'), - ), - ], - ), - ), + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: _collapse, + style: TextButton.styleFrom(foregroundColor: Colors.white), + child: const Text('Collapsed'), ), - ), - ), + const SizedBox(height: 16), + TextButton( + style: TextButton.styleFrom(foregroundColor: Colors.white), + onPressed: _expand, + child: const Text('Expanded'), + ), + ], + ) // + .center() + .boxColor(_color) + .sizeWith(_size) + .center(), ); } } diff --git a/packages/fleet/example/lib/stack.dart b/packages/fleet/example/lib/stack.dart index 4e36536..2c3aa13 100644 --- a/packages/fleet/example/lib/stack.dart +++ b/packages/fleet/example/lib/stack.dart @@ -61,25 +61,15 @@ class StackElement extends StatelessWidget { @override Widget build(BuildContext context) { final dimension = 500.0 - (index * 70); - return Animated( - animation: _buildAnimation(), - value: left, - child: AAlign( - alignment: Alignment(left ? -.5 : .5, 0), - child: SizedBox.square( - dimension: 500, - child: Center( - child: SizedBox.square( - dimension: dimension, - child: Material( - elevation: 50, - color: _buildColor(), - borderRadius: BorderRadius.circular(dimension / 20), - ), - ), - ), - ), - ), - ); + return Material( + elevation: 50, + color: _buildColor(), + borderRadius: BorderRadius.circular(dimension / 20), + ) + .square(dimension) + .center() + .square(500) + .align(Alignment(left ? -.5 : .5, 0)) + .animation(_buildAnimation(), value: left); } } diff --git a/packages/fleet/lib/fleet.dart b/packages/fleet/lib/fleet.dart index 482444f..824eef2 100644 --- a/packages/fleet/lib/fleet.dart +++ b/packages/fleet/lib/fleet.dart @@ -42,3 +42,4 @@ export 'src/animate.dart' show Animated, withAnimation, AnimatingStateMixin; export 'src/animation.dart' show AnimationSpec, AnimationFromCurveExtension; export 'src/common.dart' show Block; export 'src/duration.dart' show DurationFromIntExtension; +export 'src/widget_extension.dart' show FleetWidgetExtension; diff --git a/packages/fleet/lib/src/widget_extension.dart b/packages/fleet/lib/src/widget_extension.dart new file mode 100644 index 0000000..2932577 --- /dev/null +++ b/packages/fleet/lib/src/widget_extension.dart @@ -0,0 +1,202 @@ +import 'package:flutter/widgets.dart'; + +import 'animatable_flutter_widgets.dart'; +import 'animate.dart'; +import 'animation.dart'; + +/// Extension-based API for widgets provided by Fleet. +extension FleetWidgetExtension on Widget { + /// Applies an [animation] to state changes in the descendants of this widget. + /// + /// See also: + /// + /// - [Animated] for the widget that implements this functionality. + @widgetFactory + Widget animation( + AnimationSpec animation, { + Object? value = Animated.alwaysAnimateValue, + }) { + return Animated( + animation: animation, + value: value, + child: this, + ); + } + + /// Aligns this widget within the available space. + @widgetFactory + Widget align( + AlignmentGeometry alignment, { + double? widthFactor, + double? heightFactor, + }) { + return AAlign( + alignment: alignment, + widthFactor: widthFactor, + heightFactor: heightFactor, + child: this, + ); + } + + /// Centers this widget within the available space + @widgetFactory + Widget center({ + double? widthFactor, + double? heightFactor, + }) { + return AAlign( + widthFactor: widthFactor, + heightFactor: heightFactor, + child: this, + ); + } + + /// Sizes this widget to the given [width] and [height]. + @widgetFactory + Widget size({double? width, double? height}) { + return ASizedBox( + width: width, + height: height, + child: this, + ); + } + + /// Sizes this widget to the given [Size]. + @widgetFactory + Widget sizeWith(Size size) { + return ASizedBox.fromSize( + size: size, + child: this, + ); + } + + /// Sizes this widget to a square with the given [dimension]. + @widgetFactory + Widget square(double dimension) { + return ASizedBox.square( + dimension: dimension, + child: this, + ); + } + + /// Adds [padding] around this widget. + @widgetFactory + Widget padding(EdgeInsets padding) { + return APadding( + padding: padding, + child: this, + ); + } + + /// Applies opacity to this widget. + @widgetFactory + Widget opacity(double opacity, {bool alwaysIncludeSemantics = false}) { + return AOpacity( + opacity: opacity, + alwaysIncludeSemantics: alwaysIncludeSemantics, + child: this, + ); + } + + /// Paints the area of this widget. + @widgetFactory + Widget boxColor(Color color) { + return AColoredBox( + color: color, + child: this, + ); + } + + /// Transforms this widget using a [Matrix4]. + @widgetFactory + Widget transform( + Matrix4 transform, { + Offset? origin, + AlignmentGeometry? alignment, + bool transformHitTests = true, + FilterQuality? filterQuality, + }) { + return ATransform( + transform: transform, + origin: origin, + alignment: alignment, + transformHitTests: transformHitTests, + filterQuality: filterQuality, + child: this, + ); + } + + /// Rotates this widget by [angle] radians. + @widgetFactory + Widget rotate( + double angle, { + Offset? origin, + AlignmentGeometry? alignment, + bool transformHitTests = true, + FilterQuality? filterQuality, + }) { + return ATransform.rotate( + angle: angle, + origin: origin, + alignment: alignment, + transformHitTests: transformHitTests, + filterQuality: filterQuality, + child: this, + ); + } + + /// Translates this widget by [offset]. + @widgetFactory + Widget translate( + Offset offset, { + bool transformHitTests = true, + FilterQuality? filterQuality, + }) { + return ATransform.translate( + offset: offset, + transformHitTests: transformHitTests, + filterQuality: filterQuality, + child: this, + ); + } + + /// Scales this widget by [scale]. + @widgetFactory + Widget scale( + double scale, { + Offset? origin, + AlignmentGeometry? alignment, + bool transformHitTests = true, + FilterQuality? filterQuality, + }) { + return ATransform.scale( + scale: scale, + origin: origin, + alignment: alignment, + transformHitTests: transformHitTests, + filterQuality: filterQuality, + child: this, + ); + } + + /// Scales this widget individually along the x and y axes. + @widgetFactory + Widget scaleXY({ + double? x, + double? y, + Offset? origin, + AlignmentGeometry? alignment, + bool transformHitTests = true, + FilterQuality? filterQuality, + }) { + return ATransform.scale( + scaleX: x, + scaleY: y, + origin: origin, + alignment: alignment, + transformHitTests: transformHitTests, + filterQuality: filterQuality, + child: this, + ); + } +}