Allow Passing an AnimationController to CircularProgressIndicator and LinearProgressIndicator (#174605)

_This PR is based on https://github.com/flutter/flutter/pull/170380,
completing it with unit tests and docs._

Fix https://github.com/flutter/flutter/issues/165741

Adding this property to the indicators allows all indicators on a same
screen to have synchronized progress, an essential feature for progress
indicators.

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Stan Persoons <stan.persoons2@gmail.com>
This commit is contained in:
Tong Mu 2025-09-16 12:15:23 -07:00 committed by GitHub
parent a941a2e30a
commit e79590cff7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 567 additions and 28 deletions

View File

@ -0,0 +1,139 @@
// 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.
import 'package:flutter/material.dart';
/// Flutter code sample for [CircularProgressIndicator].
void main() => runApp(const ProgressIndicatorExampleApp());
class ProgressIndicatorExampleApp extends StatelessWidget {
const ProgressIndicatorExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4)),
home: const ProgressIndicatorExample(),
);
}
}
class ProgressIndicatorExample extends StatefulWidget {
const ProgressIndicatorExample({super.key});
@override
State<ProgressIndicatorExample> createState() => _ProgressIndicatorExampleState();
}
class _ProgressIndicatorExampleState extends State<ProgressIndicatorExample>
with TickerProviderStateMixin {
late AnimationController controller;
int indicatorNum = 1;
bool hasThemeController = true;
@override
void initState() {
controller = AnimationController(
vsync: this,
duration: CircularProgressIndicator.defaultAnimationDuration * 0.8,
);
controller.repeat();
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Theme(
data: ThemeData(
progressIndicatorTheme: hasThemeController
? ProgressIndicatorThemeData(controller: controller)
: null,
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
spacing: 8.0,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
onPressed: () {
setState(() {
indicatorNum += 1;
});
},
child: const Text('More indicators'),
),
TextButton(
onPressed: () {
setState(() {
indicatorNum -= 1;
});
},
child: const Text('Fewer indicators'),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Theme controller? ${hasThemeController ? 'Yes' : 'No'}'),
TextButton(
onPressed: () {
setState(() {
hasThemeController = !hasThemeController;
});
},
child: const Text('Toggle'),
),
],
),
ManyProgressIndicators(indicatorNum: indicatorNum),
],
),
),
),
);
}
}
/// Display several [CircularProgressIndicator] in nested `Container`s.
class ManyProgressIndicators extends StatelessWidget {
const ManyProgressIndicators({super.key, required this.indicatorNum});
final int indicatorNum;
Widget _nestIndicator({required Widget child}) {
return Container(
padding: const EdgeInsets.all(5),
margin: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: const Color.fromARGB(100, 240, 240, 0),
border: Border.all(),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[const CircularProgressIndicator(), child],
),
);
}
@override
Widget build(BuildContext context) {
Widget child = const SizedBox();
for (int i = 0; i < indicatorNum; i++) {
child = _nestIndicator(child: child);
}
return child;
}
}

View File

@ -0,0 +1,52 @@
// 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.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/progress_indicator/circular_progress_indicator.2.dart'
as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Buttons work', (WidgetTester tester) async {
await tester.pumpWidget(const example.ProgressIndicatorExampleApp());
expect(find.byType(CircularProgressIndicator), findsOne);
await tester.tap(find.text('More indicators'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsNWidgets(2));
await tester.tap(find.text('More indicators'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsNWidgets(3));
await tester.tap(find.text('Fewer indicators'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsNWidgets(2));
expect(find.text('Theme controller? Yes'), findsOne);
await tester.tap(find.text('Toggle'));
await tester.pump();
expect(find.text('Theme controller? No'), findsOne);
});
testWidgets('Theme controller can coordinate progress', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: tester, value: 0.5);
addTearDown(controller.dispose);
await tester.pumpWidget(
Theme(
data: ThemeData(progressIndicatorTheme: ProgressIndicatorThemeData(controller: controller)),
child: const example.ManyProgressIndicators(indicatorNum: 4),
),
);
expect(find.byType(CircularProgressIndicator), findsNWidgets(4));
for (int i = 0; i < 4; i++) {
expect(
find.byType(CircularProgressIndicator).at(i),
paints..arc(startAngle: 1.5707963267948966, sweepAngle: 0.001),
);
}
});
}

View File

