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

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  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: <Widget>[
                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'),
        ),
      ),
    );
  }
}
```

</details>

### Preview

<img width="824" alt="Screenshot 2024-09-09 at 13 53 10" src="https://github.com/user-attachments/assets/d12e56a5-f196-4011-8266-c7ab96be96b2">
This commit is contained in:
Taha Tesser 2024-10-30 20:14:11 +02:00 committed by GitHub
parent cf4a4b8162
commit b8dcb0c3c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 754 additions and 93 deletions

View File

@ -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,

1 Versions used 6_1_0
585 md.comp.primary-navigation-tab.with-label-text.active.label-text.color
586 md.comp.primary-navigation-tab.with-label-text.inactive.label-text.color
587 md.comp.primary-navigation-tab.with-label-text.label-text.text-style
588 md.comp.progress-indicator.active-indicator-track-space
589 md.comp.progress-indicator.active-indicator.color
590 md.comp.progress-indicator.stop-indicator.color
591 md.comp.progress-indicator.stop-indicator.size
592 md.comp.progress-indicator.track.color
593 md.comp.progress-indicator.track.thickness
594 md.comp.radio-button.disabled.selected.icon.color

View File

@ -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')};
}
''';
}

View File

@ -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<ProgressIndicatorExample> createState() => _ProgressIndicatorExampleState();
State<ProgressIndicatorExample> createState() =>
_ProgressIndicatorExampleState();
}
class _ProgressIndicatorExampleState extends State<ProgressIndicatorExample> with TickerProviderStateMixin {
class _ProgressIndicatorExampleState extends State<ProgressIndicatorExample>
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<ProgressIndicatorExample> wit
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
spacing: 16.0,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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(),
],
),
),

View File

@ -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<ProgressIndicatorExample> createState() => _ProgressIndicatorExampleState();
State<ProgressIndicatorExample> createState() =>
_ProgressIndicatorExampleState();
}
class _ProgressIndicatorExampleState extends State<ProgressIndicatorExample> with TickerProviderStateMixin {
class _ProgressIndicatorExampleState extends State<ProgressIndicatorExample>
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<ProgressIndicatorExample> 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<ProgressIndicatorExample> wit
children: <Widget>[
Expanded(
child: Text(
'determinate Mode',
'${determinate ? 'Determinate' : 'Indeterminate'} Mode',
style: Theme.of(context).textTheme.titleSmall,
),
),

View File

@ -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);
});
}

View File

@ -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(

View File

@ -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<LinearProgressIndicator> createState() => _LinearProgressIndicatorState();
@ -355,9 +464,12 @@ class _LinearProgressIndicatorState extends State<LinearProgressIndicator> 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<LinearProgressIndicator> 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

View File

@ -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<BorderRadiusGeometry>('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));
}
}

View File

@ -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 {

View File

@ -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,
),
);
});
}