Skip to content

Commit 46c4fc1

Browse files
authored
Animated fractionally sized box (#106795)
1 parent d7f7e9d commit 46c4fc1

File tree

4 files changed

+215
-0
lines changed

4 files changed

+215
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Flutter code sample for AnimatedFractionallySizedBox
6+
7+
import 'package:flutter/material.dart';
8+
9+
void main() => runApp(const MyApp());
10+
11+
class MyApp extends StatelessWidget {
12+
const MyApp({super.key});
13+
14+
static const String _title = 'Flutter Code Sample';
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return MaterialApp(
19+
title: _title,
20+
home: Scaffold(
21+
appBar: AppBar(title: const Text(_title)),
22+
body: const MyStatefulWidget(),
23+
),
24+
);
25+
}
26+
}
27+
28+
class MyStatefulWidget extends StatefulWidget {
29+
const MyStatefulWidget({super.key});
30+
31+
@override
32+
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
33+
}
34+
35+
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
36+
bool selected = false;
37+
38+
@override
39+
Widget build(BuildContext context) {
40+
return GestureDetector(
41+
onTap: () {
42+
setState(() {
43+
selected = !selected;
44+
});
45+
},
46+
child: Center(
47+
child: SizedBox(
48+
width: 200,
49+
height: 200,
50+
child: Container(
51+
color: Colors.red,
52+
child: AnimatedFractionallySizedBox(
53+
widthFactor: selected ? 0.25 : 0.75,
54+
heightFactor: selected ? 0.75 : 0.25,
55+
alignment: selected ? Alignment.topLeft : Alignment.bottomRight,
56+
duration: const Duration(seconds: 1),
57+
curve: Curves.fastOutSlowIn,
58+
child: Container(
59+
color: Colors.blue,
60+
child: const FlutterLogo(size: 75),
61+
),
62+
),
63+
),
64+
),
65+
),
66+
);
67+
}
68+
}

packages/flutter/lib/src/widgets/basic.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2798,24 +2798,29 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
27982798
assert(widthFactor == null || widthFactor >= 0.0),
27992799
assert(heightFactor == null || heightFactor >= 0.0);
28002800

2801+
/// {@template flutter.widgets.basic.fractionallySizedBox.widthFactor}
28012802
/// If non-null, the fraction of the incoming width given to the child.
28022803
///
28032804
/// If non-null, the child is given a tight width constraint that is the max
28042805
/// incoming width constraint multiplied by this factor.
28052806
///
28062807
/// If null, the incoming width constraints are passed to the child
28072808
/// unmodified.
2809+
/// {@endtemplate}
28082810
final double? widthFactor;
28092811

2812+
/// {@template flutter.widgets.basic.fractionallySizedBox.heightFactor}
28102813
/// If non-null, the fraction of the incoming height given to the child.
28112814
///
28122815
/// If non-null, the child is given a tight height constraint that is the max
28132816
/// incoming height constraint multiplied by this factor.
28142817
///
28152818
/// If null, the incoming height constraints are passed to the child
28162819
/// unmodified.
2820+
/// {@endtemplate}
28172821
final double? heightFactor;
28182822