@ -17,7 +17,11 @@ import 'material.dart';
import 'progress_indicator_theme.dart';
import 'theme.dart';
// This value is extracted from
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/res/anim/progress_indeterminate_material.xml;drc=9cb5b4c2d93acb9d6f5e14167e265c328c487d6b
const int _kIndeterminateLinearDuration = 1800;
// This value is extracted from
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/res/anim/progress_indeterminate_rotation_material.xml;drc=077b44912b879174cec48a25307f1c19b96c2a78
const int _kIndeterminateCircularDuration = 1333 * 2222;
// The progress value below which the track gap is scaled proportionally to
@ -26,6 +30,12 @@ const double _kTrackGapRampDownThreshold = 0.01;
enum _ActivityIndicatorType { material, adaptive }
const String _kValueControllerAssertion =
'A progress indicator cannot have both a value and a controller.\n'
'The "value" property is for a determinate indicator with a specific progress, '
'while the "controller" is for controlling the animation of an indeterminate indicator.\n'
'To resolve this, provide only one of the two properties.';
/// A base class for Material Design progress indicators.
///
/// This widget cannot be instantiated directly. For a linear progress
@ -378,6 +388,11 @@ class _LinearProgressIndicatorPainter extends CustomPainter {
/// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart **
/// {@end-tool}
///
/// {@macro flutter.material.ProgressIndicator.AnimationSynchronization}
///
/// See the documentation of [CircularProgressIndicator] for an example on this
/// topic.
///
/// See also:
///
/// * [CircularProgressIndicator], which shows progress along a circular arc.
@ -407,7 +422,9 @@ class LinearProgressIndicator extends ProgressIndicator {
'This feature was deprecated after v3.26.0-0.1.pre.',
)
this.year2023,
}) : assert(minHeight == null || minHeight > 0);
this.controller,
}) : assert(minHeight == null || minHeight > 0),
assert(value == null || controller == null, _kValueControllerAssertion);
/// {@template flutter.material.LinearProgressIndicator.trackColor}
/// Color of the track being filled by the linear indicator.
@ -486,42 +503,81 @@ class LinearProgressIndicator extends ProgressIndicator {
)
final bool? year2023;
/// {@template flutter.material.ProgressIndicator.controller}
/// An optional [AnimationController] that controls the animation of this
/// indeterminate progress indicator.
///
/// This controller is only used when the indicator is indeterminate (i.e.,
/// when [value] is null). If this property is non-null, [value] must be null.
///
/// The controller's value is expected to be a linear progression from 0.0 to
/// 1.0, which represents one full cycle of the indeterminate animation.
///
/// If this controller is null (and [value] is also null), the widget will
/// look for a [ProgressIndicatorThemeData.controller]. If that is also null,
/// the widget will create and manage its own internal [AnimationController]
/// to drive the default indeterminate animation.
/// {@endtemplate}
///
/// See also:
///
/// * [LinearProgressIndicator.defaultAnimationDuration], default duration
/// for one full cycle of the indeterminate animation.
final AnimationController? controller;
/// The default duration for one full cycle of the indeterminate animation.
///
/// This duration is used when the widget creates its own [AnimationController]
/// because no [controller] was provided, either directly or through a
/// [ProgressIndicatorTheme].
static const Duration defaultAnimationDuration = Duration(
milliseconds: _kIndeterminateLinearDuration,
);
@override
State<LinearProgressIndicator> createState() => _LinearProgressIndicatorState();
}
class _LinearProgressIndicatorState extends State<LinearProgressIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late final AnimationController _internalController;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: _kIndeterminateLinearDuration),
_internalController = AnimationController(
duration: LinearProgressIndicator.defaultAnimationDuration,
vsync: this,
);
if (widget.value == null) {
_controller.repeat();
}
_updateControllerAnimatingStatus();
}
@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();
}
_updateControllerAnimatingStatus();
}
@override
void dispose() {
_controller.dispose();
_internalController.dispose();
super.dispose();
}
AnimationController get _controller =>
widget.controller ??
context.getInheritedWidgetOfExactType<ProgressIndicatorTheme>()?.data.controller ??
context.findAncestorWidgetOfExactType<Theme>()?.data.progressIndicatorTheme.controller ??
_internalController;
void _updateControllerAnimatingStatus() {
if (widget.value == null && !_internalController.isAnimating) {
_internalController.repeat();
} else if (widget.value != null && _internalController.isAnimating) {
_internalController.stop();
}
}
Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context);
final bool year2023 = widget.year2023 ?? indicatorTheme.year2023 ?? true;
@ -751,6 +807,41 @@ class _CircularProgressIndicatorPainter extends CustomPainter {
/// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart **
/// {@end-tool}
///
/// {@template flutter.material.ProgressIndicator.AnimationSynchronization}
/// ## Animation synchronization
///
/// When multiple [CircularProgressIndicator]s or [LinearProgressIndicator]s are
/// animating on screen simultaneously (e.g., in a list of loading items), their
/// uncoordinated animations can appear visually cluttered. To address this, the
/// animation of an indicator can be driven by a custom [AnimationController].
///
/// This allows multiple indicators to be synchronized to a single animation
/// source. The most convenient way to achieve this for a group of indicators is
/// by providing a controller via [ProgressIndicatorTheme] (see
/// [ProgressIndicatorThemeData.controller]). All [CircularProgressIndicator]s
/// or [LinearProgressIndicator]s within that theme's subtree will then share
/// the same animation, resulting in a more coordinated and visually pleasing
/// effect.
///
/// Alternatively, a specific [AnimationController] can be passed directly to the
/// [controller] property of an individual indicator.
/// {@endtemplate}
///
/// {@tool dartpad}
/// This sample demonstrates how to synchronize the indeterminate animations
/// of multiple [CircularProgressIndicator]s using a [Theme].
///
/// Tapping the buttons adds or removes indicators. By default, they all
/// share a [ProgressIndicatorThemeData.controller], which keeps their
/// animations in sync.
///
/// Tapping the "Toggle" button sets the theme's controller to null.
/// This forces each indicator to create its own internal controller,
/// causing their animations to become desynchronized.
///
/// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [LinearProgressIndicator], which displays progress along a line.
@ -781,7 +872,9 @@ class CircularProgressIndicator extends ProgressIndicator {
)
this.year2023,
this.padding,
}) : _indicatorType = _ActivityIndicatorType.material;
this.controller,
}) : assert(value == null || controller == null, _kValueControllerAssertion),
_indicatorType = _ActivityIndicatorType.material;
/// Creates an adaptive progress indicator that is a
/// [CupertinoActivityIndicator] on [TargetPlatform.iOS] &
@ -812,7 +905,9 @@ class CircularProgressIndicator extends ProgressIndicator {
)
this.year2023,
this.padding,
}) : _indicatorType = _ActivityIndicatorType.adaptive;
this.controller,
}) : assert(value == null || controller == null, _kValueControllerAssertion),
_indicatorType = _ActivityIndicatorType.adaptive;
final _ActivityIndicatorType _indicatorType;
@ -903,6 +998,14 @@ class CircularProgressIndicator extends ProgressIndicator {
/// padding. Otherwise, defaults to zero padding.
final EdgeInsetsGeometry? padding;
/// {@macro flutter.material.ProgressIndicator.controller}
///
/// See also:
///
/// * [CircularProgressIndicator.defaultAnimationDuration], default duration
/// for one full cycle of the indeterminate animation.
final AnimationController? controller;
/// The indicator stroke is drawn fully inside of the indicator path.
///
/// This is a constant for use with [strokeAlign].
@ -922,6 +1025,17 @@ class CircularProgressIndicator extends ProgressIndicator {
/// This is a constant for use with [strokeAlign].
static const double strokeAlignOutside = 1.0;
/// The default duration for one full cycle of the indeterminate animation.
///
/// During this period, the indicator completes several full rotations.
///
/// This duration is used when the widget creates its own [AnimationController]
/// because no [controller] was provided, either directly or through a
/// [ProgressIndicatorTheme].
static const Duration defaultAnimationDuration = Duration(
milliseconds: _kIndeterminateCircularDuration,
);
@override
State<CircularProgressIndicator> createState() => _CircularProgressIndicatorState();
}
@ -942,36 +1056,44 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator>
curve: const SawTooth(_rotationCount),
);
late AnimationController _controller;
late final AnimationController _internalController;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: _kIndeterminateCircularDuration),
_internalController = AnimationController(
duration: CircularProgressIndicator.defaultAnimationDuration,
vsync: this,
);
if (widget.value == null) {
_controller.repeat();
}
_updateControllerAnimatingStatus();
}
@override
void didUpdateWidget(CircularProgressIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value == null && !_controller.isAnimating) {
_controller.repeat();
} else if (widget.value != null && _controller.isAnimating) {
_controller.stop();
}
_updateControllerAnimatingStatus();
}
@override
void dispose() {
_controller.dispose();
_internalController.dispose();
super.dispose();
}
AnimationController get _controller =>
widget.controller ??
context.getInheritedWidgetOfExactType<ProgressIndicatorTheme>()?.data.controller ??
context.findAncestorWidgetOfExactType<Theme>()?.data.progressIndicatorTheme.controller ??
_internalController;
void _updateControllerAnimatingStatus() {
if (widget.value == null && !_internalController.isAnimating) {
_internalController.repeat();
} else if (widget.value != null && _internalController.isAnimating) {
_internalController.stop();
}
}
Widget _buildCupertinoIndicator(BuildContext context) {
final Color? tickColor = widget.backgroundColor;
final double? value = widget.value;

View File

@ -54,6 +54,7 @@ class ProgressIndicatorThemeData with Diagnosticable {
'This feature was deprecated after v3.27.0-0.2.pre.',
)
this.year2023,
this.controller,
});
/// The color of the [ProgressIndicator]'s indicator.
@ -138,6 +139,17 @@ class ProgressIndicatorThemeData with Diagnosticable {
)
final bool? year2023;
/// Defines a default [AnimationController] for descendant
/// [CircularProgressIndicator] and [LinearProgressIndicator] widgets.
///
/// If a descendant progress indicator's `controller` property is null, this
/// controller will be used to drive its indeterminate animation. This allows
/// a single controller to synchronize the animations of multiple indicators.
///
/// If this property is also null, the progress indicator will create and
/// manage its own internal [AnimationController].
final AnimationController? controller;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
ProgressIndicatorThemeData copyWith({
@ -156,6 +168,7 @@ class ProgressIndicatorThemeData with Diagnosticable {
double? trackGap,
EdgeInsetsGeometry? circularTrackPadding,
bool? year2023,
AnimationController? controller,
}) {
return ProgressIndicatorThemeData(
color: color ?? this.color,
@ -173,6 +186,7 @@ class ProgressIndicatorThemeData with Diagnosticable {
trackGap: trackGap ?? this.trackGap,
circularTrackPadding: circularTrackPadding ?? this.circularTrackPadding,
year2023: year2023 ?? this.year2023,
controller: controller ?? this.controller,
);
}
@ -207,6 +221,7 @@ class ProgressIndicatorThemeData with Diagnosticable {
t,
),
year2023: t < 0.5 ? a?.year2023 : b?.year2023,
controller: t < 0.5 ? a?.controller : b?.controller,
);
}
@ -227,6 +242,7 @@ class ProgressIndicatorThemeData with Diagnosticable {
trackGap,
circularTrackPadding,
year2023,
controller,
);
@override
@ -252,7 +268,8 @@ class ProgressIndicatorThemeData with Diagnosticable {
other.constraints == constraints &&
other.trackGap == trackGap &&
other.circularTrackPadding == circularTrackPadding &&
other.year2023 == year2023;
other.year2023 == year2023 &&
other.controller == controller;
}
@override
@ -285,6 +302,9 @@ class ProgressIndicatorThemeData with Diagnosticable {
),
);
properties.add(DiagnosticsProperty<bool>('year2023', year2023, defaultValue: null));
properties.add(
DiagnosticsProperty<AnimationController>('controller', controller, defaultValue: null),
);
}
}

