I want to change color of line in stepper, Look at the below picture
my try here :
Theme( data: ThemeData(canvasColor: whiteColor), child: Stepper( type: StepperType.horizontal, steps: getSteps(), currentStep: currentStep, elevation: 2, ), ),
Please help to continue my code.
2
Let me be short and clear.
TO CHANGE STEPPER LINE COLOR
Unfortunately, the Flutter code for the Stepper does not support the change of the Stepper line color (_buildLine) and more.
Stepper
_buildLine
So I created a custom stepper called CusStepper that uses CusStep
CusStepper
CusStep
NOTE: This CusStepper has the same properties and behaves like the normal Stepper with the only difference of lineColor property.
lineColor
Steps on how to use the CusStepper
CusStepper code to paste in the .dart file created in step 1
// THIS IS A CUSTOM STEPPER THAT ADDS LINE COLOR import 'package:flutter/material.dart'; enum CusStepState { indexed, editing, complete, disabled, error, } enum CusStepperType { vertical, horizontal, } @immutable class ControlsDetails { const ControlsDetails({ required this.currentStep, required this.stepIndex, this.onStepCancel, this.onStepContinue, }); final int currentStep; final int stepIndex; final VoidCallback? onStepContinue; final VoidCallback? onStepCancel; bool get isActive => currentStep == stepIndex; } typedef ControlsWidgetBuilder = Widget Function( BuildContext context, ControlsDetails details); const TextStyle _kStepStyle = TextStyle( fontSize: 12.0, color: Colors.white, ); const Color _kErrorLight = Colors.red; final Color _kErrorDark = Colors.red.shade400; const Color _kCircleActiveLight = Colors.white; const Color _kCircleActiveDark = Colors.black87; const Color _kDisabledLight = Colors.black38; const Color _kDisabledDark = Colors.white38; const double _kStepSize = 24.0; const double _kTriangleHeight = _kStepSize * 0.866025; @immutable class CusStep { const CusStep({ required this.title, this.subtitle, required this.content, this.state = CusStepState.indexed, this.isActive = false, this.label, }); final Widget title; final Widget? subtitle; final Widget content; final CusStepState state; final bool isActive; final Widget? label; } class CusStepper extends StatefulWidget { const CusStepper({ super.key, required this.steps, this.physics, this.type = CusStepperType.vertical, this.currentStep = 0, this.onStepTapped, this.onStepContinue, this.onStepCancel, this.controlsBuilder, this.elevation, this.margin, this.lineColor = Colors.grey, }) : assert(0 <= currentStep && currentStep < steps.length); final List<CusStep> steps; final ScrollPhysics? physics; final CusStepperType type; final int currentStep; final ValueChanged<int>? onStepTapped; final VoidCallback? onStepContinue; final VoidCallback? onStepCancel; final ControlsWidgetBuilder? controlsBuilder; final double? elevation; final EdgeInsetsGeometry? margin; final Color lineColor; @override State<CusStepper> createState() => _CusStepperState(); } class _CusStepperState extends State<CusStepper> with TickerProviderStateMixin { late List<GlobalKey> _keys; final Map<int, CusStepState> _oldStates = <int, CusStepState>{}; @override void initState() { super.initState(); _keys = List<GlobalKey>.generate( widget.steps.length, (int i) => GlobalKey(), ); for (int i = 0; i < widget.steps.length; i += 1) { _oldStates[i] = widget.steps[i].state; } } @override void didUpdateWidget(CusStepper oldWidget) { super.didUpdateWidget(oldWidget); assert(widget.steps.length == oldWidget.steps.length); for (int i = 0; i < oldWidget.steps.length; i += 1) { _oldStates[i] = oldWidget.steps[i].state; } } bool _isFirst(int index) { return index == 0; } bool _isLast(int index) { return widget.steps.length - 1 == index; } bool _isCurrent(int index) { return widget.currentStep == index; } bool _isDark() { return Theme.of(context).brightness == Brightness.dark; } bool _isLabel() { for (final CusStep step in widget.steps) { if (step.label != null) { return true; } } return false; } Widget _buildLine(bool visible) { return Container( width: visible ? 1.0 : 0.0, height: 16.0, color: widget.lineColor, ); } Widget _buildCircleChild(int index, bool oldState) { final CusStepState state = oldState ? _oldStates[index]! : widget.steps[index].state; final bool isDarkActive = _isDark() && widget.steps[index].isActive; switch (state) { case CusStepState.indexed: case CusStepState.disabled: return Text( '${index + 1}', style: isDarkActive ? _kStepStyle.copyWith(color: Colors.black87) : _kStepStyle, ); case CusStepState.editing: return Icon( Icons.edit, color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight, size: 18.0, ); case CusStepState.complete: return Icon( Icons.check, color: isDarkActive ? _kCircleActiveDark : _kCircleActiveLight, size: 18.0, ); case CusStepState.error: return const Text('!', style: _kStepStyle); } } Color _circleColor(int index) { final ColorScheme colorScheme = Theme.of(context).colorScheme; if (!_isDark()) { return widget.steps[index].isActive ? colorScheme.primary : colorScheme.onSurface.withOpacity(0.38); } else { return widget.steps[index].isActive ? colorScheme.secondary : colorScheme.background; } } Widget _buildCircle(int index, bool oldState) { return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), width: _kStepSize, height: _kStepSize, child: AnimatedContainer( curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, decoration: BoxDecoration( color: _circleColor(index), shape: BoxShape.circle, ), child: Center( child: _buildCircleChild( index, oldState && widget.steps[index].state == CusStepState.error), ), ), ); } Widget _buildTriangle(int index, bool oldState) { return Container( margin: const EdgeInsets.symmetric(vertical: 8.0), width: _kStepSize, height: _kStepSize, child: Center( child: SizedBox( width: _kStepSize, height: _kTriangleHeight, // Height of 24dp-long-sided equilateral triangle. child: CustomPaint( painter: _TrianglePainter( color: _isDark() ? _kErrorDark : _kErrorLight, ), child: Align( alignment: const Alignment( 0.0, 0.8), // 0.8 looks better than the geometrical 0.33. child: _buildCircleChild(index, oldState && widget.steps[index].state != CusStepState.error), ), ), ), ), ); } Widget _buildIcon(int index) { if (widget.steps[index].state != _oldStates[index]) { return AnimatedCrossFade( firstChild: _buildCircle(index, true), secondChild: _buildTriangle(index, true), firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), sizeCurve: Curves.fastOutSlowIn, crossFadeState: widget.steps[index].state == CusStepState.error ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: kThemeAnimationDuration, ); } else { if (widget.steps[index].state != CusStepState.error) { return _buildCircle(index, false); } else { return _buildTriangle(index, false); } } } Widget _buildVerticalControls(int stepIndex) { if (widget.controlsBuilder != null) { return widget.controlsBuilder!( context, ControlsDetails( currentStep: widget.currentStep, onStepContinue: widget.onStepContinue, onStepCancel: widget.onStepCancel, stepIndex: stepIndex, ), ); } final Color cancelColor; switch (Theme.of(context).brightness) { case Brightness.light: cancelColor = Colors.black54; break; case Brightness.dark: cancelColor = Colors.white70; break; } final ThemeData themeData = Theme.of(context); final ColorScheme colorScheme = themeData.colorScheme; final MaterialLocalizations localizations = MaterialLocalizations.of(context); const OutlinedBorder buttonShape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2))); const EdgeInsets buttonPadding = EdgeInsets.symmetric(horizontal: 16.0); return Container( margin: const EdgeInsets.only(top: 16.0), child: ConstrainedBox( constraints: const BoxConstraints.tightFor(height: 48.0), child: Row( children: <Widget>[ TextButton( onPressed: widget.onStepContinue, style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith<Color?>( (Set<MaterialState> states) { return states.contains(MaterialState.disabled) ? null : (_isDark() ? colorScheme.onSurface : colorScheme.onPrimary); }), backgroundColor: MaterialStateProperty.resolveWith<Color?>( (Set<MaterialState> states) { return _isDark() || states.contains(MaterialState.disabled) ? null : colorScheme.primary; }), padding: const MaterialStatePropertyAll<EdgeInsetsGeometry>( buttonPadding), shape: const MaterialStatePropertyAll<OutlinedBorder>(buttonShape), ), child: Text(themeData.useMaterial3 ? localizations.continueButtonLabel : localizations.continueButtonLabel.toUpperCase()), ), Container( margin: const EdgeInsetsDirectional.only(start: 8.0), child: TextButton( onPressed: widget.onStepCancel, style: TextButton.styleFrom( foregroundColor: cancelColor, padding: buttonPadding, shape: buttonShape, ), child: Text(themeData.useMaterial3 ? localizations.cancelButtonLabel : localizations.cancelButtonLabel.toUpperCase()), ), ), ], ), ), ); } TextStyle _titleStyle(int index) { final ThemeData themeData = Theme.of(context); final TextTheme textTheme = themeData.textTheme; switch (widget.steps[index].state) { case CusStepState.indexed: case CusStepState.editing: case CusStepState.complete: return textTheme.bodyLarge!; case CusStepState.disabled: return textTheme.bodyLarge!.copyWith( color: _isDark() ? _kDisabledDark : _kDisabledLight, ); case CusStepState.error: return textTheme.bodyLarge!.copyWith( color: _isDark() ? _kErrorDark : _kErrorLight, ); } } TextStyle _subtitleStyle(int index) { final ThemeData themeData = Theme.of(context); final TextTheme textTheme = themeData.textTheme; switch (widget.steps[index].state) { case CusStepState.indexed: case CusStepState.editing: case CusStepState.complete: return textTheme.bodySmall!; case CusStepState.disabled: return textTheme.bodySmall!.copyWith( color: _isDark() ? _kDisabledDark : _kDisabledLight, ); case CusStepState.error: return textTheme.bodySmall!.copyWith( color: _isDark() ? _kErrorDark : _kErrorLight, ); } } TextStyle _labelStyle(int index) { final ThemeData themeData = Theme.of(context); final TextTheme textTheme = themeData.textTheme; switch (widget.steps[index].state) { case CusStepState.indexed: case CusStepState.editing: case CusStepState.complete: return textTheme.bodyLarge!; case CusStepState.disabled: return textTheme.bodyLarge!.copyWith( color: _isDark() ? _kDisabledDark : _kDisabledLight, ); case CusStepState.error: return textTheme.bodyLarge!.copyWith( color: _isDark() ? _kErrorDark : _kErrorLight, ); } } Widget _buildHeaderText(int index) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[ AnimatedDefaultTextStyle( style: _titleStyle(index), duration: kThemeAnimationDuration, curve: Curves.fastOutSlowIn, child: widget.steps[index].title, ), if (widget.steps[index].subtitle != null) Container( margin: const EdgeInsets.only(top: 2.0), child: AnimatedDefaultTextStyle( style: _subtitleStyle(index), duration: kThemeAnimationDuration, curve: Curves.fastOutSlowIn, child: widget.steps[index].subtitle!, ), ), ], ); } Widget _buildLabelText(int index) { if (widget.steps[index].label != null) { return AnimatedDefaultTextStyle( style: _labelStyle(index), duration: kThemeAnimationDuration, child: widget.steps[index].label!, ); } return const SizedBox.shrink(); } Widget _buildVerticalHeader(int index) { return Container( margin: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( children: <Widget>[ Column( children: <Widget>[ _buildLine(!_isFirst(index)), _buildIcon(index), _buildLine(!_isLast(index)), ], ), Expanded( child: Container( margin: const EdgeInsetsDirectional.only(start: 12.0), child: _buildHeaderText(index), ), ), ], ), ); } Widget _buildVerticalBody(int index) { return Stack( children: <Widget>[ PositionedDirectional( start: 24.0, top: 0.0, bottom: 0.0, child: SizedBox( width: 24.0, child: Center( child: SizedBox( width: _isLast(index) ? 0.0 : 1.0, child: Container( color: widget.lineColor, ), ), ), ), ), AnimatedCrossFade( firstChild: Container(height: 0.0), secondChild: Container( margin: widget.margin ?? const EdgeInsetsDirectional.only( start: 60.0, end: 24.0, bottom: 24.0, ), child: Column( children: <Widget>[ widget.steps[index].content, _buildVerticalControls(index), ], ), ), firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn), secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn), sizeCurve: Curves.fastOutSlowIn, crossFadeState: _isCurrent(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: kThemeAnimationDuration, ), ], ); } Widget _buildVertical() { return ListView( shrinkWrap: true, physics: widget.physics, children: <Widget>[ for (int i = 0; i < widget.steps.length; i += 1) Column( key: _keys[i], children: <Widget>[ InkWell( onTap: widget.steps[i].state != CusStepState.disabled ? () { // In the vertical case we need to scroll to the newly tapped // step. Scrollable.ensureVisible( _keys[i].currentContext!, curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, ); widget.onStepTapped?.call(i); } : null, canRequestFocus: widget.steps[i].state != CusStepState.disabled, child: _buildVerticalHeader(i), ), _buildVerticalBody(i), ], ), ], ); } Widget _buildHorizontal() { final List<Widget> children = <Widget>[ for (int i = 0; i < widget.steps.length; i += 1) ...<Widget>[ InkResponse( onTap: widget.steps[i].state != CusStepState.disabled ? () { widget.onStepTapped?.call(i); } : null, canRequestFocus: widget.steps[i].state != CusStepState.disabled, child: Row( children: <Widget>[ SizedBox( height: _isLabel() ? 104.0 : 72.0, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ if (widget.steps[i].label != null) const SizedBox( height: 24.0, ), Center(child: _buildIcon(i)), if (widget.steps[i].label != null) SizedBox( height: 24.0, child: _buildLabelText(i), ), ], ), ), Container( margin: const EdgeInsetsDirectional.only(start: 12.0), child: _buildHeaderText(i), ), ], ), ), if (!_isLast(i)) Expanded( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), height: 1.0, color: widget.lineColor, ), ), ], ]; final List<Widget> stepPanels = <Widget>[]; for (int i = 0; i < widget.steps.length; i += 1) { stepPanels.add( Visibility( maintainState: true, visible: i == widget.currentStep, child: widget.steps[i].content, ), ); } return Column( children: <Widget>[ Material( elevation: widget.elevation ?? 2, child: Container( margin: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( children: children, ), ), ), Expanded( child: ListView( physics: widget.physics, padding: const EdgeInsets.all(24.0), children: <Widget>[ AnimatedSize( curve: Curves.fastOutSlowIn, duration: kThemeAnimationDuration, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: stepPanels), ), _buildVerticalControls(widget.currentStep), ], ), ), ], ); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterialLocalizations(context)); assert(() { if (context.findAncestorWidgetOfExactType<Stepper>() != null) { throw FlutterError( 'Steppers must not be nested.n' 'The material specification advises that one should avoid embedding ' 'steppers within steppers. ' 'https://material.io/archive/guidelines/components/steppers.html#steppers-usage', ); } return true; }()); switch (widget.type) { case CusStepperType.vertical: return _buildVertical(); case CusStepperType.horizontal: return _buildHorizontal(); } } } class _TrianglePainter extends CustomPainter { _TrianglePainter({ required this.color, }); final Color color; @override bool hitTest(Offset point) => true; @override bool shouldRepaint(_TrianglePainter oldPainter) { return oldPainter.color != color; } @override void paint(Canvas canvas, Size size) { final double base = size.width; final double halfBase = size.width / 2.0; final double height = size.height; final List<Offset> points = <Offset>[ Offset(0.0, height), Offset(base, height), Offset(halfBase, 0.0), ]; canvas.drawPath( Path()..addPolygon(points, true), Paint()..color = color, ); } }
EXAMPLE CODEBASE ON HOW TO USE THE CusStepper
import 'package:flutter/material.dart'; import 'package:tester/stepper/stepper.dart'; // this is the file path of where you store your `cusStepper` void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Tester', home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _index = 0; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: CusStepper( lineColor: Colors.red, // new line add for the color change currentStep: _index, onStepCancel: () { if (_index > 0) { setState(() { _index -= 1; }); } }, onStepContinue: () { if (_index <= 0) { setState(() { _index += 1; }); } }, onStepTapped: (int index) { setState(() { _index = index; }); }, steps: <CusStep>[ CusStep( title: const Text('Step 1 title'), content: Container( alignment: Alignment.centerLeft, child: const Text('Content for Step 1')), ), const CusStep( title: Text('Step 2 title'), content: Text('Content for Step 2'), ), ], ), ), ); } }
OUTPUT
Leave a comment below if you have any questions or help on this. Bye!
The Flutter Default stepper Line has Static color use so can’t change this. chack below Image.
Here https://fluttergems.dev/stepper mention many stepper package used as you want.
Click here to cancel reply.
2
Answers
Let me be short and clear.
TO CHANGE STEPPER LINE COLOR
Unfortunately, the Flutter code for the
Stepper
does not support the change of the Stepper line color (_buildLine
) and more.So I created a custom stepper called
CusStepper
that usesCusStep
NOTE: This
CusStepper
has the same properties and behaves like the normalStepper
with the only difference oflineColor
property.Steps on how to use the
CusStepper
CusStepper
codeCusStepper
in the file.lineColor
params. on theCusStepper
CusStepper
code to paste in the .dart file created in step 1EXAMPLE CODEBASE ON HOW TO USE THE
CusStepper
OUTPUT
Leave a comment below if you have any questions or help on this.
Bye!
The Flutter Default stepper Line has Static color use so can’t change this.
chack below Image.
Here https://fluttergems.dev/stepper mention many stepper package used as you want.