2823+
/// {@template flutter.widgets.basic.fractionallySizedBox.alignment}
28192824
/// How to align the child.
28202825
///
28212826
/// The x and y values of the alignment control the horizontal and vertical
@@ -2834,6 +2839,7 @@ class FractionallySizedBox extends SingleChildRenderObjectWidget {
28342839
/// specify an [AlignmentGeometry].
28352840
/// * [AlignmentDirectional], like [Alignment] for specifying alignments
28362841
/// relative to text direction.
2842+
/// {@endtemplate}
28372843
final AlignmentGeometry alignment;
28382844

28392845
@override

packages/flutter/lib/src/widgets/implicit_animations.dart

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,3 +2108,110 @@ class _AnimatedPhysicalModelState extends AnimatedWidgetBaseState<AnimatedPhysic
21082108
);
21092109
}
21102110
}
2111+
2112+
/// Animated version of [FractionallySizedBox] which automatically transitions the
2113+
/// child's size over a given duration whenever the given [widthFactor] or
2114+
/// [heightFactor] changes, as well as the position whenever the given [alignment]
2115+
/// changes.
2116+
///
2117+
/// For the animation, you can choose a [curve] as well as a [duration] and the
2118+
/// widget will automatically animate to the new target [widthFactor] or
2119+
/// [heightFactor].
2120+
///
2121+
/// {@tool dartpad}
2122+
/// The following example transitions an [AnimatedFractionallySizedBox]
2123+
/// between two states. It adjusts the [heightFactor], [widthFactor], and
2124+
/// [alignment] properties when tapped, using a [curve] of [Curves.fastOutSlowIn]
2125+
///
2126+
/// ** See code in examples/api/lib/widgets/implicit_animations/animated_fractionally_sized_box.0.dart **
2127+
/// {@end-tool}
2128+
///
2129+
/// See also:
2130+
///
2131+
/// * [AnimatedAlign], which is an implicitly animated version of [Align].
2132+
/// * [AnimatedContainer], which can transition more values at once.
2133+
/// * [AnimatedSlide], which can animate the translation of child by a given offset relative to its size.
2134+
/// * [AnimatedPositioned], which, as a child of a [Stack], automatically
2135+
/// transitions its child's position over a given duration whenever the given
2136+
/// position changes.
2137+
class AnimatedFractionallySizedBox extends ImplicitlyAnimatedWidget {
2138+
/// Creates a widget that sizes its child to a fraction of the total available
2139+
/// space that animates implicitly, and positions its child by an alignment
2140+
/// that animates implicitly.
2141+
///
2142+
/// The [curve] and [duration] argument must not be null
2143+
/// If non-null, the [widthFactor] and [heightFactor] arguments must be
2144+
/// non-negative.
2145+
const AnimatedFractionallySizedBox({
2146+
super.key,
2147+
this.alignment = Alignment.center,
2148+
this.child,
2149+
this.heightFactor,
2150+
this.widthFactor,
2151+
super.curve,
2152+
required super.duration,
2153+
super.onEnd,
2154+
}) : assert(alignment != null),
2155+
assert(widthFactor == null || widthFactor >= 0.0),
2156+
assert(heightFactor == null || heightFactor >= 0.0);
2157+
2158+
/// The widget below this widget in the tree.
2159+
///
2160+
/// {@macro flutter.widgets.ProxyWidget.child}
2161+
final Widget? child;
2162+
2163+
/// {@macro flutter.widgets.basic.fractionallySizedBox.heightFactor}
2164+
final double? heightFactor;
2165+
2166+
/// {@macro flutter.widgets.basic.fractionallySizedBox.widthFactor}
2167+
final double? widthFactor;
2168+
2169+
/// {@macro flutter.widgets.basic.fractionallySizedBox.alignment}
2170+
final AlignmentGeometry alignment;
2171+
2172+
@override
2173+
AnimatedWidgetBaseState<AnimatedFractionallySizedBox> createState() => _AnimatedFractionallySizedBoxState();
2174+
2175+
@override
2176+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2177+
super.debugFillProperties(properties);
2178+
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
2179+
properties.add(DiagnosticsProperty<double>('widthFactor', widthFactor));
2180+
properties.add(DiagnosticsProperty<double>('heightFactor', heightFactor));
2181+
}
2182+
}
2183+
2184+
class _AnimatedFractionallySizedBoxState extends AnimatedWidgetBaseState<AnimatedFractionallySizedBox> {
2185+
AlignmentGeometryTween? _alignment;
2186+
Tween<double>? _heightFactorTween;
2187+
Tween<double>? _widthFactorTween;
2188+
2189+
@override
2190+
void forEachTween(TweenVisitor<dynamic> visitor) {
2191+
_alignment = visitor(_alignment, widget.alignment, (dynamic value) => AlignmentGeometryTween(begin: value as AlignmentGeometry)) as AlignmentGeometryTween?;
2192+
if(widget.heightFactor != null) {
2193+
_heightFactorTween = visitor(_heightFactorTween, widget.heightFactor, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
2194+
}
2195+
if(widget.widthFactor != null) {
2196+
_widthFactorTween = visitor(_widthFactorTween, widget.widthFactor, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;
2197+
}
2198+
}
2199+
2200+
@override
2201+
Widget build(BuildContext context) {
2202+
return FractionallySizedBox(
2203+
alignment: _alignment!.evaluate(animation)!,
2204+
heightFactor: _heightFactorTween?.evaluate(animation),
2205+
widthFactor: _widthFactorTween?.evaluate(animation),
2206+
child: widget.child,
2207+
);
2208+
}
2209+
2210+
@override
2211+
void debugFillProperties(DiagnosticPropertiesBuilder description) {
2212+
super.debugFillProperties(description);
2213+
description.add(DiagnosticsProperty<AlignmentGeometryTween>('alignment', _alignment, defaultValue: null));
2214+
description.add(DiagnosticsProperty<Tween<double>>('widthFactor', _widthFactorTween, defaultValue: null));
2215+
description.add(DiagnosticsProperty<Tween<double>>('heightFactor', _heightFactorTween, defaultValue: null));
2216+
}
2217+
}

packages/flutter/test/widgets/implicit_animations_test.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,27 @@ void main() {
421421
expect(state.builds, equals(2));
422422
});
423423

424+
testWidgets('AnimatedFractionallySizedBox onEnd callback test', (WidgetTester tester) async {
425+
await tester.pumpWidget(wrap(
426+
child: TestAnimatedWidget(
427+
callback: mockOnEndFunction.handler,
428+
switchKey: switchKey,
429+
state: _TestAnimatedFractionallySizedBoxWidgetState(),
430+
),
431+
));
432+
433+
final Finder widgetFinder = find.byKey(switchKey);
434+
435+
await tester.tap(widgetFinder);
436+
await tester.pump();
437+
expect(mockOnEndFunction.called, 0);
438+
await tester.pump(animationDuration);
439+
expect(mockOnEndFunction.called, 0);
440+
await tester.pump(additionalDelay);
441+
expect(mockOnEndFunction.called, 1);
442+
443+
await tapTest2and3(tester, widgetFinder, mockOnEndFunction);
444+
});
424445

425446
testWidgets('SliverAnimatedOpacity onEnd callback test', (WidgetTester tester) async {
426447
await tester.pumpWidget(TestAnimatedWidget(
@@ -812,6 +833,19 @@ class _TestAnimatedOpacityWidgetState extends _TestAnimatedWidgetState {
812833
}
813834
}
814835

836+
class _TestAnimatedFractionallySizedBoxWidgetState extends _TestAnimatedWidgetState {
837+
@override
838+
Widget getAnimatedWidget() {
839+
return AnimatedFractionallySizedBox(
840+
duration: duration,
841+
onEnd: widget.callback,
842+
heightFactor: toggle ? 0.25 : 0.75,
843+
widthFactor: toggle ? 0.25 : 0.75,
844+
child: child,
845+
);
846+
}
847+
}
848+
815849
class _TestSliverAnimatedOpacityWidgetState extends _TestAnimatedWidgetState {
816850
@override
817851
Widget getAnimatedWidget() {

0 commit comments

Comments
 (0)