View File

@ -569,6 +569,26 @@ void main() {
expect(tester.binding.transientCallbackCount, 0);
});
testWidgets('LinearProgressIndicator reflects controller value', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: tester, value: 0.5);
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SizedBox(width: 200, child: LinearProgressIndicator(controller: controller)),
),
),
),
);
expect(
find.byType(LinearProgressIndicator),
paints..rect(rect: const Rect.fromLTRB(127.79541015625, 0.0, 200.0, 4.0)),
);
});
testWidgets('CircularProgressIndicator paint colors', (WidgetTester tester) async {
const Color green = Color(0xFF00FF00);
const Color blue = Color(0xFF0000FF);
@ -1920,6 +1940,35 @@ void main() {
),
);
});
testWidgets('CircularProgressIndicator reflects controller value', (WidgetTester tester) async {
final AnimationController controller = AnimationController(vsync: tester, value: 0.5);
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: SizedBox(
width: 200,
height: 200,
child: AnimatedBuilder(
animation: controller,
builder: (BuildContext context, Widget? child) {
return CircularProgressIndicator(controller: controller);
},
),
),
),
),
),
);
expect(
find.byType(CircularProgressIndicator),
paints..arc(startAngle: 1.5707963267948966, sweepAngle: 0.001),
);
});
}
class _RefreshProgressIndicatorGolden extends StatefulWidget {

View File

@ -560,4 +560,161 @@ void main() {
..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 4.0), color: theme.colorScheme.primary),
);
});
testWidgets('LinearProgressIndicator reflects the value of the theme controller', (
WidgetTester tester,
) async {
Widget buildApp({
AnimationController? widgetController,
AnimationController? indicatorThemeController,
AnimationController? globalThemeController,
}) {
return MaterialApp(
home: Material(
child: Center(
child: Theme(
data: ThemeData(
progressIndicatorTheme: ProgressIndicatorThemeData(
controller: globalThemeController,
),
),
child: ProgressIndicatorTheme(
data: ProgressIndicatorThemeData(controller: indicatorThemeController),
child: SizedBox(
width: 200.0,
child: LinearProgressIndicator(controller: widgetController),
),
),
),
),
),
);
}
void expectProgressAt({required double left, required double right}) {
final PaintPattern expectedPaints = paints;
if (right < 200) {
// Right track
expectedPaints.rect(rect: Rect.fromLTRB(right, 0.0, 200, 4.0));
}
expectedPaints.rect(rect: Rect.fromLTRB(left, 0.0, right, 4.0));
if (left > 0) {
// Left track
expectedPaints.rect(rect: Rect.fromLTRB(0, 0.0, left, 4.0));
}
expect(find.byType(LinearProgressIndicator), expectedPaints);
}
await tester.pumpWidget(buildApp());
await tester.pump(const Duration(milliseconds: 500));
expectProgressAt(left: 16.028758883476257, right: 141.07513427734375);
final AnimationController globalThemeController = AnimationController(
vsync: tester,
value: 0.1,
);
addTearDown(globalThemeController.dispose);
await tester.pumpWidget(buildApp(globalThemeController: globalThemeController));
expectProgressAt(left: 0.0, right: 37.14974820613861);
final AnimationController indicatorThemeController = AnimationController(
vsync: tester,
value: 0.5,
);
addTearDown(indicatorThemeController.dispose);
await tester.pumpWidget(
buildApp(
globalThemeController: globalThemeController,
indicatorThemeController: indicatorThemeController,
),
);
expectProgressAt(left: 127.79541015625, right: 200.0);
final AnimationController widgetController = AnimationController(vsync: tester, value: 0.8);
addTearDown(widgetController.dispose);
await tester.pumpWidget(
buildApp(
globalThemeController: globalThemeController,
indicatorThemeController: indicatorThemeController,
widgetController: widgetController,
),
);
expectProgressAt(left: 98.24226796627045, right: 181.18448555469513);
});
testWidgets('CircularProgressIndicator reflects the value of the theme controller', (
WidgetTester tester,
) async {
Widget buildApp({
AnimationController? widgetController,
AnimationController? indicatorThemeController,
AnimationController? globalThemeController,
}) {
return MaterialApp(
home: Material(
child: Center(
child: Theme(
data: ThemeData(
progressIndicatorTheme: ProgressIndicatorThemeData(
color: Colors.black,
linearTrackColor: Colors.green,
controller: globalThemeController,
),
),
child: ProgressIndicatorTheme(
data: ProgressIndicatorThemeData(controller: indicatorThemeController),
child: SizedBox(
width: 200.0,
child: CircularProgressIndicator(controller: widgetController),
),
),
),
),
),
);
}
void expectProgressAt({required double start, required double sweep}) {
expect(
find.byType(CircularProgressIndicator),
paints..arc(startAngle: start, sweepAngle: sweep),
);
}
await tester.pumpWidget(buildApp());
await tester.pump(const Duration(milliseconds: 500));
expectProgressAt(start: 0.43225767465697107, sweep: 4.52182126629162);
final AnimationController globalThemeController = AnimationController(
vsync: tester,
value: 0.1,
);
addTearDown(globalThemeController.dispose);
await tester.pumpWidget(buildApp(globalThemeController: globalThemeController));
expectProgressAt(start: 0.628318530718057, sweep: 2.8904563625380906);
final AnimationController indicatorThemeController = AnimationController(
vsync: tester,
value: 0.5,
);
addTearDown(indicatorThemeController.dispose);
await tester.pumpWidget(
buildApp(
globalThemeController: globalThemeController,
indicatorThemeController: indicatorThemeController,
),
);
expectProgressAt(start: 1.5707963267948966, sweep: 0.001);
final AnimationController widgetController = AnimationController(vsync: tester, value: 0.8);
addTearDown(widgetController.dispose);
await tester.pumpWidget(
buildApp(
globalThemeController: globalThemeController,
indicatorThemeController: indicatorThemeController,
widgetController: widgetController,
),
);
expectProgressAt(start: 2.520489337828999, sweep: 4.076855234710353);
});
}