-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Closed
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work lista: animationAnimation APIsAnimation APIsc: new featureNothing broken; request for a new capabilityNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to FlutterA detailed proposal for a change to Flutterteam-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework team
Description
Use case
Implicit widgets are great, but they can't be used for repeating animations.
Instead, you need to use explicit animations.
Spinning green square
This is a basic animation that requires heavy boilerplate.
Code sample...
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: SpinningSquarePage()));
}
class SpinningSquarePage extends StatefulWidget {
const SpinningSquarePage({super.key});
@override
State<SpinningSquarePage> createState() => _SpinningSquarePageState();
}
class _SpinningSquarePageState extends State<SpinningSquarePage>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
@override
void initState() {
super.initState();
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RotationTransition(
turns: _controller,
child: const ColoredBox(
color: Colors.green,
child: SizedBox.square(dimension: 100),
),
),
),
);
}
}LinearProgressIndicator
Another example is LinearProgressIndicator - it shows a spinning indicator if its value is null:
flutter/packages/flutter/lib/src/material/progress_indicator.dart
Lines 434 to 536 in e65380a
| class _LinearProgressIndicatorState extends State<LinearProgressIndicator> | |
| with SingleTickerProviderStateMixin { | |
| late AnimationController _controller; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _controller = AnimationController( | |
| duration: const Duration(milliseconds: _kIndeterminateLinearDuration), | |
| vsync: this, | |
| ); | |
| if (widget.value == null) { | |
| _controller.repeat(); | |
| } | |
| } | |
| @override | |
| void didUpdateWidget(LinearProgressIndicator oldWidget) { | |
| super.didUpdateWidget(oldWidget); | |
| if (widget.value == null && !_controller.isAnimating) { | |
| _controller.repeat(); | |
| } else if (widget.value != null && _controller.isAnimating) { | |
| _controller.stop(); | |
| } | |
| } | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) { | |
| final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); | |
| final bool year2023 = widget.year2023 ?? indicatorTheme.year2023 ?? true; | |
| final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { | |
| true => | |
| year2023 | |
| ? _LinearProgressIndicatorDefaultsM3Year2023(context) | |
| : _LinearProgressIndicatorDefaultsM3(context), | |
| false => _LinearProgressIndicatorDefaultsM2(context), | |
| }; | |
| final Color trackColor = | |
| widget.backgroundColor ?? indicatorTheme.linearTrackColor ?? defaults.linearTrackColor!; | |
| final double minHeight = | |
| widget.minHeight ?? indicatorTheme.linearMinHeight ?? defaults.linearMinHeight!; | |
| final BorderRadiusGeometry? borderRadius = | |
| widget.borderRadius ?? indicatorTheme.borderRadius ?? defaults.borderRadius; | |
| final Color? stopIndicatorColor = !year2023 | |
| ? widget.stopIndicatorColor ?? | |
| indicatorTheme.stopIndicatorColor ?? | |
| defaults.stopIndicatorColor | |
| : null; | |
| final double? stopIndicatorRadius = !year2023 | |
| ? widget.stopIndicatorRadius ?? | |
| indicatorTheme.stopIndicatorRadius ?? | |
| defaults.stopIndicatorRadius | |
| : null; | |
| final double? trackGap = !year2023 | |
| ? widget.trackGap ?? indicatorTheme.trackGap ?? defaults.trackGap | |
| : null; | |
| Widget result = ConstrainedBox( | |
| constraints: BoxConstraints(minWidth: double.infinity, minHeight: minHeight), | |
| child: CustomPaint( | |
| painter: _LinearProgressIndicatorPainter( | |
| trackColor: trackColor, | |
| valueColor: widget._getValueColor(context, defaultColor: defaults.color), | |
| value: widget.value, // may be null | |
| animationValue: animationValue, // ignored if widget.value is not null | |
| textDirection: textDirection, | |
| indicatorBorderRadius: borderRadius, | |
| stopIndicatorColor: stopIndicatorColor, | |
| stopIndicatorRadius: stopIndicatorRadius, | |
| trackGap: trackGap, | |
| ), | |
| ), | |
| ); | |
| // Clip is only needed with indeterminate progress indicators | |
| if (borderRadius != null && widget.value == null) { | |
| result = ClipRRect(borderRadius: borderRadius, child: result); | |
| } | |
| return widget._buildSemanticsWrapper(context: context, child: result); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final TextDirection textDirection = Directionality.of(context); | |
| if (widget.value != null) { | |
| return _buildIndicator(context, _controller.value, textDirection); | |
| } | |
| return AnimatedBuilder( | |
| animation: _controller.view, | |
| builder: (BuildContext context, Widget? child) { | |
| return _buildIndicator(context, _controller.value, textDirection); | |
| }, | |
| ); | |
| } | |
| } |
Proposal
Option 1
class TweenAnimationBuilder {
TweenAnimationBuilder.repeat({
Key? key,
required Tween<T> tween,
required Duration duration,
Curve curve,
// If true, the animation flip-flops between forwards and reverse.
bool reverse = false,
// If true, the animation is paused.
//
// For example, LinearProgressIndicator would pause its animation
// if it has a value, and would unpause the animation if its value
// is null.
bool paused = false,
required ValueWidgetBuilder builder,
Widget? child,
});
}Option 2 (preferred)
class RepeatedTweenAnimationBuilder {
RepeatedTweenAnimationBuilder({
Key? key,
required Tween<T> tween,
required Duration duration,
Curve curve,
// If true, the animation flip-flops between forwards and reverse.
bool reverse = false,
// If true, the animation is paused.
//
// For example, LinearProgressIndicator would pause its animation
// if it has a value, and would unpause the animation if its value
// is null.
bool paused = false,
required ValueWidgetBuilder builder,
Widget? child,
});
}Usage
import 'dart:math' as math;
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: SpinningSquarePage()));
}
class SpinningSquarePage extends StatelessWidget {
const SpinningSquarePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RepeatedTweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(seconds: 2),
builder: (context, rotation, _) {
return Transform.rotate(
angle: math.pi * rotation * 2,
child: const ColoredBox(
color: Colors.green,
child: SizedBox.square(dimension: 100),
),
);
},
),
),
);
}
}sethladd, bernaferrari, AbdeMohlbi, hamza-imran75, PiotrRogulski and 4 moresethladd, bernaferrari and CoderNamedHendrick
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work lista: animationAnimation APIsAnimation APIsc: new featureNothing broken; request for a new capabilityNothing broken; request for a new capabilityc: proposalA detailed proposal for a change to FlutterA detailed proposal for a change to Flutterteam-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework team