diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart index 8978fe638c..3d374ddf3e 100644 --- a/packages/flutter/lib/src/material/stepper.dart +++ b/packages/flutter/lib/src/material/stepper.dart @@ -211,6 +211,8 @@ class Stepper extends StatefulWidget { this.controlsBuilder, this.elevation, this.margin, + this.connectorColor, + this.connectorThickness, this.stepIconBuilder, }) : assert(0 <= currentStep && currentStep < steps.length); @@ -311,6 +313,19 @@ class Stepper extends StatefulWidget { /// Custom margin on vertical stepper. final EdgeInsetsGeometry? margin; + /// Customize connected lines colors. + /// + /// Resolves in the following states: + /// * [MaterialState.selected]. + /// * [MaterialState.disabled]. + /// + /// If not set then the widget will use default colors, primary for selected state + /// and grey.shade400 for disabled state. + final MaterialStateProperty? connectorColor; + + /// The thickness of the connecting lines. + final double? connectorThickness; + /// Callback for creating custom icons for the [steps]. /// /// When overriding icon for [StepState.error], please return @@ -375,11 +390,23 @@ class _StepperState extends State with TickerProviderStateMixin { return false; } - Widget _buildLine(bool visible) { + Color _connectorColor(bool isActive) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final Set states = { + if (isActive) MaterialState.selected else MaterialState.disabled, + }; + final Color? resolvedConnectorColor = widget.connectorColor?.resolve(states); + if (resolvedConnectorColor != null) { + return resolvedConnectorColor; + } + return isActive ? colorScheme.primary : Colors.grey.shade400; + } + + Widget _buildLine(bool visible, bool isActive) { return Container( - width: visible ? 1.0 : 0.0, + width: visible ? widget.connectorThickness ?? 1.0 : 0.0, height: 16.0, - color: Colors.grey.shade400, + color: _connectorColor(isActive), ); } @@ -415,11 +442,19 @@ class _StepperState extends State with TickerProviderStateMixin { } Color _circleColor(int index) { + final bool isActive = widget.steps[index].isActive; final ColorScheme colorScheme = Theme.of(context).colorScheme; + final Set states = { + if (isActive) MaterialState.selected else MaterialState.disabled, + }; + final Color? resolvedConnectorColor = widget.connectorColor?.resolve(states); + if (resolvedConnectorColor != null) { + return resolvedConnectorColor; + } if (!_isDark()) { - return widget.steps[index].isActive ? colorScheme.primary : colorScheme.onSurface.withOpacity(0.38); + return isActive ? colorScheme.primary : colorScheme.onSurface.withOpacity(0.38); } else { - return widget.steps[index].isActive ? colorScheme.secondary : colorScheme.background; + return isActive ? colorScheme.secondary : colorScheme.background; } } @@ -659,6 +694,7 @@ class _StepperState extends State with TickerProviderStateMixin { } Widget _buildVerticalHeader(int index) { + final bool isActive = widget.steps[index].isActive; return Container( margin: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( @@ -667,9 +703,9 @@ class _StepperState extends State with TickerProviderStateMixin { children: [ // Line parts are always added in order for the ink splash to // flood the tips of the connector lines. - _buildLine(!_isFirst(index)), + _buildLine(!_isFirst(index), isActive), _buildIcon(index), - _buildLine(!_isLast(index)), + _buildLine(!_isLast(index), isActive), ], ), Expanded( @@ -694,9 +730,9 @@ class _StepperState extends State with TickerProviderStateMixin { width: 24.0, child: Center( child: SizedBox( - width: _isLast(index) ? 0.0 : 1.0, + width: widget.connectorThickness ?? 1.0, child: Container( - color: Colors.grey.shade400, + color: _connectorColor(widget.steps[index].isActive), ), ), ), @@ -789,9 +825,10 @@ class _StepperState extends State with TickerProviderStateMixin { if (!_isLast(i)) Expanded( child: Container( + key: Key('line$i'), margin: const EdgeInsets.symmetric(horizontal: 8.0), - height: 1.0, - color: Colors.grey.shade400, + height: widget.connectorThickness ?? 1.0, + color: _connectorColor(widget.steps[i+1].isActive), ), ), ], diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index 03f2c5c62c..08aa0af729 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -1260,6 +1260,75 @@ testWidgets('Stepper custom indexed controls test', (WidgetTester tester) async expect(bodyMediumStyle, nextLabelTextWidget.style); }); + testWidgets('Stepper Connector Style', (WidgetTester tester) async { + const Color selectedColor = Colors.black; + const Color disabledColor = Colors.white; + int index = 0; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Stepper( + type: StepperType.horizontal, + connectorColor: MaterialStateProperty.resolveWith((Set states) => + states.contains(MaterialState.selected) + ? selectedColor + : disabledColor), + onStepTapped: (int i) => setState(() => index = i), + currentStep: index, + steps: [ + Step( + isActive: index >= 0, + title: const Text('step1'), + content: const Text('step1 content'), + ), + Step( + isActive: index >= 1, + title: const Text('step2'), + content: const Text('step2 content'), + ), + ], + ); + }, + ), + ), + ), + ) + ); + + Color? circleColor(String circleText) => (tester.widget( + find.widgetWithText(AnimatedContainer, circleText), + ).decoration as BoxDecoration?)?.color; + + Color? lineColor(String keyStep) => tester.widget(find.byKey(Key(keyStep))).color; + + // Step 1 + // check if I'm in step 1 + expect(find.text('step1 content'), findsOneWidget); + expect(find.text('step2 content'), findsNothing); + + expect(circleColor('1'), selectedColor); + expect(circleColor('2'), disabledColor); + // in two steps case there will be single line + expect(lineColor('line0'), disabledColor); + + // now hitting step two + await tester.tap(find.text('step2')); + await tester.pumpAndSettle(); + + // check if I'm in step 1 + expect(find.text('step1 content'), findsNothing); + expect(find.text('step2 content'), findsOneWidget); + + expect(circleColor('1'), selectedColor); + expect(circleColor('2'), selectedColor); + + expect(lineColor('line0'), selectedColor); + }); + testWidgets('Stepper stepIconBuilder test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp(