Animated fractionally sized box (#106795)
diff --git a/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart b/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart
new file mode 100644
index 0000000..2595cfb
--- /dev/null
+++ b/examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart
@@ -0,0 +1,68 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flutter code sample for AnimatedFractionallySizedBox
+
+import 'package:flutter/material.dart';
+
+void main() => runApp(const MyApp());
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ static const String _title = 'Flutter Code Sample';
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: _title,
+ home: Scaffold(
+ appBar: AppBar(title: const Text(_title)),
+ body: const MyStatefulWidget(),
+ ),
+ );
+ }
+}
+
+class MyStatefulWidget extends StatefulWidget {
+ const MyStatefulWidget({super.key});
+
+ @override
+ State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
+}
+
+class _MyStatefulWidgetState extends State<MyStatefulWidget> {
+ bool selected = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () {
+ setState(() {
+ selected = !selected;
+ });
+ },
+ child: Center(
+ child: SizedBox(
+ width: 200,
+ height: 200,
+ child: Container(
+ color: Colors.red,
+ child: AnimatedFractionallySizedBox(
+ widthFactor: selected ? 0.25 : 0.75,
+ heightFactor: selected ? 0.75 : 0.25,
+ alignment: selected ? Alignment.topLeft : Alignment.bottomRight,
+ duration: const Duration(seconds: 1),
+ curve: Curves.fastOutSlowIn,
+ child: Container(
+ color: Colors.blue,
+ child: const FlutterLogo(size: 75),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 0693e16..c14f7ca 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -2798,6 +2798,7 @@
assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0);
+ /// {@template flutter.widgets.basic.fractionallySizedBox.widthFactor}
/// If non-null, the fraction of the incoming width given to the child.
///
/// If non-null, the child is given a tight width constraint that is the max
@@ -2805,8 +2806,10 @@
///
/// If null, the incoming width constraints are passed to the child
/// unmodified.
+ /// {@endtemplate}
final double? widthFactor;
+ /// {@template flutter.widgets.basic.fractionallySizedBox.heightFactor}
/// If non-null, the fraction of the incoming height given to the child.
///
/// If non-null, the child is given a tight height constraint that is the max
@@ -2814,8 +2817,10 @@
///
/// If null, the incoming height constraints are passed to the child
/// unmodified.
+ /// {@endtemplate}
final double? heightFactor;
+ /// {@template flutter.widgets.basic.fractionallySizedBox.alignment}
/// How to align the child.
///
/// The x and y values of the alignment control the horizontal and vertical
@@ -2834,6 +2839,7 @@
/// specify an [AlignmentGeometry].
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
/// relative to text direction.
+ /// {@endtemplate}
final AlignmentGeometry alignment;
@override
diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart
index 98653ec..522ca05 100644
--- a/packages/flutter/lib/src/widgets/implicit_animations.dart
+++ b/packages/flutter/lib/src/widgets/implicit_animations.dart
@@ -2108,3 +2108,110 @@
);
}
}
+
+/// Animated version of [FractionallySizedBox] which automatically transitions the
+/// child's size over a given duration whenever the given [widthFactor] or
+/// [heightFactor] changes, as well as the position whenever the given [alignment]
+/// changes.
+///
+/// For the animation, you can choose a [curve] as well as a [duration] and the
+/// widget will automatically animate to the new target [widthFactor] or
+/// [heightFactor].
+///
+/// {@tool dartpad}
+/// The following example transitions an [AnimatedFractionallySizedBox]
+/// between two states. It adjusts the [heightFactor], [widthFactor], and
+/// [alignment] properties when tapped, using a [curve] of [Curves.fastOutSlowIn]
+///
+/// ** See code in examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart **
+/// {@end-tool}
+///
+/// See also:
+///
+/// * [AnimatedAlign], which is an implicitly animated version of [Align].
+/// * [AnimatedContainer], which can transition more values at once.
+/// * [AnimatedSlide], which can animate the translation of child by a given offset relative to its size.
+/// * [AnimatedPositioned], which, as a child of a [Stack], automatically
+/// transitions its child's position over a given duration whenever the given
+/// position changes.
+class AnimatedFractionallySizedBox extends ImplicitlyAnimatedWidget {
+ /// Creates a widget that sizes its child to a fraction of the total available
+ /// space that animates implicitly, and positions its child by an alignment
+ /// that animates implicitly.
+ ///
+ /// The [curve] and [duration] argument must not be null
+ /// If non-null, the [widthFactor] and [heightFactor] arguments must be
+ /// non-negative.
+ const AnimatedFractionallySizedBox({
+ super.key,
+ this.alignment = Alignment.center,
+ this.child,
+ this.heightFactor,
+ this.widthFactor,
+ super.curve,
+ required super.duration,
+ super.onEnd,
+ }) : assert(alignment != null),
+ assert(widthFactor == null || widthFactor >= 0.0),
+ assert(heightFactor == null || heightFactor >= 0.0);
+
+ /// The widget below this widget in the tree.
+ ///
+ /// {@macro flutter.widgets.ProxyWidget.child}
+ final Widget? child;
+
+ /// {@macro flutter.widgets.basic.fractionallySizedBox.heightFactor}
+ final double? heightFactor;
+
+ /// {@macro flutter.widgets.basic.fractionallySizedBox.widthFactor}
+ final double? widthFactor;
+
+ /// {@macro flutter.widgets.basic.fractionallySizedBox.alignment}
+ final AlignmentGeometry alignment;
+
+ @override
+ AnimatedWidgetBaseState<AnimatedFractionallySizedBox> createState() => _AnimatedFractionallySizedBoxState();
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
+ properties.add(DiagnosticsProperty<double>('widthFactor', widthFactor));
+ properties.add(DiagnosticsProperty<double>('heightFactor', heightFactor));
+ }
+}
+
+class _AnimatedFractionallySizedBoxState extends AnimatedWidgetBaseState<AnimatedFractionallySizedBox> {
+ AlignmentGeometryTween? _alignment;
+ Tween<double>? _heightFactorTween;
+ Tween<double>? _widthFactorTween;
+
+ @override
+ void forEachTween(TweenVisitor<dynamic> visitor) {
+ _alignment = visitor(_alignment, widget.alignment, (dynamic value) => AlignmentGeometryTween(begin: value as AlignmentGeometry)) as AlignmentGeometryTween?;
+ if(widget.heightFactor != null) {
+ _heightFactorTween = visitor(_heightFactorTween, widget.heightFactor, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
+ }
+ if(widget.widthFactor != null) {
+ _widthFactorTween = visitor(_widthFactorTween, widget.widthFactor, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return FractionallySizedBox(
+ alignment: _alignment!.evaluate(animation)!,
+ heightFactor: _heightFactorTween?.evaluate(animation),
+ widthFactor: _widthFactorTween?.evaluate(animation),
+ child: widget.child,
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder description) {
+ super.debugFillProperties(description);
+ description.add(DiagnosticsProperty<AlignmentGeometryTween>('alignment', _alignment, defaultValue: null));
+ description.add(DiagnosticsProperty<Tween<double>>('widthFactor', _widthFactorTween, defaultValue: null));
+ description.add(DiagnosticsProperty<Tween<double>>('heightFactor', _heightFactorTween, defaultValue: null));
+ }
+}
diff --git a/packages/flutter/test/widgets/implicit_animations_test.dart b/packages/flutter/test/widgets/implicit_animations_test.dart
index b0b9402..4059687 100644
--- a/packages/flutter/test/widgets/implicit_animations_test.dart
+++ b/packages/flutter/test/widgets/implicit_animations_test.dart
@@ -421,6 +421,27 @@
expect(state.builds, equals(2));
});
+ testWidgets('AnimatedFractionallySizedBox onEnd callback test', (WidgetTester tester) async {
+ await tester.pumpWidget(wrap(
+ child: TestAnimatedWidget(
+ callback: mockOnEndFunction.handler,
+ switchKey: switchKey,
+ state: _TestAnimatedFractionallySizedBoxWidgetState(),
+ ),
+ ));
+
+ final Finder widgetFinder = find.byKey(switchKey);
+
+ await tester.tap(widgetFinder);
+ await tester.pump();
+ expect(mockOnEndFunction.called, 0);
+ await tester.pump(animationDuration);
+ expect(mockOnEndFunction.called, 0);
+ await tester.pump(additionalDelay);
+ expect(mockOnEndFunction.called, 1);
+
+ await tapTest2and3(tester, widgetFinder, mockOnEndFunction);
+ });
testWidgets('SliverAnimatedOpacity onEnd callback test', (WidgetTester tester) async {
await tester.pumpWidget(TestAnimatedWidget(
@@ -812,6 +833,19 @@
}
}
+class _TestAnimatedFractionallySizedBoxWidgetState extends _TestAnimatedWidgetState {
+ @override
+ Widget getAnimatedWidget() {
+ return AnimatedFractionallySizedBox(
+ duration: duration,
+ onEnd: widget.callback,
+ heightFactor: toggle ? 0.25 : 0.75,
+ widthFactor: toggle ? 0.25 : 0.75,
+ child: child,
+ );
+ }
+}
+
class _TestSliverAnimatedOpacityWidgetState extends _TestAnimatedWidgetState {
@override
Widget getAnimatedWidget() {