From b8dcb0c3c577cf476bf5bcc654b599724e13edde Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Wed, 30 Oct 2024 20:14:11 +0200 Subject: [PATCH] Update Material 3 `LinearProgressIndicator` for new visual style (#154817) Related to [Update both `ProgressIndicator` for Material 3 redesign](https://github.com/flutter/flutter/issues/141340) ### Code sample
expand to view the code sample ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { bool isRTL = false; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Directionality( textDirection: isRTL ? TextDirection.rtl : TextDirection.ltr, child: Center( child: Column( spacing: 2.0, mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Default LinearProgressIndicator'), const Padding( padding: EdgeInsets.all(16.0), child: LinearProgressIndicator( value: 0.45, ), ), const Text('Default indefinite LinearProgressIndicator'), const Padding( padding: EdgeInsets.all(16.0), child: LinearProgressIndicator(), ), const Text('Updated height and border radius'), Padding( padding: const EdgeInsets.all(16.0), child: LinearProgressIndicator( value: 0.25, minHeight: 16.0, borderRadius: BorderRadius.circular(16.0), ), ), const Text('Updated stop indicator color and radius'), Padding( padding: const EdgeInsets.all(16.0), child: LinearProgressIndicator( value: 0.74, minHeight: 16.0, borderRadius: BorderRadius.circular(16.0), stopIndicatorColor: Theme.of(context).colorScheme.error, stopIndicatorRadius: 32.0, ), ), const Text('Track gap and stop indicator radius set to 0'), Padding( padding: const EdgeInsets.all(16.0), child: LinearProgressIndicator( value: 0.50, minHeight: 16.0, borderRadius: BorderRadius.circular(16.0), trackGap: 0, stopIndicatorRadius: 0, ), ), ], ), ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () { setState(() { isRTL = !isRTL; }); }, label: const Text('Toggle Direction'), ), ), ); } } ```
### Preview Screenshot 2024-09-09 at 13 53 10 --- .../gen_defaults/generated/used_tokens.csv | 3 + .../lib/progress_indicator_template.dart | 12 + .../linear_progress_indicator.0.dart | 35 ++- .../linear_progress_indicator.1.dart | 29 +- .../linear_progress_indicator.0_test.dart | 32 ++- .../linear_progress_indicator.1_test.dart | 5 +- .../lib/src/material/progress_indicator.dart | 263 ++++++++++++++---- .../material/progress_indicator_theme.dart | 51 +++- .../material/progress_indicator_test.dart | 251 +++++++++++++++++ .../progress_indicator_theme_test.dart | 166 +++++++++++ 10 files changed, 754 insertions(+), 93 deletions(-) diff --git a/dev/tools/gen_defaults/generated/used_tokens.csv b/dev/tools/gen_defaults/generated/used_tokens.csv index b7929b4c4a4..c5ecfb6f58e 100644 --- a/dev/tools/gen_defaults/generated/used_tokens.csv +++ b/dev/tools/gen_defaults/generated/used_tokens.csv @@ -585,7 +585,10 @@ md.comp.primary-navigation-tab.inactive.pressed.state-layer.opacity, md.comp.primary-navigation-tab.with-label-text.active.label-text.color, md.comp.primary-navigation-tab.with-label-text.inactive.label-text.color, md.comp.primary-navigation-tab.with-label-text.label-text.text-style, +md.comp.progress-indicator.active-indicator-track-space, md.comp.progress-indicator.active-indicator.color, +md.comp.progress-indicator.stop-indicator.color, +md.comp.progress-indicator.stop-indicator.size, md.comp.progress-indicator.track.color, md.comp.progress-indicator.track.thickness, md.comp.radio-button.disabled.selected.icon.color, diff --git a/dev/tools/gen_defaults/lib/progress_indicator_template.dart b/dev/tools/gen_defaults/lib/progress_indicator_template.dart index de7ba774f4a..82e1a8b13ba 100644 --- a/dev/tools/gen_defaults/lib/progress_indicator_template.dart +++ b/dev/tools/gen_defaults/lib/progress_indicator_template.dart @@ -38,6 +38,18 @@ class _Linear${blockName}DefaultsM3 extends ProgressIndicatorThemeData { @override double get linearMinHeight => ${getToken('md.comp.progress-indicator.track.thickness')}; + + @override + BorderRadius get borderRadius => BorderRadius.circular(${getToken('md.comp.progress-indicator.track.thickness')} / 2); + + @override + Color get stopIndicatorColor => ${componentColor('md.comp.progress-indicator.stop-indicator')}; + + @override + double? get stopIndicatorRadius => ${getToken('md.comp.progress-indicator.stop-indicator.size')} / 2; + + @override + double? get trackGap => ${getToken('md.comp.progress-indicator.active-indicator-track-space')}; } '''; } diff --git a/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart b/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart index 7e2e7c35873..2d7a626d83c 100644 --- a/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart +++ b/examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart @@ -6,10 +6,10 @@ import 'package:flutter/material.dart'; /// Flutter code sample for [LinearProgressIndicator]. -void main() => runApp(const ProgressIndicatorApp()); +void main() => runApp(const ProgressIndicatorExampleApp()); -class ProgressIndicatorApp extends StatelessWidget { - const ProgressIndicatorApp({super.key}); +class ProgressIndicatorExampleApp extends StatelessWidget { + const ProgressIndicatorExampleApp({super.key}); @override Widget build(BuildContext context) { @@ -23,24 +23,26 @@ class ProgressIndicatorExample extends StatefulWidget { const ProgressIndicatorExample({super.key}); @override - State createState() => _ProgressIndicatorExampleState(); + State createState() => + _ProgressIndicatorExampleState(); } -class _ProgressIndicatorExampleState extends State with TickerProviderStateMixin { +class _ProgressIndicatorExampleState extends State + with TickerProviderStateMixin { late AnimationController controller; @override void initState() { + super.initState(); controller = AnimationController( /// [AnimationController]s can be created with `vsync: this` because of /// [TickerProviderStateMixin]. vsync: this, duration: const Duration(seconds: 5), )..addListener(() { - setState(() {}); - }); - controller.repeat(reverse: true); - super.initState(); + setState(() {}); + }) + ..repeat(reverse: true); } @override @@ -55,16 +57,13 @@ class _ProgressIndicatorExampleState extends State wit body: Padding( padding: const EdgeInsets.all(20.0), child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - 'Linear progress indicator with a fixed color', - style: TextStyle(fontSize: 20), - ), - LinearProgressIndicator( - value: controller.value, - semanticsLabel: 'Linear progress indicator', - ), + const Text('Determinate LinearProgressIndicator'), + LinearProgressIndicator(value: controller.value), + const Text('Indeterminate LinearProgressIndicator'), + const LinearProgressIndicator(), ], ), ), diff --git a/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart b/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart index 9a954e46ebe..6663c60b43e 100644 --- a/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart +++ b/examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart @@ -6,16 +6,15 @@ import 'package:flutter/material.dart'; /// Flutter code sample for [LinearProgressIndicator]. -void main() => runApp(const ProgressIndicatorApp()); +void main() => runApp(const ProgressIndicatorExampleApp()); -class ProgressIndicatorApp extends StatelessWidget { - const ProgressIndicatorApp({super.key}); +class ProgressIndicatorExampleApp extends StatelessWidget { + const ProgressIndicatorExampleApp({super.key}); @override Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4)), - home: const ProgressIndicatorExample(), + return const MaterialApp( + home: ProgressIndicatorExample(), ); } } @@ -24,25 +23,27 @@ class ProgressIndicatorExample extends StatefulWidget { const ProgressIndicatorExample({super.key}); @override - State createState() => _ProgressIndicatorExampleState(); + State createState() => + _ProgressIndicatorExampleState(); } -class _ProgressIndicatorExampleState extends State with TickerProviderStateMixin { +class _ProgressIndicatorExampleState extends State + with TickerProviderStateMixin { late AnimationController controller; bool determinate = false; @override void initState() { + super.initState(); controller = AnimationController( /// [AnimationController]s can be created with `vsync: this` because of /// [TickerProviderStateMixin]. vsync: this, duration: const Duration(seconds: 2), )..addListener(() { - setState(() {}); - }); - controller.repeat(); - super.initState(); + setState(() {}); + }) + ..repeat(reverse: true); } @override @@ -65,7 +66,7 @@ class _ProgressIndicatorExampleState extends State wit ), const SizedBox(height: 30), LinearProgressIndicator( - value: controller.value, + value: determinate ? controller.value : null, semanticsLabel: 'Linear progress indicator', ), const SizedBox(height: 10), @@ -73,7 +74,7 @@ class _ProgressIndicatorExampleState extends State wit children: [ Expanded( child: Text( - 'determinate Mode', + '${determinate ? 'Determinate' : 'Indeterminate'} Mode', style: Theme.of(context).textTheme.titleSmall, ), ), diff --git a/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart b/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart index 339e3dc7eee..5f8b019f833 100644 --- a/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart +++ b/examples/api/test/material/progress_indicator/linear_progress_indicator.0_test.dart @@ -2,23 +2,39 @@ // 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/linear_progress_indicator.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('Finds LinearProgressIndicator', (WidgetTester tester) async { + testWidgets('Determinate and Indeterminate LinearProgressIndicators', + (WidgetTester tester) async { await tester.pumpWidget( - const example.ProgressIndicatorApp(), + const example.ProgressIndicatorExampleApp(), ); - expect( - find.bySemanticsLabel('Linear progress indicator'), - findsOneWidget, - ); + expect(find.text('Determinate LinearProgressIndicator'), findsOneWidget); + expect(find.text('Indeterminate LinearProgressIndicator'), findsOneWidget); + expect(find.byType(LinearProgressIndicator), findsNWidgets(2)); - // Test if LinearProgressIndicator is animating. + // Test determinate LinearProgressIndicator. + LinearProgressIndicator determinateIndicator = tester.firstWidget( + find.byType(LinearProgressIndicator).first, + ); + expect(determinateIndicator.value, equals(0.0)); + + // Advance the animation by 2 seconds. await tester.pump(const Duration(seconds: 2)); - expect(tester.hasRunningAnimations, isTrue); + determinateIndicator = tester.firstWidget( + find.byType(LinearProgressIndicator).first, + ); + expect(determinateIndicator.value, equals(0.4)); + + // Test indeterminate LinearProgressIndicator. + final LinearProgressIndicator indeterminateIndicator = tester.firstWidget( + find.byType(LinearProgressIndicator).last, + ); + expect(indeterminateIndicator.value, null); }); } diff --git a/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart b/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart index 9cf4e91d6d1..f38d94e15c5 100644 --- a/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart +++ b/examples/api/test/material/progress_indicator/linear_progress_indicator.1_test.dart @@ -8,9 +8,10 @@ import 'package:flutter_api_samples/material/progress_indicator/linear_progress_ import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('Finds LinearProgressIndicator', (WidgetTester tester) async { + testWidgets('Can control LinearProgressIndicator value', + (WidgetTester tester) async { await tester.pumpWidget( - const example.ProgressIndicatorApp(), + const example.ProgressIndicatorExampleApp(), ); expect( diff --git a/packages/flutter/lib/src/material/progress_indicator.dart b/packages/flutter/lib/src/material/progress_indicator.dart index d380c2c0d80..0b454bda51d 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -145,20 +145,26 @@ abstract class ProgressIndicator extends StatefulWidget { class _LinearProgressIndicatorPainter extends CustomPainter { const _LinearProgressIndicatorPainter({ - required this.backgroundColor, + required this.trackColor, required this.valueColor, this.value, required this.animationValue, required this.textDirection, required this.indicatorBorderRadius, + required this.stopIndicatorColor, + required this.stopIndicatorRadius, + required this.trackGap, }); - final Color backgroundColor; + final Color trackColor; final Color valueColor; final double? value; final double animationValue; final TextDirection textDirection; - final BorderRadiusGeometry indicatorBorderRadius; + final BorderRadiusGeometry? indicatorBorderRadius; + final Color? stopIndicatorColor; + final double? stopIndicatorRadius; + final double? trackGap; // The indeterminate progress animation displays two lines whose leading (head) // and trailing (tail) endpoints are defined by the following four curves. @@ -185,33 +191,78 @@ class _LinearProgressIndicatorPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.fill; + final double effectiveTrackGap = switch (value) { + null || 1.0 => 0.0, + _ => trackGap ?? 0.0, + }; - paint.color = valueColor; + final Rect trackRect; + if (value != null && effectiveTrackGap > 0) { + trackRect = switch (textDirection) { + TextDirection.ltr => Rect.fromLTRB( + clampDouble(value!, 0.0, 1.0) * size.width + effectiveTrackGap, + 0, + size.width, + size.height, + ), + TextDirection.rtl => Rect.fromLTRB( + 0, + 0, + size.width - clampDouble(value!, 0.0, 1.0) * size.width - effectiveTrackGap, + size.height, + ), + }; + } else { + trackRect = Offset.zero & size; + } - void drawBar(double x, double width) { + // Draw the track. + final Paint trackPaint = Paint()..color = trackColor; + if (indicatorBorderRadius != null) { + final RRect trackRRect = indicatorBorderRadius!.resolve(textDirection).toRRect(trackRect); + canvas.drawRRect(trackRRect, trackPaint); + } else { + canvas.drawRect(trackRect, trackPaint); + } + + void drawStopIndicator() { + // Limit the stop indicator radius to the height of the indicator. + final double radius = math.min(stopIndicatorRadius!, size.height / 2); + final Paint indicatorPaint = Paint()..color = stopIndicatorColor!; + final Offset position = switch (textDirection) { + TextDirection.rtl => Offset(size.height / 2, size.height / 2), + TextDirection.ltr => Offset(size.width - size.height / 2, size.height / 2), + }; + canvas.drawCircle(position, radius, indicatorPaint); + } + + // Draw the stop indicator. + if (value != null && stopIndicatorRadius != null && stopIndicatorRadius! > 0) { + drawStopIndicator(); + } + + void drawActiveIndicator(double x, double width) { if (width <= 0.0) { return; } - + final Paint activeIndicatorPaint = Paint()..color = valueColor; final double left = switch (textDirection) { TextDirection.rtl => size.width - width - x, TextDirection.ltr => x, }; - final Rect rect = Offset(left, 0.0) & Size(width, size.height); - if (indicatorBorderRadius != BorderRadius.zero) { - final RRect rrect = indicatorBorderRadius.resolve(textDirection).toRRect(rect); - canvas.drawRRect(rrect, paint); + final Rect activeRect = Offset(left, 0.0) & Size(width, size.height); + if (indicatorBorderRadius != null) { + final RRect activeRRect = indicatorBorderRadius!.resolve(textDirection).toRRect(activeRect); + canvas.drawRRect(activeRRect, activeIndicatorPaint); } else { - canvas.drawRect(rect, paint); + canvas.drawRect(activeRect, activeIndicatorPaint); } } + // Draw the active indicator. if (value != null) { - drawBar(0.0, clampDouble(value!, 0.0, 1.0) * size.width); + drawActiveIndicator(0.0, clampDouble(value!, 0.0, 1.0) * size.width); } else { final double x1 = size.width * line1Tail.transform(animationValue); final double width1 = size.width * line1Head.transform(animationValue) - x1; @@ -219,19 +270,22 @@ class _LinearProgressIndicatorPainter extends CustomPainter { final double x2 = size.width * line2Tail.transform(animationValue); final double width2 = size.width * line2Head.transform(animationValue) - x2; - drawBar(x1, width1); - drawBar(x2, width2); + drawActiveIndicator(x1, width1); + drawActiveIndicator(x2, width2); } } @override bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) { - return oldPainter.backgroundColor != backgroundColor + return oldPainter.trackColor != trackColor || oldPainter.valueColor != valueColor || oldPainter.value != value || oldPainter.animationValue != animationValue || oldPainter.textDirection != textDirection - || oldPainter.indicatorBorderRadius != indicatorBorderRadius; + || oldPainter.indicatorBorderRadius != indicatorBorderRadius + || oldPainter.stopIndicatorColor != stopIndicatorColor + || oldPainter.stopIndicatorRadius != stopIndicatorRadius + || oldPainter.trackGap != trackGap; } } @@ -258,7 +312,7 @@ class _LinearProgressIndicatorPainter extends CustomPainter { /// The indicator can be made taller by wrapping the widget with a [SizedBox]. /// /// {@tool dartpad} -/// This example shows a [LinearProgressIndicator] with a changing value. +/// This example showcases determinate and indeterminate [LinearProgressIndicator]s. /// /// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart ** /// {@end-tool} @@ -290,7 +344,15 @@ class LinearProgressIndicator extends ProgressIndicator { this.minHeight, super.semanticsLabel, super.semanticsValue, - this.borderRadius = BorderRadius.zero, + this.borderRadius, + this.stopIndicatorColor, + this.stopIndicatorRadius, + this.trackGap, + @Deprecated( + 'Use ProgressIndicatorTheme to customize the ProgressIndicator appearance. ' + 'This feature was deprecated after v3.26.0-0.1.pre.' + ) + this.year2023 = true, }) : assert(minHeight == null || minHeight > 0); /// {@template flutter.material.LinearProgressIndicator.trackColor} @@ -315,9 +377,56 @@ class LinearProgressIndicator extends ProgressIndicator { /// The border radius of both the indicator and the track. /// - /// By default it is [BorderRadius.zero], which produces a rectangular shape + /// If null, then the [ProgressIndicatorThemeData.borderRadius] will be used. + /// If that is also null, then defaults to radius of 2, which produces a + /// rounded shape with a rounded indicator. If [ThemeData.useMaterial3] is false, + /// then defaults to [BorderRadius.zero], which produces a rectangular shape /// with a rectangular indicator. - final BorderRadiusGeometry borderRadius; + final BorderRadiusGeometry? borderRadius; + + /// The color of the stop indicator. + /// + /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no stop + /// indicator will be drawn. + /// + /// If null, then the [ProgressIndicatorThemeData.stopIndicatorColor] will be used. + /// If that is null, then the [ColorScheme.primary] will be used. + final Color? stopIndicatorColor; + + /// The radius of the stop indicator. + /// + /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no stop + /// indicator will be drawn. + /// + /// Set [stopIndicatorRadius] to 0 to hide the stop indicator. + /// + /// If null, then the [ProgressIndicatorThemeData.stopIndicatorRadius] will be used. + /// If that is null, then defaults to 2. + final double? stopIndicatorRadius; + + /// The gap between the indicator and the track. + /// + /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no track + /// gap will be drawn. + /// + /// Set [trackGap] to 0 to hide the track gap. + /// + /// If null, then the [ProgressIndicatorThemeData.trackGap] will be used. + /// If that is null, then defaults to 4. + final double? trackGap; + + /// When true, the [LinearProgressIndicator] will use the 2023 Material 3 + /// Design appearance. + /// + /// Defaults to true. If false, the [LinearProgressIndicator] will use the + /// latest Material 3 Design appearance, which was introduced in December 2023. + /// + /// If [ThemeData.useMaterial3] is false, then this property is ignored. + @Deprecated( + 'Use ProgressIndicatorTheme to customize the ProgressIndicator appearance. ' + 'This feature was deprecated after v3.27.0-0.1.pre.' + ) + final bool year2023; @override State createState() => _LinearProgressIndicatorState(); @@ -355,9 +464,12 @@ class _LinearProgressIndicatorState extends State with } Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) { - final ProgressIndicatorThemeData defaults = Theme.of(context).useMaterial3 - ? _LinearProgressIndicatorDefaultsM3(context) - : _LinearProgressIndicatorDefaultsM2(context); + final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { + true => widget.year2023 + ? _LinearProgressIndicatorDefaultsM3Year2023(context) + : _LinearProgressIndicatorDefaultsM3(context), + false => _LinearProgressIndicatorDefaultsM2(context), + }; final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); final Color trackColor = widget.backgroundColor ?? @@ -366,33 +478,56 @@ class _LinearProgressIndicatorState extends State with final double minHeight = widget.minHeight ?? indicatorTheme.linearMinHeight ?? defaults.linearMinHeight!; + final BorderRadiusGeometry? borderRadius = widget.borderRadius + ?? indicatorTheme.borderRadius + ?? defaults.borderRadius; + final Color? stopIndicatorColor = !widget.year2023 + ? widget.stopIndicatorColor ?? + indicatorTheme.stopIndicatorColor ?? + defaults.stopIndicatorColor + : null; + final double? stopIndicatorRadius = !widget.year2023 + ? widget.stopIndicatorRadius ?? + indicatorTheme.stopIndicatorRadius ?? + defaults.stopIndicatorRadius + : null; + final double? trackGap = !widget.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: Container( - // Clip is only needed with indeterminate progress indicators - clipBehavior: (widget.borderRadius != BorderRadius.zero && widget.value == null) - ? Clip.antiAlias - : Clip.none, - decoration: ShapeDecoration( - color: trackColor, - shape: RoundedRectangleBorder(borderRadius: widget.borderRadius), - ), - constraints: BoxConstraints( - minWidth: double.infinity, - minHeight: minHeight, - ), - child: CustomPaint( - painter: _LinearProgressIndicatorPainter( - backgroundColor: 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: widget.borderRadius, - ), - ), - ), + child: result, ); } @@ -1065,6 +1200,22 @@ class _LinearProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { double get linearMinHeight => 4.0; } +class _LinearProgressIndicatorDefaultsM3Year2023 extends ProgressIndicatorThemeData { + _LinearProgressIndicatorDefaultsM3Year2023(this.context); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + Color get color => _colors.primary; + + @override + Color get linearTrackColor => _colors.secondaryContainer; + + @override + double get linearMinHeight => 4.0; +} + // BEGIN GENERATED TOKEN PROPERTIES - ProgressIndicator // Do not edit by hand. The code between the "BEGIN GENERATED" and @@ -1099,6 +1250,18 @@ class _LinearProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { @override double get linearMinHeight => 4.0; + + @override + BorderRadius get borderRadius => BorderRadius.circular(4.0 / 2); + + @override + Color get stopIndicatorColor => _colors.primary; + + @override + double? get stopIndicatorRadius => 4.0 / 2; + + @override + double? get trackGap => 4.0; } // END GENERATED TOKEN PROPERTIES - ProgressIndicator diff --git a/packages/flutter/lib/src/material/progress_indicator_theme.dart b/packages/flutter/lib/src/material/progress_indicator_theme.dart index 2e7e3c7e267..f93cd215e10 100644 --- a/packages/flutter/lib/src/material/progress_indicator_theme.dart +++ b/packages/flutter/lib/src/material/progress_indicator_theme.dart @@ -39,6 +39,10 @@ class ProgressIndicatorThemeData with Diagnosticable { this.linearMinHeight, this.circularTrackColor, this.refreshBackgroundColor, + this.borderRadius, + this.stopIndicatorColor, + this.stopIndicatorRadius, + this.trackGap, }); /// The color of the [ProgressIndicator]'s indicator. @@ -66,6 +70,27 @@ class ProgressIndicatorThemeData with Diagnosticable { /// {@macro flutter.material.RefreshProgressIndicator.backgroundColor} final Color? refreshBackgroundColor; + /// Overrides the border radius of the [ProgressIndicator]. + final BorderRadiusGeometry? borderRadius; + + /// Overrides the stop indicator color of the [LinearProgressIndicator]. + /// + /// If [LinearProgressIndicator.year2023] is false or [ThemeData.useMaterial3] + /// is false, then no stop indicator will be drawn. + final Color? stopIndicatorColor; + + /// Overrides the stop indicator radius of the [LinearProgressIndicator]. + /// + /// If [LinearProgressIndicator.year2023] is false or [ThemeData.useMaterial3] + /// is false, then no stop indicator will be drawn. + final double? stopIndicatorRadius; + + /// Overrides the gap between the [LinearProgressIndicator]. + /// + /// If [LinearProgressIndicator.year2023] is false or [ThemeData.useMaterial3] + /// is false, then no track gap will be drawn. + final double? trackGap; + /// Creates a copy of this object but with the given fields replaced with the /// new values. ProgressIndicatorThemeData copyWith({ @@ -74,6 +99,10 @@ class ProgressIndicatorThemeData with Diagnosticable { double? linearMinHeight, Color? circularTrackColor, Color? refreshBackgroundColor, + BorderRadiusGeometry? borderRadius, + Color? stopIndicatorColor, + double? stopIndicatorRadius, + double? trackGap, }) { return ProgressIndicatorThemeData( color: color ?? this.color, @@ -81,6 +110,10 @@ class ProgressIndicatorThemeData with Diagnosticable { linearMinHeight : linearMinHeight ?? this.linearMinHeight, circularTrackColor : circularTrackColor ?? this.circularTrackColor, refreshBackgroundColor : refreshBackgroundColor ?? this.refreshBackgroundColor, + borderRadius : borderRadius ?? this.borderRadius, + stopIndicatorColor : stopIndicatorColor ?? this.stopIndicatorColor, + stopIndicatorRadius : stopIndicatorRadius ?? this.stopIndicatorRadius, + trackGap : trackGap ?? this.trackGap, ); } @@ -97,6 +130,10 @@ class ProgressIndicatorThemeData with Diagnosticable { linearMinHeight : lerpDouble(a?.linearMinHeight, b?.linearMinHeight, t), circularTrackColor : Color.lerp(a?.circularTrackColor, b?.circularTrackColor, t), refreshBackgroundColor : Color.lerp(a?.refreshBackgroundColor, b?.refreshBackgroundColor, t), + borderRadius : BorderRadiusGeometry.lerp(a?.borderRadius, b?.borderRadius, t), + stopIndicatorColor : Color.lerp(a?.stopIndicatorColor, b?.stopIndicatorColor, t), + stopIndicatorRadius : lerpDouble(a?.stopIndicatorRadius, b?.stopIndicatorRadius, t), + trackGap : lerpDouble(a?.trackGap, b?.trackGap, t), ); } @@ -107,6 +144,10 @@ class ProgressIndicatorThemeData with Diagnosticable { linearMinHeight, circularTrackColor, refreshBackgroundColor, + borderRadius, + stopIndicatorColor, + stopIndicatorRadius, + trackGap, ); @override @@ -122,7 +163,11 @@ class ProgressIndicatorThemeData with Diagnosticable { && other.linearTrackColor == linearTrackColor && other.linearMinHeight == linearMinHeight && other.circularTrackColor == circularTrackColor - && other.refreshBackgroundColor == refreshBackgroundColor; + && other.refreshBackgroundColor == refreshBackgroundColor + && other.borderRadius == borderRadius + && other.stopIndicatorColor == stopIndicatorColor + && other.stopIndicatorRadius == stopIndicatorRadius + && other.trackGap == trackGap; } @override @@ -133,6 +178,10 @@ class ProgressIndicatorThemeData with Diagnosticable { properties.add(DoubleProperty('linearMinHeight', linearMinHeight, defaultValue: null)); properties.add(ColorProperty('circularTrackColor', circularTrackColor, defaultValue: null)); properties.add(ColorProperty('refreshBackgroundColor', refreshBackgroundColor, defaultValue: null)); + properties.add(DiagnosticsProperty('borderRadius', borderRadius, defaultValue: null)); + properties.add(ColorProperty('stopIndicatorColor', stopIndicatorColor, defaultValue: null)); + properties.add(DoubleProperty('stopIndicatorRadius', stopIndicatorRadius, defaultValue: null)); + properties.add(DoubleProperty('trackGap', trackGap, defaultValue: null)); } } diff --git a/packages/flutter/test/material/progress_indicator_test.dart b/packages/flutter/test/material/progress_indicator_test.dart index 735faff2a2d..e5558b07b33 100644 --- a/packages/flutter/test/material/progress_indicator_test.dart +++ b/packages/flutter/test/material/progress_indicator_test.dart @@ -1300,6 +1300,257 @@ void main() { expect(padding.padding, testIndicatorMargin); expect(innerPadding.padding, testIndicatorPadding); }); + + testWidgets('LinearProgressIndicator default stop indicator when year2023 is false', (WidgetTester tester) async { + Widget buildIndicator({ required TextDirection textDirection }) { + return Directionality( + textDirection: textDirection, + child: const Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + value: 0.5, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildIndicator(textDirection: TextDirection.ltr)); + expect( + find.byType(LinearProgressIndicator), + paints..circle(x: 198.0, y: 2.0, radius: 2.0, color: theme.colorScheme.primary), + ); + + await tester.pumpWidget(buildIndicator(textDirection: TextDirection.rtl)); + expect( + find.byType(LinearProgressIndicator), + paints..circle(x: 2.0, y: 2.0, radius: 2.0, color: theme.colorScheme.primary) + ); + }); + + testWidgets('Indeterminate LinearProgressIndicator does not paint stop indicator', (WidgetTester tester) async { + Widget buildIndicator({ double? value }) { + return Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + value: value, + ), + ), + ), + ); + } + + // Determinate LinearProgressIndicator paints stop indicator. + await tester.pumpWidget(buildIndicator(value: 0.5)); + expect( + find.byType(LinearProgressIndicator), + // Stop indicator. + paints..circle(x: 198.0, y: 2.0, radius: 2.0, color: theme.colorScheme.primary), + ); + + // Indeterminate LinearProgressIndicator does not paint stop indicator. + await tester.pumpWidget(buildIndicator()); + expect( + find.byType(LinearProgressIndicator), + // Stop indicator. + isNot(paints..circle(x: 198.0, y: 2.0, radius: 2.0, color: theme.colorScheme.primary)), + ); + }); + + testWidgets('Can customise LinearProgressIndicator stop indicator when year2023 is false', (WidgetTester tester) async { + const Color stopIndicatorColor = Color(0XFF00FF00); + const double stopIndicatorRadius = 5.0; + Widget buildIndicator({ Color? stopIndicatorColor, double? stopIndicatorRadius }) { + return Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + stopIndicatorColor: stopIndicatorColor, + stopIndicatorRadius: stopIndicatorRadius, + minHeight: 20.0, + value: 0.5, + ), + ), + ), + ); + } + + // Test customized stop indicator. + await tester.pumpWidget(buildIndicator( + stopIndicatorColor: stopIndicatorColor, + stopIndicatorRadius: stopIndicatorRadius, + )); + expect( + find.byType(LinearProgressIndicator), + // Stop indicator. + paints..circle(x: 190.0, y: 10.0, radius: stopIndicatorRadius, color: stopIndicatorColor), + ); + + // Remove stop indicator. + await tester.pumpWidget(buildIndicator(stopIndicatorRadius: 0)); + expect( + find.byType(LinearProgressIndicator), + // Stop indicator. + isNot(paints..circle(color: stopIndicatorColor)), + ); + + // Test stop indicator with transparent color. + await tester.pumpWidget(buildIndicator(stopIndicatorColor: const Color(0x00000000))); + expect( + find.byType(LinearProgressIndicator), + // Stop indicator. + paints..circle(color: const Color(0x00000000)), + ); + }); + + testWidgets('Stop indicator size cannot be larger than the progress indicator', (WidgetTester tester) async { + Widget buildIndicator({ double? stopIndicatorRadius, double? minHeight }) { + return Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + stopIndicatorRadius: stopIndicatorRadius, + minHeight: minHeight, + value: 0.5, + ), + ), + ), + ); + } + + // Test stop indicator radius equals to minHeight. + await tester.pumpWidget(buildIndicator(stopIndicatorRadius: 10.0, minHeight: 20.0)); + expect( + find.byType(LinearProgressIndicator), + paints..circle(x: 190.0, y: 10.0, radius: 10.0, color: theme.colorScheme.primary), + ); + + // Test stop indicator radius larger than minHeight. + await tester.pumpWidget(buildIndicator(stopIndicatorRadius: 30.0, minHeight: 20.0)); + expect( + find.byType(LinearProgressIndicator), + // Stop indicator radius is clamped to minHeight. + paints..circle(x: 190.0, y: 10.0, radius: 10.0, color: theme.colorScheme.primary), + ); + }); + + testWidgets('LinearProgressIndicator default track gap when year2023 is false', (WidgetTester tester) async { + const double defaultTrackGap = 4.0; + Widget buildIndicator({ required TextDirection textDirection }) { + return Directionality( + textDirection: textDirection, + child: const Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + value: 0.5, + ), + ), + ), + ); + } + + // Test default track gap in LTR. + await tester.pumpWidget(buildIndicator(textDirection: TextDirection.ltr)); + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(100.0 + defaultTrackGap, 0.0, 200.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.secondaryContainer, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.primary, + ), + ); + + // Test default track gap in RTL. + await tester.pumpWidget(buildIndicator(textDirection: TextDirection.rtl)); + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0 - defaultTrackGap, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.secondaryContainer, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(100.0, 0.0, 200.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.primary, + ), + ); + }); + + testWidgets('Can customise LinearProgressIndicator track gap when year2023 is false', (WidgetTester tester) async { + const double customTrackGap = 12.0; + const double noTrackGap = 0.0; + Widget buildIndicator({ double? trackGap }) { + return Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + trackGap: trackGap, + value: 0.5, + ), + ), + ), + ); + } + + // Test customized track gap. + await tester.pumpWidget(buildIndicator(trackGap: customTrackGap)); + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(100.0 + customTrackGap, 0.0, 200.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.secondaryContainer, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.primary, + ), + ); + + // Remove track gap. + await tester.pumpWidget(buildIndicator(trackGap: noTrackGap)); + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 200.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.secondaryContainer, + ) + // Active indicator. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0, 4.0, const Radius.circular(2.0)), + color: theme.colorScheme.primary, + ), + ); + }); } class _RefreshProgressIndicatorGolden extends StatefulWidget { diff --git a/packages/flutter/test/material/progress_indicator_theme_test.dart b/packages/flutter/test/material/progress_indicator_theme_test.dart index 8dc68289f1a..2e74f9beae7 100644 --- a/packages/flutter/test/material/progress_indicator_theme_test.dart +++ b/packages/flutter/test/material/progress_indicator_theme_test.dart @@ -16,4 +16,170 @@ void main() { const ProgressIndicatorThemeData data = ProgressIndicatorThemeData(); expect(identical(ProgressIndicatorThemeData.lerp(data, data, 0.5), data), true); }); + + testWidgets('Can theme LinearProgressIndicator using ProgressIndicatorTheme', (WidgetTester tester) async { + const Color color = Color(0XFF00FF00); + const Color linearTrackColor = Color(0XFFFF0000); + const double linearMinHeight = 25.0; + const double borderRadius = 8.0; + final ThemeData theme = ThemeData( + progressIndicatorTheme: ProgressIndicatorThemeData( + color: color, + linearTrackColor: linearTrackColor, + linearMinHeight: linearMinHeight, + borderRadius: BorderRadius.circular(borderRadius), + ), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Scaffold( + body: Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + value: 0.5, + ), + ), + ), + ), + ), + ); + + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 200.0, linearMinHeight, const Radius.circular(borderRadius)), + color: linearTrackColor, + ) + // Active indicator. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0, linearMinHeight, const Radius.circular(borderRadius)), + color: color, + ), + ); + }); + + testWidgets('Can theme LinearProgressIndicator with year2023 to false', (WidgetTester tester) async { + const Color color = Color(0XFF00FF00); + const Color linearTrackColor = Color(0XFFFF0000); + const double linearMinHeight = 25.0; + const double borderRadius = 8.0; + const Color stopIndicatorColor = Color(0XFF0000FF); + const double stopIndicatorRadius = 10.0; + const double trackGap = 16.0; + final ThemeData theme = ThemeData( + progressIndicatorTheme: ProgressIndicatorThemeData( + color: color, + linearTrackColor: linearTrackColor, + linearMinHeight: linearMinHeight, + borderRadius: BorderRadius.circular(borderRadius), + stopIndicatorColor: stopIndicatorColor, + stopIndicatorRadius: stopIndicatorRadius, + trackGap: trackGap, + ), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Scaffold( + body: Center( + child: SizedBox( + width: 200.0, + child: LinearProgressIndicator( + year2023: false, + value: 0.5, + ), + ), + ), + ), + ), + ); + + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(100.0 + trackGap, 0.0, 200.0, linearMinHeight, const Radius.circular(borderRadius)), + color: linearTrackColor, + ) + // Stop indicator. + ..circle( + x: 187.5, + y: 12.5, + radius: stopIndicatorRadius, + color: stopIndicatorColor, + ) + // Active indicator. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0, linearMinHeight, const Radius.circular(borderRadius)), + color: color, + ), + ); + }); + + testWidgets('Local ProgressIndicatorTheme takes precedence over inherited ProgressIndicatorTheme', (WidgetTester tester) async { + const Color color = Color(0XFFFF00FF); + const Color linearTrackColor = Color(0XFF00FFFF); + const double linearMinHeight = 20.0; + const double borderRadius = 6.0; + const Color stopIndicatorColor = Color(0XFFFFFF00); + const double stopIndicatorRadius = 8.0; + const double trackGap = 12.0; + final ThemeData theme = ThemeData( + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: Color(0XFF00FF00), + linearTrackColor: Color(0XFFFF0000), + linearMinHeight: 25.0, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + stopIndicatorColor: Color(0XFF0000FF), + stopIndicatorRadius: 10.0, + trackGap: 16.0, + ), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + body: Center( + child: ProgressIndicatorTheme( + data: ProgressIndicatorThemeData( + color: color, + linearTrackColor: linearTrackColor, + linearMinHeight: linearMinHeight, + borderRadius: BorderRadius.circular(borderRadius), + stopIndicatorColor: stopIndicatorColor, + stopIndicatorRadius: stopIndicatorRadius, + trackGap: trackGap, + ), + child: const SizedBox( + width: 200.0, + child: LinearProgressIndicator( + value: 0.5, + ), + ), + ), + ), + ), + ), + ); + + expect( + find.byType(LinearProgressIndicator), + paints + // Track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 200.0, linearMinHeight, const Radius.circular(borderRadius)), + color: linearTrackColor, + ) + // Active indicator. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 0.0, 100.0, linearMinHeight, const Radius.circular(borderRadius)), + color: color, + ), + ); + }); }