From d3bdbd9f8a25519e5e10f8d1e700b1b6369dc262 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Sun, 20 Jul 2025 13:15:23 +0330 Subject: [PATCH 01/11] ci: update flutter version in workflow --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 050073f..621e4cf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,7 +27,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.32.0' + flutter-version: '3.32.6' - name: Install dependencies run: flutter pub get From 007276b10bccec09ea3609bc6b4edb0a2006c190 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Sun, 20 Jul 2025 14:06:08 +0330 Subject: [PATCH 02/11] chore: organize the examples in correct order --- example/lib/example_eighteen.dart | 8 +- example/lib/example_fifteen.dart | 77 +++++++--- example/lib/example_fourteen.dart | 137 +++++------------- example/lib/example_nineteen.dart | 6 +- example/lib/example_seventeen.dart | 16 +- example/lib/example_sixteen.dart | 80 ++-------- example/lib/example_twenty.dart | 23 ++- example/lib/example_twenty_one.dart | 37 +++-- example/lib/main.dart | 60 +++++--- ...duce_issues.dart => reproduce_issues.dart} | 4 +- example/lib/reproduce_large_labels.dart | 134 +++++++++++++++++ example/pubspec.lock | 2 +- 12 files changed, 326 insertions(+), 258 deletions(-) rename example/lib/{example_reproduce_issues.dart => reproduce_issues.dart} (93%) create mode 100644 example/lib/reproduce_large_labels.dart diff --git a/example/lib/example_eighteen.dart b/example/lib/example_eighteen.dart index bd2af12..8f6da7e 100644 --- a/example/lib/example_eighteen.dart +++ b/example/lib/example_eighteen.dart @@ -16,13 +16,7 @@ class ExampleEighteen extends StatelessWidget { padding: const EdgeInsets.all(10), controller: stepProgressController, theme: const StepProgressThemeData( - stepLineStyle: StepLineStyle( - borderRadius: Radius.circular(8), - borderStyle: OuterBorderStyle( - isDotted: true, - borderWidth: 3, - ), - ), + enableRippleEffect: true, ), ), bottomNavigationBar: SafeArea( diff --git a/example/lib/example_fifteen.dart b/example/lib/example_fifteen.dart index 3e90949..2d6c387 100644 --- a/example/lib/example_fifteen.dart +++ b/example/lib/example_fifteen.dart @@ -6,43 +6,80 @@ class ExampleFifteen extends StatelessWidget { @override Widget build(BuildContext context) { - final stepProgressController = StepProgressController(totalSteps: 4); + final stepProgressController = StepProgressController(totalSteps: 6); return Scaffold( + backgroundColor: const Color(0xFF444444), appBar: AppBar( - title: const Text('StepProgress - LineLabel'), + title: const Text('StepProgress - Custom Vertical Timeline'), ), body: StepProgress( - totalSteps: 4, + totalSteps: 6, padding: const EdgeInsets.all(10), - lineTitles: const [ - 'line title 1', - 'line title 2', - 'line title 3', - ], + axis: Axis.vertical, + reversed: true, controller: stepProgressController, nodeIconBuilder: (index, completedStepIndex) { if (index <= completedStepIndex) { + //step completed return const Icon( Icons.check, + size: 18, color: Colors.white, ); - } else { - return const Icon( - Icons.more_horiz, - color: Colors.white, + } + return null; + }, + lineLabelBuilder: (index, completedStepIndex) { + // here index is index of current line + // (numbers of lines is equal to toalSteps - 1) + if (index.isEven) { + return Text( + 'December ${index + 10} 2020', + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(color: Colors.white), ); } + return null; + }, + nodeLabelBuilder: (index, completedStepIndex) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2, + children: [ + Text( + 'Invisalign ClinCheck $index', + maxLines: 3, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + decorationColor: const Color(0xFF4e97fc), + color: const Color(0xFF4e97fc), + decoration: TextDecoration.underline, + ), + ), + Text( + '9:20 AM - 9:40 AM', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: const Color(0xFF7e7971), + ), + ), + ], + ); }, theme: const StepProgressThemeData( - lineLabelAlignment: Alignment.bottomCenter, - lineLabelStyle: StepLabelStyle( - defualtColor: Colors.grey, - activeColor: Colors.green, + defaultForegroundColor: Color(0xFF666666), + activeForegroundColor: Color(0xFF4e97fc), + lineLabelAlignment: Alignment.topLeft, + nodeLabelStyle: StepLabelStyle( + maxWidth: double.infinity, + margin: EdgeInsets.all(4), ), - stepLineSpacing: 20, - stepLineStyle: StepLineStyle( - lineThickness: 3, - borderRadius: Radius.circular(4), + lineLabelStyle: StepLabelStyle( + maxWidth: double.infinity, + margin: EdgeInsets.only( + right: 18, + ), ), ), ), diff --git a/example/lib/example_fourteen.dart b/example/lib/example_fourteen.dart index 691b27a..380faf5 100644 --- a/example/lib/example_fourteen.dart +++ b/example/lib/example_fourteen.dart @@ -9,114 +9,47 @@ class ExampleFourteen extends StatelessWidget { final stepProgressController = StepProgressController(totalSteps: 4); return Scaffold( appBar: AppBar( - title: const Text('StepProgress - large labels'), + title: const Text('StepProgress - LineLabel'), ), - body: SingleChildScrollView( - child: Column( - spacing: 18, - children: [ - StepProgress( - totalSteps: 4, - stepSize: 28, - controller: stepProgressController, - nodeTitles: const [ - 'Step 1', - 'Step 2', - 'Step 3', - 'Step 4 and a long title', - ], - theme: const StepProgressThemeData( - nodeLabelAlignment: StepLabelAlignment.top, - ), - ), - StepProgress( - totalSteps: 4, - stepSize: 28, - controller: stepProgressController, - nodeTitles: const [ - 'Step 1', - 'Step 2', - 'Step 3', - 'Step 4 and a long title', - ], - theme: const StepProgressThemeData( - nodeLabelAlignment: StepLabelAlignment.bottom, - ), - ), - StepProgress( - totalSteps: 4, - stepSize: 28, - controller: stepProgressController, - nodeTitles: const [ - 'Step 1 and a long title here', - 'Step 2', - 'Step 3', - 'Step 4 and a long title', - ], - theme: const StepProgressThemeData( - nodeLabelAlignment: StepLabelAlignment.topBottom, - ), - ), - Row( - spacing: 18, - children: [ - StepProgress( - totalSteps: 4, - stepSize: 28, - height: 390, - axis: Axis.vertical, - controller: stepProgressController, - nodeTitles: const [ - 'Step 1', - 'Step 2', - 'Step 3', - 'Step 4 and a long title', - ], - theme: const StepProgressThemeData( - nodeLabelAlignment: StepLabelAlignment.left, - ), - ), - StepProgress( - totalSteps: 4, - stepSize: 28, - height: 390, - axis: Axis.vertical, - controller: stepProgressController, - nodeTitles: const [ - 'Step 1', - 'Step 2', - 'Step 3', - 'Step 4 and a long title', - ], - theme: const StepProgressThemeData( - nodeLabelAlignment: StepLabelAlignment.right, - ), - ), - StepProgress( - totalSteps: 4, - stepSize: 28, - height: 390, - axis: Axis.vertical, - controller: stepProgressController, - nodeTitles: const [ - 'Step 1', - 'Step 2', - 'Step 3', - 'Step 4 and a long title', - ], - theme: const StepProgressThemeData( - nodeLabelAlignment: StepLabelAlignment.leftRight, - ), - ), - ], - ), - ], + body: StepProgress( + totalSteps: 4, + padding: const EdgeInsets.all(10), + lineTitles: const [ + 'line title 1', + 'line title 2', + 'line title 3', + ], + controller: stepProgressController, + nodeIconBuilder: (index, completedStepIndex) { + if (index <= completedStepIndex) { + return const Icon( + Icons.check, + color: Colors.white, + ); + } else { + return const Icon( + Icons.more_horiz, + color: Colors.white, + ); + } + }, + theme: const StepProgressThemeData( + lineLabelAlignment: Alignment.bottomCenter, + lineLabelStyle: StepLabelStyle( + defualtColor: Colors.grey, + activeColor: Colors.green, + ), + stepLineSpacing: 20, + stepLineStyle: StepLineStyle( + lineThickness: 3, + borderRadius: Radius.circular(4), + ), ), ), bottomNavigationBar: SafeArea( child: Row( mainAxisAlignment: MainAxisAlignment.center, - spacing: 38, + spacing: 40, children: [ ElevatedButton( onPressed: stepProgressController.previousStep, diff --git a/example/lib/example_nineteen.dart b/example/lib/example_nineteen.dart index 20662be..927086d 100644 --- a/example/lib/example_nineteen.dart +++ b/example/lib/example_nineteen.dart @@ -9,15 +9,13 @@ class ExampleNineteen extends StatelessWidget { final stepProgressController = StepProgressController(totalSteps: 5); return Scaffold( appBar: AppBar( - title: const Text('StepProgress - Dotted Line'), + title: const Text('StepProgress - HighlitCurrentStepNode'), ), body: StepProgress( totalSteps: 5, padding: const EdgeInsets.all(10), controller: stepProgressController, - theme: const StepProgressThemeData( - enableRippleEffect: true, - ), + highlightOptions: StepProgressHighlightOptions.highlightCurrentNode, ), bottomNavigationBar: SafeArea( child: Row( diff --git a/example/lib/example_seventeen.dart b/example/lib/example_seventeen.dart index d556135..b5dacf9 100644 --- a/example/lib/example_seventeen.dart +++ b/example/lib/example_seventeen.dart @@ -9,23 +9,19 @@ class ExampleSeventeen extends StatelessWidget { final stepProgressController = StepProgressController(totalSteps: 5); return Scaffold( appBar: AppBar( - title: const Text('StepProgress - BreadCrumb Line'), + title: const Text('StepProgress - Dotted Line'), ), body: StepProgress( totalSteps: 5, padding: const EdgeInsets.all(10), controller: stepProgressController, - lineSubTitles: const [ - 'Step 2', - 'Step 3', - 'Step 4', - 'Step 5', - ], theme: const StepProgressThemeData( - stepLineSpacing: 28, stepLineStyle: StepLineStyle( - lineThickness: 10, - isBreadcrumb: true, + borderRadius: Radius.circular(8), + borderStyle: OuterBorderStyle( + isDotted: true, + borderWidth: 3, + ), ), ), ), diff --git a/example/lib/example_sixteen.dart b/example/lib/example_sixteen.dart index 5d5bda4..bbb0742 100644 --- a/example/lib/example_sixteen.dart +++ b/example/lib/example_sixteen.dart @@ -6,80 +6,26 @@ class ExampleSixteen extends StatelessWidget { @override Widget build(BuildContext context) { - final stepProgressController = StepProgressController(totalSteps: 6); + final stepProgressController = StepProgressController(totalSteps: 5); return Scaffold( - backgroundColor: const Color(0xFF444444), appBar: AppBar( - title: const Text('StepProgress - Custom Vertical Timeline'), + title: const Text('StepProgress - BreadCrumb Line'), ), body: StepProgress( - totalSteps: 6, + totalSteps: 5, padding: const EdgeInsets.all(10), - axis: Axis.vertical, - reversed: true, controller: stepProgressController, - nodeIconBuilder: (index, completedStepIndex) { - if (index <= completedStepIndex) { - //step completed - return const Icon( - Icons.check, - size: 18, - color: Colors.white, - ); - } - return null; - }, - lineLabelBuilder: (index, completedStepIndex) { - // here index is index of current line - // (numbers of lines is equal to toalSteps - 1) - if (index.isEven) { - return Text( - 'December ${index + 10} 2020', - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith(color: Colors.white), - ); - } - return null; - }, - nodeLabelBuilder: (index, completedStepIndex) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 2, - children: [ - Text( - 'Invisalign ClinCheck $index', - maxLines: 3, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - decorationColor: const Color(0xFF4e97fc), - color: const Color(0xFF4e97fc), - decoration: TextDecoration.underline, - ), - ), - Text( - '9:20 AM - 9:40 AM', - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: const Color(0xFF7e7971), - ), - ), - ], - ); - }, + lineSubTitles: const [ + 'Step 2', + 'Step 3', + 'Step 4', + 'Step 5', + ], theme: const StepProgressThemeData( - defaultForegroundColor: Color(0xFF666666), - activeForegroundColor: Color(0xFF4e97fc), - lineLabelAlignment: Alignment.topLeft, - nodeLabelStyle: StepLabelStyle( - maxWidth: double.infinity, - margin: EdgeInsets.all(4), - ), - lineLabelStyle: StepLabelStyle( - maxWidth: double.infinity, - margin: EdgeInsets.only( - right: 18, - ), + stepLineSpacing: 28, + stepLineStyle: StepLineStyle( + lineThickness: 10, + isBreadcrumb: true, ), ), ), diff --git a/example/lib/example_twenty.dart b/example/lib/example_twenty.dart index fb962dc..265e1ac 100644 --- a/example/lib/example_twenty.dart +++ b/example/lib/example_twenty.dart @@ -7,15 +7,34 @@ class ExampleTwenty extends StatelessWidget { @override Widget build(BuildContext context) { final stepProgressController = StepProgressController(totalSteps: 5); + const nodeIcons = [ + Icon(Icons.home), + Icon(Icons.star), + Icon(Icons.settings), + Icon(Icons.person), + Icon(Icons.check) + ]; return Scaffold( + backgroundColor: Colors.grey[200], appBar: AppBar( - title: const Text('StepProgress - HighlitCurrentStepNode'), + title: const Text('StepProgress - Custom Stepper Without Lines'), ), body: StepProgress( totalSteps: 5, + stepSize: 75, padding: const EdgeInsets.all(10), controller: stepProgressController, - highlightOptions: StepProgressHighlightOptions.highlightCurrentNode, + visibilityOptions: StepProgressVisibilityOptions.nodeOnly, + nodeIconBuilder: (index, completedStepIndex) => nodeIcons[index], + theme: const StepProgressThemeData( + stepAnimationDuration: Duration.zero, + stepNodeStyle: StepNodeStyle( + iconColor: Color(0xfffdfdfd), + activeIconColor: Color(0xff72479e), + ), + activeForegroundColor: Color(0xFF181818), + defaultForegroundColor: Color(0xff4c4c4c), + ), ), bottomNavigationBar: SafeArea( child: Row( diff --git a/example/lib/example_twenty_one.dart b/example/lib/example_twenty_one.dart index 176325c..5755e76 100644 --- a/example/lib/example_twenty_one.dart +++ b/example/lib/example_twenty_one.dart @@ -7,33 +7,24 @@ class ExampleTwentyOne extends StatelessWidget { @override Widget build(BuildContext context) { final stepProgressController = StepProgressController(totalSteps: 5); - const nodeIcons = [ - Icon(Icons.home), - Icon(Icons.star), - Icon(Icons.settings), - Icon(Icons.person), - Icon(Icons.check) - ]; return Scaffold( - backgroundColor: Colors.grey[200], + backgroundColor: Colors.black45, appBar: AppBar( - title: const Text('StepProgress - Custom Stepper Without Lines'), + title: const Text('StepProgress - Instagram Story Like Stepper'), ), body: StepProgress( totalSteps: 5, - stepSize: 75, padding: const EdgeInsets.all(10), controller: stepProgressController, - visibilityOptions: StepProgressVisibilityOptions.nodeOnly, - nodeIconBuilder: (index, completedStepIndex) => nodeIcons[index], + visibilityOptions: StepProgressVisibilityOptions.lineOnly, theme: const StepProgressThemeData( - stepAnimationDuration: Duration.zero, - stepNodeStyle: StepNodeStyle( - iconColor: Color(0xfffdfdfd), - activeIconColor: Color(0xff72479e), + activeForegroundColor: Color.fromARGB(255, 255, 255, 255), + defaultForegroundColor: Color.fromARGB(255, 171, 168, 168), + stepLineSpacing: 3, + stepLineStyle: StepLineStyle( + lineThickness: 5, + borderRadius: Radius.circular(5), ), - activeForegroundColor: Color(0xFF181818), - defaultForegroundColor: Color(0xff4c4c4c), ), ), bottomNavigationBar: SafeArea( @@ -43,11 +34,17 @@ class ExampleTwentyOne extends StatelessWidget { children: [ ElevatedButton( onPressed: stepProgressController.previousStep, - child: const Text('Prev'), + child: const Icon(Icons.arrow_back, size: 20), + ), + ElevatedButton( + onPressed: () { + // play or pause the step progress + }, + child: const Icon(Icons.play_arrow, size: 20), ), ElevatedButton( onPressed: stepProgressController.nextStep, - child: const Text('Next'), + child: const Icon(Icons.arrow_forward, size: 20), ), ], ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 64d4710..b56ec69 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,7 +8,6 @@ import 'package:example/example_fourteen.dart'; import 'package:example/example_nine.dart'; import 'package:example/example_nineteen.dart'; import 'package:example/example_one.dart'; -import 'package:example/example_reproduce_issues.dart'; import 'package:example/example_seven.dart'; import 'package:example/example_seventeen.dart'; import 'package:example/example_six.dart'; @@ -20,6 +19,8 @@ import 'package:example/example_twelve.dart'; import 'package:example/example_twenty.dart'; import 'package:example/example_twenty_one.dart'; import 'package:example/example_two.dart'; +import 'package:example/reproduce_issues.dart'; +import 'package:example/reproduce_large_labels.dart'; import 'package:flutter/material.dart'; void main() { @@ -64,7 +65,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example One (Vertical)'), + child: const Text('1-Vertical'), ), ElevatedButton( onPressed: () { @@ -75,7 +76,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Two (Horizontal - without labels)'), + child: const Text('2-HorizontalWithoutLabels'), ), ElevatedButton( onPressed: () { @@ -86,7 +87,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Three (LineOnly-Custom)'), + child: const Text('3-LineOnlyCustom'), ), ElevatedButton( onPressed: () { @@ -97,7 +98,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Four (LineOnly Custom)'), + child: const Text('4-LineOnlyCustom'), ), ElevatedButton( onPressed: () { @@ -108,7 +109,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Five (Border)'), + child: const Text('5-Border'), ), ElevatedButton( onPressed: () { @@ -119,7 +120,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Six (Positioned Labels)'), + child: const Text('6-PositionedLabels'), ), ElevatedButton( onPressed: () { @@ -130,7 +131,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Seven (NodeOnly)'), + child: const Text('7-NodeOnly'), ), ElevatedButton( onPressed: () { @@ -141,7 +142,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Eight (LineSpacing)'), + child: const Text('8-LineSpacing'), ), ElevatedButton( onPressed: () { @@ -152,7 +153,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Nine (SquareShape)'), + child: const Text('9-SquareShape'), ), ElevatedButton( onPressed: () { @@ -163,7 +164,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Ten (TriangleShape)'), + child: const Text('10-TriangleShape'), ), ElevatedButton( onPressed: () { @@ -174,7 +175,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Elevn (DiamondShape)'), + child: const Text('11-DiamondShape'), ), ElevatedButton( onPressed: () { @@ -185,7 +186,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Twelve (HexagonShape)'), + child: const Text('12-HexagonShape'), ), ElevatedButton( onPressed: () { @@ -196,7 +197,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Thirteen (StarShape)'), + child: const Text('13-StarShape'), ), ElevatedButton( onPressed: () { @@ -207,7 +208,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Fourteen (LargeLabel)'), + child: const Text('14-LineLabels'), ), ElevatedButton( onPressed: () { @@ -218,7 +219,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Fifteen (LineLabel)'), + child: const Text('15-CustomVerticalTimeLine'), ), ElevatedButton( onPressed: () { @@ -229,7 +230,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Sixteen (CustomVerticalTimeLine)'), + child: const Text('16-BreadCrumbLine'), ), ElevatedButton( onPressed: () { @@ -240,7 +241,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Seventeen (BreadCrumbLine)'), + child: const Text('17-DottedLine'), ), ElevatedButton( onPressed: () { @@ -251,7 +252,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Eighteen (DottedLine)'), + child: const Text('18-RippleEffectNode'), ), ElevatedButton( onPressed: () { @@ -262,7 +263,7 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Nineteen (RippleEffect node)'), + child: const Text('19-HighlitCurrentStepNode'), ), ElevatedButton( onPressed: () { @@ -273,7 +274,9 @@ class HomePage extends StatelessWidget { ), ); }, - child: const Text('Example Twenty (HighlitCurrentStepNode)'), + child: const Text( + '20-CustomStepperWithoutLines', + ), ), ElevatedButton( onPressed: () { @@ -285,7 +288,7 @@ class HomePage extends StatelessWidget { ); }, child: const Text( - 'Example Twenty One (CustomStepperWithoutLines)', + '21-InstagramStoryLikeStepper', ), ), ElevatedButton( @@ -293,7 +296,18 @@ class HomePage extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (_) => const ExampleReproduceIssues(), + builder: (_) => const ReproduceLargeLabels(), + ), + ); + }, + child: const Text('ReproduceLargeLabels'), + ), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const ReproduceIssues(), ), ); }, diff --git a/example/lib/example_reproduce_issues.dart b/example/lib/reproduce_issues.dart similarity index 93% rename from example/lib/example_reproduce_issues.dart rename to example/lib/reproduce_issues.dart index 2f4eb6b..7fa91c4 100644 --- a/example/lib/example_reproduce_issues.dart +++ b/example/lib/reproduce_issues.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:step_progress/step_progress.dart'; -class ExampleReproduceIssues extends StatelessWidget { - const ExampleReproduceIssues({super.key}); +class ReproduceIssues extends StatelessWidget { + const ReproduceIssues({super.key}); @override Widget build(BuildContext context) { diff --git a/example/lib/reproduce_large_labels.dart b/example/lib/reproduce_large_labels.dart new file mode 100644 index 0000000..24bdc36 --- /dev/null +++ b/example/lib/reproduce_large_labels.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:step_progress/step_progress.dart'; + +class ReproduceLargeLabels extends StatelessWidget { + const ReproduceLargeLabels({super.key}); + + @override + Widget build(BuildContext context) { + final stepProgressController = StepProgressController(totalSteps: 4); + return Scaffold( + appBar: AppBar( + title: const Text('StepProgress - large labels'), + ), + body: SingleChildScrollView( + child: Column( + spacing: 18, + children: [ + StepProgress( + totalSteps: 4, + stepSize: 28, + controller: stepProgressController, + nodeTitles: const [ + 'Step 1', + 'Step 2', + 'Step 3', + 'Step 4 and a long title', + ], + theme: const StepProgressThemeData( + nodeLabelAlignment: StepLabelAlignment.top, + ), + ), + StepProgress( + totalSteps: 4, + stepSize: 28, + controller: stepProgressController, + nodeTitles: const [ + 'Step 1', + 'Step 2', + 'Step 3', + 'Step 4 and a long title', + ], + theme: const StepProgressThemeData( + nodeLabelAlignment: StepLabelAlignment.bottom, + ), + ), + StepProgress( + totalSteps: 4, + stepSize: 28, + controller: stepProgressController, + nodeTitles: const [ + 'Step 1 and a long title here', + 'Step 2', + 'Step 3', + 'Step 4 and a long title', + ], + theme: const StepProgressThemeData( + nodeLabelAlignment: StepLabelAlignment.topBottom, + ), + ), + Row( + spacing: 18, + children: [ + StepProgress( + totalSteps: 4, + stepSize: 28, + height: 390, + axis: Axis.vertical, + controller: stepProgressController, + nodeTitles: const [ + 'Step 1', + 'Step 2', + 'Step 3', + 'Step 4 and a long title', + ], + theme: const StepProgressThemeData( + nodeLabelAlignment: StepLabelAlignment.left, + ), + ), + StepProgress( + totalSteps: 4, + stepSize: 28, + height: 390, + axis: Axis.vertical, + controller: stepProgressController, + nodeTitles: const [ + 'Step 1', + 'Step 2', + 'Step 3', + 'Step 4 and a long title', + ], + theme: const StepProgressThemeData( + nodeLabelAlignment: StepLabelAlignment.right, + ), + ), + StepProgress( + totalSteps: 4, + stepSize: 28, + height: 390, + axis: Axis.vertical, + controller: stepProgressController, + nodeTitles: const [ + 'Step 1', + 'Step 2', + 'Step 3', + 'Step 4 and a long title', + ], + theme: const StepProgressThemeData( + nodeLabelAlignment: StepLabelAlignment.leftRight, + ), + ), + ], + ), + ], + ), + ), + bottomNavigationBar: SafeArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 38, + children: [ + ElevatedButton( + onPressed: stepProgressController.previousStep, + child: const Text('Prev'), + ), + ElevatedButton( + onPressed: stepProgressController.nextStep, + child: const Text('Next'), + ), + ], + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 008a53d..b9526f3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -158,7 +158,7 @@ packages: path: ".." relative: true source: path - version: "2.5.2" + version: "2.5.3" stream_channel: dependency: transitive description: From 1bedb71e325a1558500cad65a49fcd9e3b748a63 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Sun, 20 Jul 2025 16:48:59 +0330 Subject: [PATCH 03/11] feat: add autoStartProgress property --- example/lib/example_twenty_one.dart | 2 ++ lib/src/step_line/step_line.dart | 19 +++++++++-- lib/src/step_progress.dart | 33 +++++++++++++++++-- .../horizontal_step_progress.dart | 6 ++++ .../step_progress_widget.dart | 32 +++++++++++++----- .../vertical_step_progress.dart | 6 ++++ 6 files changed, 85 insertions(+), 13 deletions(-) diff --git a/example/lib/example_twenty_one.dart b/example/lib/example_twenty_one.dart index 5755e76..81029ec 100644 --- a/example/lib/example_twenty_one.dart +++ b/example/lib/example_twenty_one.dart @@ -17,12 +17,14 @@ class ExampleTwentyOne extends StatelessWidget { padding: const EdgeInsets.all(10), controller: stepProgressController, visibilityOptions: StepProgressVisibilityOptions.lineOnly, + autoStartProgress: true, theme: const StepProgressThemeData( activeForegroundColor: Color.fromARGB(255, 255, 255, 255), defaultForegroundColor: Color.fromARGB(255, 171, 168, 168), stepLineSpacing: 3, stepLineStyle: StepLineStyle( lineThickness: 5, + animationDuration: Duration(seconds: 6), borderRadius: Radius.circular(5), ), ), diff --git a/lib/src/step_line/step_line.dart b/lib/src/step_line/step_line.dart index f8d6129..2f7bb0f 100644 --- a/lib/src/step_line/step_line.dart +++ b/lib/src/step_line/step_line.dart @@ -28,12 +28,16 @@ import 'package:step_progress/step_progress.dart'; /// The [onTap] parameter is a callback function that is executed when the step /// line is tapped. It is optional and defaults to `null`. /// +/// The [onStepLineAnimationCompleted] parameter is a callback that is +/// triggered when the step line's animation completes. It is optional +/// and defaults to `null`. +/// /// Example usage: /// /// ```dart /// StepLine( /// axis: Axis.vertical, -/// isActive: true, +/// highlighted: true, /// isReversed: true, /// stepLineStyle: StepLineStyle( /// color: Colors.blue, @@ -42,6 +46,9 @@ import 'package:step_progress/step_progress.dart'; /// onTap: () { /// print('Step line tapped'); /// }, +/// onStepLineAnimationCompleted: () { +/// print('Line animation completed'); +/// }, /// ) /// ``` class StepLine extends StatelessWidget { @@ -50,6 +57,7 @@ class StepLine extends StatelessWidget { this.stepLineStyle, this.highlighted = false, this.isReversed = false, + this.onStepLineAnimationCompleted, this.onTap, super.key, }); @@ -69,6 +77,9 @@ class StepLine extends StatelessWidget { /// Indicates whether the step line is displayed in reverse order. final bool isReversed; + /// Callback triggered when the line animation completed. + final VoidCallback? onStepLineAnimationCompleted; + @override Widget build(BuildContext context) { final theme = StepProgressTheme.of(context)!.data; @@ -106,7 +117,9 @@ class StepLine extends StatelessWidget { width: lineSize.width, height: lineSize.height, decoration: containerDecoration, - alignment: AlignmentDirectional.centerStart, + alignment: _isHorizontal + ? AlignmentDirectional.centerStart + : AlignmentDirectional.topStart, child: AnimatedContainer( width: _isHorizontal ? (highlighted ? lineSize.width : 0) @@ -118,7 +131,7 @@ class StepLine extends StatelessWidget { color: style.activeColor ?? theme.activeForegroundColor, borderRadius: BorderRadius.all(borderRadius), ), - curve: Curves.fastLinearToSlowEaseIn, + onEnd: onStepLineAnimationCompleted, duration: style.animationDuration ?? theme.stepAnimationDuration, ), ); diff --git a/lib/src/step_progress.dart b/lib/src/step_progress.dart index e6c38f1..4d48c67 100644 --- a/lib/src/step_progress.dart +++ b/lib/src/step_progress.dart @@ -110,6 +110,9 @@ typedef OnStepNodeTapped = void Function(int index); /// /// The [reversed] parameter indicates whether the step progress is displayed /// in reverse order. It defaults to false. +/// +/// The [autoStartProgress] parameter determines whether the progress should +/// automatically start when the widget is initialized. It defaults to false. class StepProgress extends StatefulWidget { const StepProgress({ required this.totalSteps, @@ -124,6 +127,7 @@ class StepProgress extends StatefulWidget { this.padding = EdgeInsets.zero, this.axis = Axis.horizontal, this.reversed = false, + this.autoStartProgress = false, this.visibilityOptions = StepProgressVisibilityOptions.both, this.highlightOptions = StepProgressHighlightOptions.highlightCompletedNodesAndLines, @@ -228,6 +232,9 @@ class StepProgress extends StatefulWidget { /// Indicates whether the step progress is displayed in reverse order. final bool reversed; + /// Whether the progress should start automatically when the widget is built. + final bool autoStartProgress; + @override _StepProgressState createState() { assert( @@ -247,6 +254,12 @@ class _StepProgressState extends State widget.controller?.addListener(() { _changeStep(widget.controller!.currentStep); }); + if (widget.autoStartProgress) { + Future.delayed( + Duration.zero, + () => _handleAutoChangeSteps(index: _currentStep), + ); + } super.initState(); } @@ -258,8 +271,6 @@ class _StepProgressState extends State /// is empty. DataCache().clearCache(); - /// Disposes of the controller if it is not null. - widget.controller?.dispose(); super.dispose(); } @@ -323,6 +334,22 @@ class _StepProgressState extends State } } + /// Handles automatic step changes based on the current index and direction. + /// Advances to the next step if auto progress is enabled and not reverted. + void _handleAutoChangeSteps({ + int index = 0, + bool isReverted = false, + }) { + if (!widget.autoStartProgress || isReverted) { + return; + } + if (widget.controller != null) { + widget.controller!.nextStep(); + } else { + _changeStep(index + 1); + } + } + @override Widget build(BuildContext context) { return StepProgressTheme( @@ -341,6 +368,7 @@ class _StepProgressState extends State currentStep: _currentStep, reversed: widget.reversed, highlightOptions: widget.highlightOptions, + onStepLineAnimationCompleted: _handleAutoChangeSteps, needsRebuildWidget: _needsRebuildWidget, nodeTitles: widget.nodeTitles, nodeSubTitles: widget.nodeSubTitles, @@ -359,6 +387,7 @@ class _StepProgressState extends State currentStep: _currentStep, reversed: widget.reversed, highlightOptions: widget.highlightOptions, + onStepLineAnimationCompleted: _handleAutoChangeSteps, needsRebuildWidget: _needsRebuildWidget, nodeTitles: widget.nodeTitles, nodeSubTitles: widget.nodeSubTitles, diff --git a/lib/src/step_progress_widgets/horizontal_step_progress.dart b/lib/src/step_progress_widgets/horizontal_step_progress.dart index adf6b63..b81f66a 100644 --- a/lib/src/step_progress_widgets/horizontal_step_progress.dart +++ b/lib/src/step_progress_widgets/horizontal_step_progress.dart @@ -50,6 +50,7 @@ class HorizontalStepProgress extends StepProgressWidget { required super.stepSize, required super.visibilityOptions, required super.needsRebuildWidget, + super.onStepLineAnimationCompleted, super.highlightOptions, super.reversed, super.nodeTitles, @@ -130,6 +131,11 @@ class HorizontalStepProgress extends StepProgressWidget { return StepLine( isReversed: reversed, highlighted: isHighlightedStepLine(index), + onStepLineAnimationCompleted: () => + onStepLineAnimationCompleted?.call( + index: index, + isReverted: !isHighlightedStepLine(index), + ), onTap: () => onStepLineTapped?.call(index), ); }); diff --git a/lib/src/step_progress_widgets/step_progress_widget.dart b/lib/src/step_progress_widgets/step_progress_widget.dart index ab459af..53ce3a7 100644 --- a/lib/src/step_progress_widgets/step_progress_widget.dart +++ b/lib/src/step_progress_widgets/step_progress_widget.dart @@ -8,6 +8,17 @@ import 'package:step_progress/src/step_progress_highlight_options.dart'; import 'package:step_progress/src/step_progress_theme.dart'; import 'package:step_progress/src/step_progress_visibility_options.dart'; +/// Signature for a callback that is invoked when the step line animation is +/// completed. +/// +/// [index] is the index of the step whose animation has completed. +/// [isReverted] indicates whether the animation was reverted (true) or +/// completed normally (false). +typedef OnStepLineAnimationCompleted = void Function({ + int index, + bool isReverted, +}); + /// An abstract class representing a step progress widget. /// /// This widget displays a progress indicator with multiple steps, allowing for @@ -25,24 +36,26 @@ import 'package:step_progress/src/step_progress_visibility_options.dart'; /// - [lineTitles]: An optional list of titles for each line segment. /// - [lineSubTitles]: An optional list of subtitles for each line segment. /// - [axis]: The axis in which the step progress is laid out -/// (horizontal or vertical). +/// (horizontal or vertical). /// - [visibilityOptions]: Options to control the visibility of elements. /// - [nodeSubTitles]: An optional list of subtitles for each step. /// - [onStepNodeTapped]: An optional callback function triggered when a step -/// node is tapped. +/// node is tapped. /// - [onStepLineTapped]: An optional callback function triggered when a step -/// line is tapped. +/// line is tapped. +/// - [onStepLineAnimationCompleted]: An optional callback function triggered +/// when a step line is highlighted. /// - [nodeIconBuilder]: An optional builder for the icon of a step node. /// - [nodeLabelBuilder]: A builder for creating custom label widgets for -/// step nodes. +/// step nodes. /// - [lineLabelBuilder]: A builder for creating custom label widgets for step -/// lines. +/// lines. /// - [reversed]: Indicates whether the step progress is displayed in reverse -/// order. It defaults to false. +/// order. It defaults to false. /// - [needsRebuildWidget]: Callback to request a rebuild of the parent widget. -/// This is triggered when dynamic size calculations are needed. +/// This is triggered when dynamic size calculations are needed. /// - [highlightOptions]: Options to customize the highlight behavior of the -/// step progress widget. +/// step progress widget. abstract class StepProgressWidget extends StatelessWidget { const StepProgressWidget({ required this.totalSteps, @@ -53,6 +66,7 @@ abstract class StepProgressWidget extends StatelessWidget { required this.needsRebuildWidget, this.highlightOptions = StepProgressHighlightOptions.highlightCompletedNodesAndLines, + this.onStepLineAnimationCompleted, this.reversed = false, this.nodeTitles, this.nodeSubTitles, @@ -133,6 +147,8 @@ abstract class StepProgressWidget extends StatelessWidget { /// Options to customize the highlight behavior of the step progress widget. final StepProgressHighlightOptions highlightOptions; + final OnStepLineAnimationCompleted? onStepLineAnimationCompleted; + /// Determines if a step line at the given index should be highlighted /// based on the current step and highlight options. bool isHighlightedStepLine(int index) { diff --git a/lib/src/step_progress_widgets/vertical_step_progress.dart b/lib/src/step_progress_widgets/vertical_step_progress.dart index 0b5a54f..9aa88a7 100644 --- a/lib/src/step_progress_widgets/vertical_step_progress.dart +++ b/lib/src/step_progress_widgets/vertical_step_progress.dart @@ -89,6 +89,7 @@ class VerticalStepProgress extends StepProgressWidget { required super.stepSize, required super.visibilityOptions, required super.needsRebuildWidget, + super.onStepLineAnimationCompleted, super.highlightOptions, super.reversed, super.nodeTitles, @@ -167,6 +168,11 @@ class VerticalStepProgress extends StepProgressWidget { axis: Axis.vertical, isReversed: reversed, highlighted: isHighlightedStepLine(index), + onStepLineAnimationCompleted: () => + onStepLineAnimationCompleted?.call( + index: index, + isReverted: !isHighlightedStepLine(index), + ), onTap: () => onStepLineTapped?.call(index), ); }); From b72b43f8fc5294ce0b382825259b02a35eb6256f Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 13:44:00 +0330 Subject: [PATCH 04/11] feat: implement auto step progression --- example/lib/example_twenty_one.dart | 3 +- lib/src/step_line/step_line.dart | 64 +++++-- lib/src/step_line/step_value_line.dart | 98 ++++++++++ lib/src/step_progress.dart | 37 ++-- lib/src/step_progress_controller.dart | 5 + .../horizontal_step_progress.dart | 6 +- .../step_progress_widget.dart | 9 +- .../vertical_step_progress.dart | 8 +- test/src/step_line/step_value_line_test.dart | 167 ++++++++++++++++++ 9 files changed, 363 insertions(+), 34 deletions(-) create mode 100644 lib/src/step_line/step_value_line.dart create mode 100644 test/src/step_line/step_value_line_test.dart diff --git a/example/lib/example_twenty_one.dart b/example/lib/example_twenty_one.dart index 81029ec..6c3db42 100644 --- a/example/lib/example_twenty_one.dart +++ b/example/lib/example_twenty_one.dart @@ -6,7 +6,8 @@ class ExampleTwentyOne extends StatelessWidget { @override Widget build(BuildContext context) { - final stepProgressController = StepProgressController(totalSteps: 5); + final stepProgressController = + StepProgressController(totalSteps: 5, initialStep: -1); return Scaffold( backgroundColor: Colors.black45, appBar: AppBar( diff --git a/lib/src/step_line/step_line.dart b/lib/src/step_line/step_line.dart index 2f7bb0f..402f884 100644 --- a/lib/src/step_line/step_line.dart +++ b/lib/src/step_line/step_line.dart @@ -1,6 +1,7 @@ import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; import 'package:step_progress/src/step_line/breadcrumb_clipper.dart'; +import 'package:step_progress/src/step_line/step_value_line.dart'; import 'package:step_progress/step_progress.dart'; /// A widget that represents a step line in a step progress indicator. @@ -25,6 +26,12 @@ import 'package:step_progress/step_progress.dart'; /// The [isReversed] parameter indicates whether the step line is displayed in /// reverse order. It defaults to `false`. /// +/// The [isCurrentStep] parameter indicates whether this step is the current +/// active step. It defaults to `false`. +/// +/// The [isAutoStepChange] parameter determines if the step progression advances +/// automatically. It defaults to `false`. +/// /// The [onTap] parameter is a callback function that is executed when the step /// line is tapped. It is optional and defaults to `null`. /// @@ -39,6 +46,8 @@ import 'package:step_progress/step_progress.dart'; /// axis: Axis.vertical, /// highlighted: true, /// isReversed: true, +/// isCurrentStep: true, +/// isAutoStepChange: false, /// stepLineStyle: StepLineStyle( /// color: Colors.blue, /// thickness: 2.0, @@ -53,6 +62,8 @@ import 'package:step_progress/step_progress.dart'; /// ``` class StepLine extends StatelessWidget { const StepLine({ + this.isCurrentStep = false, + this.isAutoStepChange = false, this.axis = Axis.horizontal, this.stepLineStyle, this.highlighted = false, @@ -80,6 +91,12 @@ class StepLine extends StatelessWidget { /// Callback triggered when the line animation completed. final VoidCallback? onStepLineAnimationCompleted; + /// Indicates whether this step is the current active step. + final bool isCurrentStep; + + /// Determines if step changes occur automatically. + final bool isAutoStepChange; + @override Widget build(BuildContext context) { final theme = StepProgressTheme.of(context)!.data; @@ -87,6 +104,8 @@ class StepLine extends StatelessWidget { final isBreadcrumb = style.isBreadcrumb; final borderStyle = style.borderStyle ?? theme.borderStyle; final borderRadius = style.borderRadius; + // + final activeColor = style.activeColor ?? theme.activeForegroundColor; final lineSpacing = EdgeInsets.symmetric( horizontal: _isHorizontal ? theme.stepLineSpacing : 0, @@ -118,22 +137,35 @@ class StepLine extends StatelessWidget { height: lineSize.height, decoration: containerDecoration, alignment: _isHorizontal - ? AlignmentDirectional.centerStart - : AlignmentDirectional.topStart, - child: AnimatedContainer( - width: _isHorizontal - ? (highlighted ? lineSize.width : 0) - : lineSize.width, - height: !_isHorizontal - ? (highlighted ? lineSize.height : 0) - : lineSize.height, - decoration: BoxDecoration( - color: style.activeColor ?? theme.activeForegroundColor, - borderRadius: BorderRadius.all(borderRadius), - ), - onEnd: onStepLineAnimationCompleted, - duration: style.animationDuration ?? theme.stepAnimationDuration, - ), + ? (isReversed + ? AlignmentDirectional.centerEnd + : AlignmentDirectional.centerStart) + : (isReversed + ? AlignmentDirectional.bottomEnd + : AlignmentDirectional.topStart), + child: isCurrentStep && isAutoStepChange + ? StepValueLine( + duration: + style.animationDuration ?? theme.stepAnimationDuration, + activeColor: activeColor, + borderRadius: borderRadius, + lineSize: lineSize, + highlighted: highlighted, + isHorizontal: _isHorizontal, + onAnimationCompleted: onStepLineAnimationCompleted, + ) + : Container( + width: _isHorizontal + ? (highlighted ? lineSize.width : 0) + : lineSize.width, + height: !_isHorizontal + ? (highlighted ? lineSize.height : 0) + : lineSize.height, + decoration: BoxDecoration( + color: activeColor, + borderRadius: BorderRadius.all(borderRadius), + ), + ), ); if (isBreadcrumb) { diff --git a/lib/src/step_line/step_value_line.dart b/lib/src/step_line/step_value_line.dart new file mode 100644 index 0000000..db40db2 --- /dev/null +++ b/lib/src/step_line/step_value_line.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +/// A widget that displays a step progress line with customizable appearance. +/// Supports animation, orientation, and completion callbacks. +/// Useful for visualizing progress in multi-step processes. +class StepValueLine extends StatefulWidget { + const StepValueLine({ + required this.activeColor, + required this.borderRadius, + required this.lineSize, + required this.highlighted, + required this.isHorizontal, + required this.duration, + this.onAnimationCompleted, + super.key, + }); + + /// Callback triggered when the animation is completed. + final VoidCallback? onAnimationCompleted; + + /// The color used for the active state of the step line. + final Color activeColor; + + /// The border radius applied to the step line. + final Radius borderRadius; + + /// The size of the step line. + final Size lineSize; + + /// Whether the step line is highlighted. + final bool highlighted; + + /// Whether the step line is oriented horizontally. + final bool isHorizontal; + + /// The duration of the animation. + final Duration duration; + + @override + State createState() => _StepValueLineState(); +} + +class _StepValueLineState extends State + with SingleTickerProviderStateMixin { + late final AnimationController animationController; + @override + void initState() { + animationController = AnimationController( + vsync: this, + duration: widget.duration, + ); + animationController.forward(); + if (widget.onAnimationCompleted != null) { + animationController.addStatusListener(statusListener); + } + super.initState(); + } + + void statusListener(AnimationStatus status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted!(); + } + } + + @override + void dispose() { + if (widget.onAnimationCompleted != null) { + animationController.removeStatusListener(statusListener); + } + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animationController, + builder: (context, child) { + return Container( + width: widget.isHorizontal + ? (widget.highlighted + ? widget.lineSize.width * animationController.value + : 0) + : widget.lineSize.width, + height: !widget.isHorizontal + ? (widget.highlighted + ? widget.lineSize.height * animationController.value + : 0) + : widget.lineSize.height, + decoration: BoxDecoration( + color: widget.activeColor, + borderRadius: BorderRadius.all(widget.borderRadius), + ), + ); + }, + ); + } +} diff --git a/lib/src/step_progress.dart b/lib/src/step_progress.dart index 4d48c67..d3f1711 100644 --- a/lib/src/step_progress.dart +++ b/lib/src/step_progress.dart @@ -248,6 +248,7 @@ class StepProgress extends StatefulWidget { class _StepProgressState extends State with SingleTickerProviderStateMixin { late int _currentStep = _getCurrentStep; + late int _previousStep = _getPreviousStep; @override void initState() { @@ -257,7 +258,9 @@ class _StepProgressState extends State if (widget.autoStartProgress) { Future.delayed( Duration.zero, - () => _handleAutoChangeSteps(index: _currentStep), + () => _onStepAnimationCompleted( + index: _currentStep >= 0 ? _currentStep : 0, + ), ); } super.initState(); @@ -300,6 +303,12 @@ class _StepProgressState extends State : widget.currentStep; } + int get _getPreviousStep { + return widget.controller != null + ? widget.controller!.prevStep + : widget.currentStep - 1; + } + /// Changes the current step to the specified [newStep]. /// /// If [newStep] is the same as the current step, less than -1, or greater @@ -318,9 +327,13 @@ class _StepProgressState extends State } if (mounted) { setState(() { + _previousStep = _currentStep; _currentStep = newStep; }); } + if (_currentStep != widget.controller?.currentStep) { + widget.controller?.setCurrentStep(_currentStep); + } widget.onStepChanged?.call(_currentStep); } @@ -335,18 +348,18 @@ class _StepProgressState extends State } /// Handles automatic step changes based on the current index and direction. - /// Advances to the next step if auto progress is enabled and not reverted. - void _handleAutoChangeSteps({ + void _onStepAnimationCompleted({ int index = 0, - bool isReverted = false, }) { - if (!widget.autoStartProgress || isReverted) { + if (!widget.autoStartProgress) { return; - } - if (widget.controller != null) { - widget.controller!.nextStep(); } else { - _changeStep(index + 1); + final isForward = _currentStep > _previousStep; + if (isForward) { + _changeStep(index + 1); + } else { + _changeStep(index - 1); + } } } @@ -366,9 +379,10 @@ class _StepProgressState extends State ? HorizontalStepProgress( totalSteps: widget.totalSteps, currentStep: _currentStep, + isAutoStepChange: widget.autoStartProgress, reversed: widget.reversed, highlightOptions: widget.highlightOptions, - onStepLineAnimationCompleted: _handleAutoChangeSteps, + onStepLineAnimationCompleted: _onStepAnimationCompleted, needsRebuildWidget: _needsRebuildWidget, nodeTitles: widget.nodeTitles, nodeSubTitles: widget.nodeSubTitles, @@ -385,9 +399,10 @@ class _StepProgressState extends State : VerticalStepProgress( totalSteps: widget.totalSteps, currentStep: _currentStep, + isAutoStepChange: widget.autoStartProgress, reversed: widget.reversed, highlightOptions: widget.highlightOptions, - onStepLineAnimationCompleted: _handleAutoChangeSteps, + onStepLineAnimationCompleted: _onStepAnimationCompleted, needsRebuildWidget: _needsRebuildWidget, nodeTitles: widget.nodeTitles, nodeSubTitles: widget.nodeSubTitles, diff --git a/lib/src/step_progress_controller.dart b/lib/src/step_progress_controller.dart index 305acc4..a4bf349 100644 --- a/lib/src/step_progress_controller.dart +++ b/lib/src/step_progress_controller.dart @@ -25,6 +25,8 @@ class StepProgressController extends ChangeNotifier { /// Returns current step int get currentStep => _currentStep; + late int prevStep = currentStep - 1; + /// validate newStep and set it to current step void setCurrentStep(int newStep) { assert( @@ -32,6 +34,7 @@ class StepProgressController extends ChangeNotifier { 'new step must be equal or greater than -1 and lower than $totalSteps', ); if (_currentStep != newStep) { + prevStep = _currentStep; _currentStep = newStep; notifyListeners(); } @@ -43,6 +46,7 @@ class StepProgressController extends ChangeNotifier { /// incremented step is still less than the total number of steps. void nextStep() { if (_currentStep + 1 < totalSteps) { + prevStep = _currentStep; _currentStep++; notifyListeners(); } @@ -52,6 +56,7 @@ class StepProgressController extends ChangeNotifier { /// Decrements the `currentStep` by 1 and notifies listeners. void previousStep() { if (_currentStep >= 0) { + prevStep = _currentStep; _currentStep--; notifyListeners(); } diff --git a/lib/src/step_progress_widgets/horizontal_step_progress.dart b/lib/src/step_progress_widgets/horizontal_step_progress.dart index b81f66a..f03e32d 100644 --- a/lib/src/step_progress_widgets/horizontal_step_progress.dart +++ b/lib/src/step_progress_widgets/horizontal_step_progress.dart @@ -42,6 +42,8 @@ import 'package:step_progress/step_progress.dart'; /// step nodes. /// - [lineLabelBuilder]: A builder for creating custom label widgets for step /// lines. +/// - [isAutoStepChange]: A boolean that determines if the step change should +/// occur automatically. /// - [key]: An optional key for the widget. class HorizontalStepProgress extends StepProgressWidget { const HorizontalStepProgress({ @@ -52,6 +54,7 @@ class HorizontalStepProgress extends StepProgressWidget { required super.needsRebuildWidget, super.onStepLineAnimationCompleted, super.highlightOptions, + super.isAutoStepChange, super.reversed, super.nodeTitles, super.nodeSubTitles, @@ -130,11 +133,12 @@ class HorizontalStepProgress extends StepProgressWidget { List children = List.generate(totalSteps - 1, (index) { return StepLine( isReversed: reversed, + isCurrentStep: currentStep == index + 1, + isAutoStepChange: isAutoStepChange, highlighted: isHighlightedStepLine(index), onStepLineAnimationCompleted: () => onStepLineAnimationCompleted?.call( index: index, - isReverted: !isHighlightedStepLine(index), ), onTap: () => onStepLineTapped?.call(index), ); diff --git a/lib/src/step_progress_widgets/step_progress_widget.dart b/lib/src/step_progress_widgets/step_progress_widget.dart index 53ce3a7..4f036a3 100644 --- a/lib/src/step_progress_widgets/step_progress_widget.dart +++ b/lib/src/step_progress_widgets/step_progress_widget.dart @@ -12,11 +12,8 @@ import 'package:step_progress/src/step_progress_visibility_options.dart'; /// completed. /// /// [index] is the index of the step whose animation has completed. -/// [isReverted] indicates whether the animation was reverted (true) or -/// completed normally (false). typedef OnStepLineAnimationCompleted = void Function({ int index, - bool isReverted, }); /// An abstract class representing a step progress widget. @@ -43,7 +40,7 @@ typedef OnStepLineAnimationCompleted = void Function({ /// node is tapped. /// - [onStepLineTapped]: An optional callback function triggered when a step /// line is tapped. -/// - [onStepLineAnimationCompleted]: An optional callback function triggered +/// - [onStepLineAnimationCompleted]: An optional callback function triggered /// when a step line is highlighted. /// - [nodeIconBuilder]: An optional builder for the icon of a step node. /// - [nodeLabelBuilder]: A builder for creating custom label widgets for @@ -67,6 +64,7 @@ abstract class StepProgressWidget extends StatelessWidget { this.highlightOptions = StepProgressHighlightOptions.highlightCompletedNodesAndLines, this.onStepLineAnimationCompleted, + this.isAutoStepChange = false, this.reversed = false, this.nodeTitles, this.nodeSubTitles, @@ -101,6 +99,9 @@ abstract class StepProgressWidget extends StatelessWidget { /// The current step that is active or completed. final int currentStep; + /// Determines if the step changes automatically without user interaction. + final bool isAutoStepChange; + /// The size of each step in the progress indicator. final double stepSize; diff --git a/lib/src/step_progress_widgets/vertical_step_progress.dart b/lib/src/step_progress_widgets/vertical_step_progress.dart index 9aa88a7..7c74003 100644 --- a/lib/src/step_progress_widgets/vertical_step_progress.dart +++ b/lib/src/step_progress_widgets/vertical_step_progress.dart @@ -38,6 +38,9 @@ import 'package:step_progress/src/step_progress_widgets/step_progress_widget.dar /// The [needsRebuildWidget] parameter is a [VoidCallback] function that can be /// used to trigger a rebuild of the widget when necessary. /// +/// The [isAutoStepChange] parameter is a [bool] that determines whether the +/// step change should occur automatically (e.g., with animation) or manually. +/// /// Optional parameters include [nodeTitles], [nodeSubTitles], [lineTitles], /// and [lineSubTitles], which allow you to provide titles and subtitles for /// step nodes and lines. The [onStepNodeTapped] callback can be used to handle @@ -57,6 +60,7 @@ import 'package:step_progress/src/step_progress_widgets/step_progress_widget.dar /// stepSize: 30.0, /// visibilityOptions: StepProgressVisibilityOptions.both, /// reversed: false, +/// isAutoStepChange: true, /// lineTitles: ['Line1', 'Line2', 'Line3', 'Line4' ], /// lineSubTitles: ['sub1', 'sub2', 'sub3', 'sub4', ] /// nodeTitles: ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5'], @@ -91,6 +95,7 @@ class VerticalStepProgress extends StepProgressWidget { required super.needsRebuildWidget, super.onStepLineAnimationCompleted, super.highlightOptions, + super.isAutoStepChange, super.reversed, super.nodeTitles, super.nodeSubTitles, @@ -167,11 +172,12 @@ class VerticalStepProgress extends StepProgressWidget { return StepLine( axis: Axis.vertical, isReversed: reversed, + isCurrentStep: currentStep == index + 1, + isAutoStepChange: isAutoStepChange, highlighted: isHighlightedStepLine(index), onStepLineAnimationCompleted: () => onStepLineAnimationCompleted?.call( index: index, - isReverted: !isHighlightedStepLine(index), ), onTap: () => onStepLineTapped?.call(index), ); diff --git a/test/src/step_line/step_value_line_test.dart b/test/src/step_line/step_value_line_test.dart new file mode 100644 index 0000000..ebd5a65 --- /dev/null +++ b/test/src/step_line/step_value_line_test.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:step_progress/src/step_line/step_value_line.dart'; + +void main() { + group('StepValueLine', () { + testWidgets('renders a Container with correct color and borderRadius', + (tester) async { + const color = Colors.red; + const borderRadius = Radius.circular(8); + const size = Size(100, 10); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepValueLine( + activeColor: color, + borderRadius: borderRadius, + lineSize: size, + highlighted: true, + isHorizontal: true, + duration: Duration(milliseconds: 300), + ), + ), + ), + ); + + final container = tester.widget(find.byType(Container)); + final decoration = container.decoration! as BoxDecoration; + expect(decoration.color, color); + expect(decoration.borderRadius, const BorderRadius.all(borderRadius)); + }); + + testWidgets('has correct width and height for horizontal orientation', + (tester) async { + const size = Size(120, 8); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepValueLine( + activeColor: Colors.blue, + borderRadius: Radius.zero, + lineSize: size, + highlighted: true, + isHorizontal: true, + duration: Duration(milliseconds: 100), + ), + ), + ), + ); + + // At start of animation, width should be 0 + final container0 = tester.widget(find.byType(Container)); + expect(container0.constraints?.minWidth ?? 0, 0); + + // Complete the animation + await tester.pumpAndSettle(); + + final container = tester.widget(find.byType(Container)); + expect(container.constraints?.minWidth ?? 0, size.width); + expect(container.constraints?.minHeight ?? 0, size.height); + }); + + testWidgets('has correct width and height for vertical orientation', + (tester) async { + const size = Size(12, 60); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepValueLine( + activeColor: Colors.green, + borderRadius: Radius.zero, + lineSize: size, + highlighted: true, + isHorizontal: false, + duration: Duration(milliseconds: 100), + ), + ), + ), + ); + + // At start of animation, height should be 0 + final container0 = tester.widget(find.byType(Container)); + expect(container0.constraints?.minHeight ?? 0, 0); + + // Complete the animation + await tester.pumpAndSettle(); + + final container = tester.widget(find.byType(Container)); + expect(container.constraints?.minHeight ?? 0, size.height); + expect(container.constraints?.minWidth ?? 0, size.width); + }); + + testWidgets('sets width/height to 0 when highlighted is false', + (tester) async { + const size = Size(50, 20); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepValueLine( + activeColor: Colors.black, + borderRadius: Radius.zero, + lineSize: size, + highlighted: false, + isHorizontal: true, + duration: Duration(milliseconds: 100), + ), + ), + ), + ); + + final container = tester.widget(find.byType(Container)); + expect(container.constraints?.minWidth ?? 0, 0); + expect(container.constraints?.minHeight ?? 0, size.height); + }); + + testWidgets('calls onAnimationCompleted when animation finishes', + (tester) async { + bool completed = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StepValueLine( + activeColor: Colors.orange, + borderRadius: Radius.zero, + lineSize: const Size(40, 8), + highlighted: true, + isHorizontal: true, + duration: const Duration(milliseconds: 50), + onAnimationCompleted: () { + completed = true; + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(completed, isTrue); + }); + + testWidgets('disposes animationController without error', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepValueLine( + activeColor: Colors.purple, + borderRadius: Radius.zero, + lineSize: Size(30, 5), + highlighted: true, + isHorizontal: true, + duration: Duration(milliseconds: 10), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + await tester.pumpWidget(Container()); // Unmount widget + // No exceptions should be thrown + }); + }); +} From 72fb70dcfef01f58314afdf3923d39ac91f1c368 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 16:12:39 +0330 Subject: [PATCH 05/11] feat: handle play and pause animation of progress --- example/lib/example_twenty_one.dart | 19 +++++++++++++--- lib/src/step_line/step_line.dart | 11 +++++++++- lib/src/step_line/step_value_line.dart | 22 +++++++++++++++++++ lib/src/step_progress.dart | 20 ++++++++--------- lib/src/step_progress_controller.dart | 7 ++++++ .../horizontal_step_progress.dart | 4 +++- .../step_progress_widget.dart | 8 +++++++ .../vertical_step_progress.dart | 4 +++- 8 files changed, 79 insertions(+), 16 deletions(-) diff --git a/example/lib/example_twenty_one.dart b/example/lib/example_twenty_one.dart index 6c3db42..4878646 100644 --- a/example/lib/example_twenty_one.dart +++ b/example/lib/example_twenty_one.dart @@ -6,8 +6,7 @@ class ExampleTwentyOne extends StatelessWidget { @override Widget build(BuildContext context) { - final stepProgressController = - StepProgressController(totalSteps: 5, initialStep: -1); + final stepProgressController = StepProgressController(totalSteps: 5); return Scaffold( backgroundColor: Colors.black45, appBar: AppBar( @@ -19,6 +18,10 @@ class ExampleTwentyOne extends StatelessWidget { controller: stepProgressController, visibilityOptions: StepProgressVisibilityOptions.lineOnly, autoStartProgress: true, + onStepChanged: (currentIndex) { + // Notice that the currentIndex starts from 1 in the LineOnly mode + debugPrint('Current step changed to: $currentIndex'); + }, theme: const StepProgressThemeData( activeForegroundColor: Color.fromARGB(255, 255, 255, 255), defaultForegroundColor: Color.fromARGB(255, 171, 168, 168), @@ -41,7 +44,17 @@ class ExampleTwentyOne extends StatelessWidget { ), ElevatedButton( onPressed: () { - // play or pause the step progress + if (stepProgressController.isAnimating()) { + stepProgressController.pauseAnimation(); + } + }, + child: const Icon(Icons.pause, size: 20), + ), + ElevatedButton( + onPressed: () { + if (!stepProgressController.isAnimating()) { + stepProgressController.playAnimation(); + } }, child: const Icon(Icons.play_arrow, size: 20), ), diff --git a/lib/src/step_line/step_line.dart b/lib/src/step_line/step_line.dart index 402f884..baa9a2c 100644 --- a/lib/src/step_line/step_line.dart +++ b/lib/src/step_line/step_line.dart @@ -30,7 +30,10 @@ import 'package:step_progress/step_progress.dart'; /// active step. It defaults to `false`. /// /// The [isAutoStepChange] parameter determines if the step progression advances -/// automatically. It defaults to `false`. +/// automatically. It defaults to `false`. +/// +/// The [controller] parameter is an optional [StepProgressController] that can +/// be used to manage and update the step progress state. It defaults to `null`. /// /// The [onTap] parameter is a callback function that is executed when the step /// line is tapped. It is optional and defaults to `null`. @@ -48,6 +51,7 @@ import 'package:step_progress/step_progress.dart'; /// isReversed: true, /// isCurrentStep: true, /// isAutoStepChange: false, +/// controller: myStepProgressController, /// stepLineStyle: StepLineStyle( /// color: Colors.blue, /// thickness: 2.0, @@ -69,6 +73,7 @@ class StepLine extends StatelessWidget { this.highlighted = false, this.isReversed = false, this.onStepLineAnimationCompleted, + this.controller, this.onTap, super.key, }); @@ -97,6 +102,9 @@ class StepLine extends StatelessWidget { /// Determines if step changes occur automatically. final bool isAutoStepChange; + /// Optional controller to manage and update the step progress state. + final StepProgressController? controller; + @override Widget build(BuildContext context) { final theme = StepProgressTheme.of(context)!.data; @@ -145,6 +153,7 @@ class StepLine extends StatelessWidget { : AlignmentDirectional.topStart), child: isCurrentStep && isAutoStepChange ? StepValueLine( + controller: controller, duration: style.animationDuration ?? theme.stepAnimationDuration, activeColor: activeColor, diff --git a/lib/src/step_line/step_value_line.dart b/lib/src/step_line/step_value_line.dart index db40db2..007e94c 100644 --- a/lib/src/step_line/step_value_line.dart +++ b/lib/src/step_line/step_value_line.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:step_progress/src/step_progress_controller.dart'; /// A widget that displays a step progress line with customizable appearance. /// Supports animation, orientation, and completion callbacks. @@ -12,6 +13,7 @@ class StepValueLine extends StatefulWidget { required this.isHorizontal, required this.duration, this.onAnimationCompleted, + this.controller, super.key, }); @@ -36,6 +38,9 @@ class StepValueLine extends StatefulWidget { /// The duration of the animation. final Duration duration; + /// Optional controller to manage and update the step progress state. + final StepProgressController? controller; + @override State createState() => _StepValueLineState(); } @@ -53,6 +58,9 @@ class _StepValueLineState extends State if (widget.onAnimationCompleted != null) { animationController.addStatusListener(statusListener); } + widget.controller?.playAnimation = playAnimation; + widget.controller?.pauseAnimation = pauseAnimation; + widget.controller?.isAnimating = () => animationController.isAnimating; super.initState(); } @@ -71,6 +79,20 @@ class _StepValueLineState extends State super.dispose(); } + /// Starts the animation if it is not already running. + void playAnimation() { + if (!animationController.isAnimating) { + animationController.forward(); + } + } + + /// Pauses the animation if it is currently running. + void pauseAnimation() { + if (animationController.isAnimating) { + animationController.stop(); + } + } + @override Widget build(BuildContext context) { return AnimatedBuilder( diff --git a/lib/src/step_progress.dart b/lib/src/step_progress.dart index d3f1711..00aa9ce 100644 --- a/lib/src/step_progress.dart +++ b/lib/src/step_progress.dart @@ -256,12 +256,11 @@ class _StepProgressState extends State _changeStep(widget.controller!.currentStep); }); if (widget.autoStartProgress) { - Future.delayed( - Duration.zero, - () => _onStepAnimationCompleted( - index: _currentStep >= 0 ? _currentStep : 0, - ), - ); + // Because the first step has no line, we start from 1 + if (_currentStep <= 0) { + _currentStep = 1; + widget.onStepChanged?.call(_currentStep); + } } super.initState(); } @@ -325,11 +324,10 @@ class _StepProgressState extends State newStep >= widget.totalSteps) { return; } + _previousStep = _currentStep; + _currentStep = newStep; if (mounted) { - setState(() { - _previousStep = _currentStep; - _currentStep = newStep; - }); + setState(() {}); } if (_currentStep != widget.controller?.currentStep) { widget.controller?.setCurrentStep(_currentStep); @@ -377,6 +375,7 @@ class _StepProgressState extends State padding: widget.padding, child: widget.axis == Axis.horizontal ? HorizontalStepProgress( + controller: widget.controller, totalSteps: widget.totalSteps, currentStep: _currentStep, isAutoStepChange: widget.autoStartProgress, @@ -397,6 +396,7 @@ class _StepProgressState extends State nodeLabelBuilder: widget.nodeLabelBuilder, ) : VerticalStepProgress( + controller: widget.controller, totalSteps: widget.totalSteps, currentStep: _currentStep, isAutoStepChange: widget.autoStartProgress, diff --git a/lib/src/step_progress_controller.dart b/lib/src/step_progress_controller.dart index a4bf349..5e7a27a 100644 --- a/lib/src/step_progress_controller.dart +++ b/lib/src/step_progress_controller.dart @@ -25,8 +25,15 @@ class StepProgressController extends ChangeNotifier { /// Returns current step int get currentStep => _currentStep; + /// Stores the previous step index based on the current step. late int prevStep = currentStep - 1; + VoidCallback playAnimation = () {}; + + VoidCallback pauseAnimation = () {}; + + bool Function() isAnimating = () => false; + /// validate newStep and set it to current step void setCurrentStep(int newStep) { assert( diff --git a/lib/src/step_progress_widgets/horizontal_step_progress.dart b/lib/src/step_progress_widgets/horizontal_step_progress.dart index f03e32d..95dfc0a 100644 --- a/lib/src/step_progress_widgets/horizontal_step_progress.dart +++ b/lib/src/step_progress_widgets/horizontal_step_progress.dart @@ -53,6 +53,7 @@ class HorizontalStepProgress extends StepProgressWidget { required super.visibilityOptions, required super.needsRebuildWidget, super.onStepLineAnimationCompleted, + super.controller, super.highlightOptions, super.isAutoStepChange, super.reversed, @@ -132,13 +133,14 @@ class HorizontalStepProgress extends StepProgressWidget { Widget buildWidget() { List children = List.generate(totalSteps - 1, (index) { return StepLine( + controller: controller, isReversed: reversed, isCurrentStep: currentStep == index + 1, isAutoStepChange: isAutoStepChange, highlighted: isHighlightedStepLine(index), onStepLineAnimationCompleted: () => onStepLineAnimationCompleted?.call( - index: index, + index: index + 1, ), onTap: () => onStepLineTapped?.call(index), ); diff --git a/lib/src/step_progress_widgets/step_progress_widget.dart b/lib/src/step_progress_widgets/step_progress_widget.dart index 4f036a3..32f253d 100644 --- a/lib/src/step_progress_widgets/step_progress_widget.dart +++ b/lib/src/step_progress_widgets/step_progress_widget.dart @@ -4,6 +4,7 @@ import 'package:step_progress/src/step_label/step_label_style.dart'; import 'package:step_progress/src/step_label_alignment.dart'; import 'package:step_progress/src/step_line/step_line_style.dart'; import 'package:step_progress/src/step_progress.dart'; +import 'package:step_progress/src/step_progress_controller.dart'; import 'package:step_progress/src/step_progress_highlight_options.dart'; import 'package:step_progress/src/step_progress_theme.dart'; import 'package:step_progress/src/step_progress_visibility_options.dart'; @@ -53,6 +54,8 @@ typedef OnStepLineAnimationCompleted = void Function({ /// This is triggered when dynamic size calculations are needed. /// - [highlightOptions]: Options to customize the highlight behavior of the /// step progress widget. +/// - [controller]: Optional controller to manage and update the step progress +/// state. abstract class StepProgressWidget extends StatelessWidget { const StepProgressWidget({ required this.totalSteps, @@ -63,6 +66,7 @@ abstract class StepProgressWidget extends StatelessWidget { required this.needsRebuildWidget, this.highlightOptions = StepProgressHighlightOptions.highlightCompletedNodesAndLines, + this.controller, this.onStepLineAnimationCompleted, this.isAutoStepChange = false, this.reversed = false, @@ -148,8 +152,12 @@ abstract class StepProgressWidget extends StatelessWidget { /// Options to customize the highlight behavior of the step progress widget. final StepProgressHighlightOptions highlightOptions; + /// Callback triggered when the step line animation completes. final OnStepLineAnimationCompleted? onStepLineAnimationCompleted; + /// Optional controller to manage and update the step progress state. + final StepProgressController? controller; + /// Determines if a step line at the given index should be highlighted /// based on the current step and highlight options. bool isHighlightedStepLine(int index) { diff --git a/lib/src/step_progress_widgets/vertical_step_progress.dart b/lib/src/step_progress_widgets/vertical_step_progress.dart index 7c74003..8448603 100644 --- a/lib/src/step_progress_widgets/vertical_step_progress.dart +++ b/lib/src/step_progress_widgets/vertical_step_progress.dart @@ -94,6 +94,7 @@ class VerticalStepProgress extends StepProgressWidget { required super.visibilityOptions, required super.needsRebuildWidget, super.onStepLineAnimationCompleted, + super.controller, super.highlightOptions, super.isAutoStepChange, super.reversed, @@ -170,6 +171,7 @@ class VerticalStepProgress extends StepProgressWidget { Widget buildWidget() { List children = List.generate(totalSteps - 1, (index) { return StepLine( + controller: controller, axis: Axis.vertical, isReversed: reversed, isCurrentStep: currentStep == index + 1, @@ -177,7 +179,7 @@ class VerticalStepProgress extends StepProgressWidget { highlighted: isHighlightedStepLine(index), onStepLineAnimationCompleted: () => onStepLineAnimationCompleted?.call( - index: index, + index: index + 1, ), onTap: () => onStepLineTapped?.call(index), ); From e077155071c467d405fcafa26d2692332ac45c60 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 16:59:35 +0330 Subject: [PATCH 06/11] update README.md file and add new sample --- README.md | 31 +++++++++++++++++++++++++++++ example/lib/example_twenty_one.dart | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b5f080..aedf5a8 100644 --- a/README.md +++ b/README.md @@ -715,6 +715,37 @@ StepProgress( ``` +### Example 21: Instagram Story Stepper +![StepProgress-horizontal-instagram-story-stepper](https://raw.githubusercontent.com/TalebRafiepour/showcase/main/step_progress/sample-21-stepprogress-instagaram-story-stepper.gif) + +
+ Show Implementation + +```dart +StepProgress( + totalSteps: 5, + padding: const EdgeInsets.all(10), + controller: stepProgressController, + visibilityOptions: StepProgressVisibilityOptions.lineOnly, + autoStartProgress: true, + onStepChanged: (currentIndex) { + // Notice that the currentIndex starts from 1 in the LineOnly mode + debugPrint('Current step changed to: $currentIndex'); + }, + theme: const StepProgressThemeData( + activeForegroundColor: Color.fromARGB(255, 255, 255, 255), + defaultForegroundColor: Color.fromARGB(255, 171, 168, 168), + stepLineSpacing: 3, + stepLineStyle: StepLineStyle( + lineThickness: 5, + animationDuration: Duration(seconds: 3), + borderRadius: Radius.circular(5), + ), + ), +), +``` +
+ ## Installation To use StepProgress, add it to your `pubspec.yaml` file: diff --git a/example/lib/example_twenty_one.dart b/example/lib/example_twenty_one.dart index 4878646..d43a1b3 100644 --- a/example/lib/example_twenty_one.dart +++ b/example/lib/example_twenty_one.dart @@ -28,7 +28,7 @@ class ExampleTwentyOne extends StatelessWidget { stepLineSpacing: 3, stepLineStyle: StepLineStyle( lineThickness: 5, - animationDuration: Duration(seconds: 6), + animationDuration: Duration(seconds: 3), borderRadius: Radius.circular(5), ), ), From 10abbd738d60c3fde2d35cdb506a6b2eb1221002 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 17:21:20 +0330 Subject: [PATCH 07/11] test: update the StepLine class tests --- test/src/step_line/step_line_test.dart | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/src/step_line/step_line_test.dart b/test/src/step_line/step_line_test.dart index 8fdd9d3..385b2eb 100644 --- a/test/src/step_line/step_line_test.dart +++ b/test/src/step_line/step_line_test.dart @@ -39,18 +39,18 @@ void main() { // Wait for animations to finish. await tester.pumpAndSettle(); - // The AnimatedContainer inside StepLine is used for the active color + // The container inside StepLine is used for the active color // fill. - final animatedContainerFinder = find.descendant( + final containerFinder = find.descendant( of: find.byKey(testKey), - matching: find.byType(AnimatedContainer), + matching: find.byType(Container), ); - expect(animatedContainerFinder, findsOneWidget); + expect(containerFinder, findsNWidgets(2)); // For horizontal active, the animated container width should equal // the parent's width (200) // and its height should equal the defined lineThickness. - final animatedSize = tester.getSize(animatedContainerFinder); + final animatedSize = tester.getSize(containerFinder.last); expect(animatedSize.width, equals(200)); expect(animatedSize.height, equals(lineThickness)); }, @@ -77,14 +77,14 @@ void main() { await tester.pumpAndSettle(); - final animatedContainerFinder = find.descendant( + final containerFinder = find.descendant( of: find.byKey(testKey), - matching: find.byType(AnimatedContainer), + matching: find.byType(Container), ); - expect(animatedContainerFinder, findsOneWidget); + expect(containerFinder, findsNWidgets(2)); - final animatedSize = tester.getSize(animatedContainerFinder); - // For horizontal inactive, the animated container width should be 0 + final animatedSize = tester.getSize(containerFinder.last); + // For horizontal inactive, the active container width should be 0 // while the height remains equal to lineThickness. expect(animatedSize.width, equals(0)); expect(animatedSize.height, equals(lineThickness)); @@ -117,13 +117,13 @@ void main() { await tester.pumpAndSettle(); - final animatedContainerFinder = find.descendant( + final containerFinder = find.descendant( of: find.byKey(testKey), - matching: find.byType(AnimatedContainer), + matching: find.byType(Container), ); - expect(animatedContainerFinder, findsOneWidget); + expect(containerFinder, findsNWidgets(2)); - final animatedSize = tester.getSize(animatedContainerFinder); + final animatedSize = tester.getSize(containerFinder.last); // For vertical active, the width is set to the lineThickness // and height should match parent's height (200). expect(animatedSize.width, equals(lineThickness)); @@ -156,13 +156,13 @@ void main() { await tester.pumpAndSettle(); - final animatedContainerFinder = find.descendant( + final containerFinder = find.descendant( of: find.byKey(testKey), - matching: find.byType(AnimatedContainer), + matching: find.byType(Container), ); - expect(animatedContainerFinder, findsOneWidget); + expect(containerFinder, findsNWidgets(2)); - final animatedSize = tester.getSize(animatedContainerFinder); + final animatedSize = tester.getSize(containerFinder.last); // For vertical inactive, the animated container height should be 0 // while the width remains equal to lineThickness. expect(animatedSize.width, equals(lineThickness)); From 693cc5c67eb623a1a5e6f10aa2667835d4113d5d Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 17:56:03 +0330 Subject: [PATCH 08/11] test: write new test to cover autoStartProgress property of StepProgress class --- lib/src/step_progress.dart | 1 - test/src/step_progress_test.dart | 132 +++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/lib/src/step_progress.dart b/lib/src/step_progress.dart index 00aa9ce..36a4d10 100644 --- a/lib/src/step_progress.dart +++ b/lib/src/step_progress.dart @@ -259,7 +259,6 @@ class _StepProgressState extends State // Because the first step has no line, we start from 1 if (_currentStep <= 0) { _currentStep = 1; - widget.onStepChanged?.call(_currentStep); } } super.initState(); diff --git a/test/src/step_progress_test.dart b/test/src/step_progress_test.dart index 108d22f..a9ee741 100644 --- a/test/src/step_progress_test.dart +++ b/test/src/step_progress_test.dart @@ -600,4 +600,136 @@ void main() { ); // Only one text widget should exist }); }); + + group( + 'StepProgress autoStartProgress', + () { + testWidgets('Should rebuild when autoStartProgress is true', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepProgress( + totalSteps: 3, + currentStep: 0, + autoStartProgress: true, + ), + ), + ), + ); + + // After pump, the widget should auto-advance to step 1 + await tester.pumpAndSettle(); + expect(find.byType(StepNode), findsNWidgets(3)); + }); + + testWidgets('Should not auto-advance if autoStartProgress is false', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepProgress( + totalSteps: 3, + currentStep: 0, + autoStartProgress: false, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + // The currentStep should remain at 0 + expect(find.byType(StepNode), findsNWidgets(3)); + // Optionally, check that the first node is still the active one + // (implementation detail may vary) + }); + + testWidgets( + 'Should auto-advance only once when autoStartProgress is true', + (tester) async { + int stepChangedCount = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StepProgress( + totalSteps: 3, + currentStep: 0, + autoStartProgress: true, + onStepChanged: (_) { + stepChangedCount++; + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(stepChangedCount, equals(1)); + }); + + testWidgets('Should not auto-advance if currentStep is last step', + (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: StepProgress( + totalSteps: 3, + currentStep: 2, + autoStartProgress: true, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + // Should not advance beyond last step + expect(find.byType(StepNode), findsNWidgets(3)); + }); + + testWidgets('Should auto-advance to next step if not last step', + (tester) async { + int? changedStep; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StepProgress( + totalSteps: 3, + currentStep: 1, + autoStartProgress: true, + onStepChanged: (step) { + changedStep = step; + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(changedStep, equals(2)); + }); + + testWidgets('Should auto-advance with reversed order', (tester) async { + int? changedStep; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StepProgress( + totalSteps: 3, + currentStep: -1, + autoStartProgress: true, + reversed: true, + onStepChanged: (step) { + // in auto start progress, the first step is 1 + changedStep = step;// the second step triggrered and must be 2 + }, + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(changedStep, equals(2)); + }); + }, + ); } From c32b43808e01cb8ebd938464395f9fca1689cf20 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 18:06:56 +0330 Subject: [PATCH 09/11] test: write new group of test for testing AutoStart progress of StepLine --- test/src/step_line/step_line_test.dart | 424 ++++++++++++++++++++++++- test/src/step_progress_test.dart | 3 +- 2 files changed, 425 insertions(+), 2 deletions(-) diff --git a/test/src/step_line/step_line_test.dart b/test/src/step_line/step_line_test.dart index 385b2eb..4f91695 100644 --- a/test/src/step_line/step_line_test.dart +++ b/test/src/step_line/step_line_test.dart @@ -5,12 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:step_progress/src/step_line/breadcrumb_clipper.dart'; import 'package:step_progress/src/step_line/step_line.dart'; +import 'package:step_progress/src/step_line/step_value_line.dart'; import 'package:step_progress/step_progress.dart'; import '../helper/test_theme_wrapper.dart'; void main() { - group('StepLine Widget Tests', () { + group('StepLine Widget Fundamental Tests', () { const lineThickness = 5.0; const dummyStepLineStyle = StepLineStyle(lineThickness: lineThickness); testWidgets( @@ -407,4 +408,425 @@ void main() { }, ); }); + + group('StepLine Widget AutoStartProgress Tests', () { + testWidgets( + 'StepLine with isAutoStepChange and isCurrentStep triggers animation and ' + 'calls onStepLineAnimationCompleted', + (tester) async { + bool animationCompleted = false; + const testKey = Key('step_line_auto_current'); + const style = StepLineStyle(lineThickness: 10); + + await tester.pumpWidget( + TestThemeWrapper( + child: SizedBox( + width: 100, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: true, + onStepLineAnimationCompleted: () { + animationCompleted = true; + }, + ), + ], + ), + ), + ), + ); + + // Wait for animation to complete + await tester.pumpAndSettle(); + + expect(animationCompleted, isTrue); + + // Should contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'StepLine with isAutoStepChange but not current step does not show' + ' StepValueLine', + (tester) async { + const testKey = Key('step_line_auto_not_current'); + const style = StepLineStyle(lineThickness: 10); + + await tester.pumpWidget( + const TestThemeWrapper( + child: SizedBox( + width: 100, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: false, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Should not contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsNothing, + ); + }, + ); + + testWidgets( + 'StepLine with isCurrentStep but isAutoStepChange false does not show' + ' StepValueLine', + (tester) async { + const testKey = Key('step_line_current_no_auto'); + const style = StepLineStyle(lineThickness: 10); + + await tester.pumpWidget( + const TestThemeWrapper( + child: SizedBox( + width: 100, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: false, + isCurrentStep: true, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Should not contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsNothing, + ); + }, + ); + + testWidgets( + 'StepLine with isAutoStepChange and isCurrentStep vertical axis triggers' + ' animation', + (tester) async { + bool animationCompleted = false; + const testKey = Key('step_line_auto_current_vertical'); + const style = StepLineStyle(lineThickness: 8); + + await tester.pumpWidget( + TestThemeWrapper( + child: SizedBox( + width: 20, + height: 100, + child: Column( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + axis: Axis.vertical, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: true, + onStepLineAnimationCompleted: () { + animationCompleted = true; + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(animationCompleted, isTrue); + + // Should contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'StepLine with isAutoStepChange, isCurrentStep, and reversed ' + 'horizontal axis', + (tester) async { + bool animationCompleted = false; + const testKey = Key('step_line_auto_current_reversed'); + const style = StepLineStyle(lineThickness: 6); + + await tester.pumpWidget( + TestThemeWrapper( + child: SizedBox( + width: 120, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: true, + isReversed: true, + onStepLineAnimationCompleted: () { + animationCompleted = true; + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(animationCompleted, isTrue); + + // Should contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'StepLine with isAutoStepChange, isCurrentStep, and breadcrumb style', + (tester) async { + bool animationCompleted = false; + const testKey = Key('step_line_auto_current_breadcrumb'); + const style = StepLineStyle( + lineThickness: 8, + isBreadcrumb: true, + chevronAngle: 20, + ); + + await tester.pumpWidget( + TestThemeWrapper( + child: SizedBox( + width: 100, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: true, + onStepLineAnimationCompleted: () { + animationCompleted = true; + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(animationCompleted, isTrue); + + // Should contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsOneWidget, + ); + + // Should contain ClipPath widget for breadcrumb + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(ClipPath), + ), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'StepLine with isAutoStepChange, isCurrentStep, and dotted border style', + (tester) async { + bool animationCompleted = false; + const testKey = Key('step_line_auto_current_dotted'); + const dottedBorderStyle = OuterBorderStyle( + isDotted: true, + borderWidth: 2, + dashPattern: [4, 2], + defaultBorderColor: Colors.grey, + activeBorderColor: Colors.blue, + ); + const style = StepLineStyle( + lineThickness: 8, + borderStyle: dottedBorderStyle, + ); + + await tester.pumpWidget( + TestThemeWrapper( + child: SizedBox( + width: 100, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: true, + onStepLineAnimationCompleted: () { + animationCompleted = true; + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(animationCompleted, isTrue); + + // Should contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsOneWidget, + ); + + // Should contain DottedBorder widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(DottedBorder), + ), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'StepLine with isAutoStepChange, isCurrentStep, dotted border and ' + 'breadcrumb style', + (tester) async { + bool animationCompleted = false; + const testKey = Key('step_line_auto_current_dotted_breadcrumb'); + const dottedBorderStyle = OuterBorderStyle( + isDotted: true, + borderWidth: 2, + dashPattern: [4, 2], + defaultBorderColor: Colors.grey, + activeBorderColor: Colors.blue, + ); + const style = StepLineStyle( + lineThickness: 8, + borderStyle: dottedBorderStyle, + isBreadcrumb: true, + chevronAngle: 25, + ); + + await tester.pumpWidget( + TestThemeWrapper( + child: SizedBox( + width: 100, + height: 20, + child: Row( + children: [ + StepLine( + key: testKey, + stepLineStyle: style, + highlighted: true, + isAutoStepChange: true, + isCurrentStep: true, + onStepLineAnimationCompleted: () { + animationCompleted = true; + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(animationCompleted, isTrue); + + // Should contain StepValueLine widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(StepValueLine), + ), + findsOneWidget, + ); + + // Should contain DottedBorder widget + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(DottedBorder), + ), + findsOneWidget, + ); + + // Should contain ClipPath widget for breadcrumb + expect( + find.descendant( + of: find.byKey(testKey), + matching: find.byType(ClipPath), + ), + findsOneWidget, + ); + }, + ); + }); } diff --git a/test/src/step_progress_test.dart b/test/src/step_progress_test.dart index a9ee741..113482d 100644 --- a/test/src/step_progress_test.dart +++ b/test/src/step_progress_test.dart @@ -720,7 +720,8 @@ void main() { reversed: true, onStepChanged: (step) { // in auto start progress, the first step is 1 - changedStep = step;// the second step triggrered and must be 2 + changedStep = + step; // the second step triggrered and must be 2 }, ), ), From f97997833f2407354ee3b2b95a351db7925fdff8 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 18:11:08 +0330 Subject: [PATCH 10/11] test: write more test for StepProgressController --- test/src/step_progress_controller_test.dart | 74 +++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/src/step_progress_controller_test.dart b/test/src/step_progress_controller_test.dart index 3f60f97..4e42bbe 100644 --- a/test/src/step_progress_controller_test.dart +++ b/test/src/step_progress_controller_test.dart @@ -42,4 +42,78 @@ void main() { expect(notified, true); }); }); + + group('StepProgressController play and pause functions', () { + test('playAnimation is called', () { + final controller = StepProgressController(totalSteps: 3); + bool played = false; + controller.playAnimation = () { + played = true; + }; + controller.playAnimation(); + expect(played, true); + }); + + test('pauseAnimation is called', () { + final controller = StepProgressController(totalSteps: 3); + bool paused = false; + controller.pauseAnimation = () { + paused = true; + }; + controller.pauseAnimation(); + expect(paused, true); + }); + + test('isAnimating returns true when set to true', () { + final controller = StepProgressController(totalSteps: 3) + ..isAnimating = () => true; + expect(controller.isAnimating(), true); + }); + + test('isAnimating returns false when set to false', () { + final controller = StepProgressController(totalSteps: 3) + ..isAnimating = () => false; + expect(controller.isAnimating(), false); + }); + + test('playAnimation and pauseAnimation can be swapped', () { + final controller = StepProgressController(totalSteps: 3); + bool played = false; + bool paused = false; + controller + ..playAnimation = () { + played = true; + } + ..pauseAnimation = () { + paused = true; + }; + controller.playAnimation(); + controller.pauseAnimation(); + expect(played, true); + expect(paused, true); + }); + + test('isAnimating can be dynamically changed', () { + final controller = StepProgressController(totalSteps: 3) + ..isAnimating = () => false; + expect(controller.isAnimating(), false); + controller.isAnimating = () => true; + expect(controller.isAnimating(), true); + }); + + test('playAnimation default does nothing', () { + final controller = StepProgressController(totalSteps: 3); + expect(() => controller.playAnimation(), returnsNormally); + }); + + test('pauseAnimation default does nothing', () { + final controller = StepProgressController(totalSteps: 3); + expect(() => controller.pauseAnimation(), returnsNormally); + }); + + test('isAnimating default returns false', () { + final controller = StepProgressController(totalSteps: 3); + expect(controller.isAnimating(), false); + }); + }); } From c2ef9afe59cdf96451f91ce148f906c283ec3c14 Mon Sep 17 00:00:00 2001 From: TalebRafiepour Date: Thu, 24 Jul 2025 18:19:59 +0330 Subject: [PATCH 11/11] test: write test to verify onStepLineAnimationCompleted callback works correctly in vertical and horizontal widgets --- .../horizontal_step_progress_test.dart | 118 ++++++++++++++++++ .../vertical_step_progress_test.dart | 116 +++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/test/src/step_progress_widget/horizontal_step_progress_test.dart b/test/src/step_progress_widget/horizontal_step_progress_test.dart index d2172a5..1d620d2 100644 --- a/test/src/step_progress_widget/horizontal_step_progress_test.dart +++ b/test/src/step_progress_widget/horizontal_step_progress_test.dart @@ -503,5 +503,123 @@ void main() { } }, ); + testWidgets( + 'onStepLineAnimationCompleted callback is triggered for each StepLine', + (tester) async { + const int totalSteps = 4; + const int currentStep = 2; + const double stepSize = 50; + const visibilityOptions = StepProgressVisibilityOptions.both; + final List completedIndices = []; + + await tester.pumpWidget( + TestThemeWrapper( + child: Scaffold( + body: HorizontalStepProgress( + totalSteps: totalSteps, + currentStep: currentStep, + stepSize: stepSize, + visibilityOptions: visibilityOptions, + onStepLineAnimationCompleted: ({index = 0}) { + completedIndices.add(index); + }, + needsRebuildWidget: () {}, + ), + ), + ), + ); + + // Simulate triggering the animation completed callback for + // each StepLine. + final stepLineFinder = find.byType(StepLine); + final stepLineWidgets = + tester.widgetList(stepLineFinder).toList(); + + for (var i = 0; i < stepLineWidgets.length; i++) { + // Directly call the callback as StepLine does not animate in tests. + stepLineWidgets[i].onStepLineAnimationCompleted?.call(); + } + + // The callback should be called for each line with correct index. + expect(completedIndices.length, equals(totalSteps - 1)); + expect(completedIndices, containsAll([1, 2, 3])); + }, + ); + + testWidgets( + 'onStepLineAnimationCompleted is not called if not provided', + (tester) async { + const int totalSteps = 3; + const int currentStep = 1; + const double stepSize = 40; + const visibilityOptions = StepProgressVisibilityOptions.both; + + await tester.pumpWidget( + TestThemeWrapper( + child: Scaffold( + body: HorizontalStepProgress( + totalSteps: totalSteps, + currentStep: currentStep, + stepSize: stepSize, + visibilityOptions: visibilityOptions, + needsRebuildWidget: () {}, + ), + ), + ), + ); + + final stepLineFinder = find.byType(StepLine); + final stepLineWidgets = + tester.widgetList(stepLineFinder).toList(); + + for (var i = 0; i < stepLineWidgets.length; i++) { + // Should not throw if callback is null. + expect(() => stepLineWidgets[i].onStepLineAnimationCompleted?.call(), + returnsNormally); + } + }, + ); + + testWidgets( + 'onStepLineAnimationCompleted receives correct index for reversed' + ' HorizontalStepProgress', + (tester) async { + const int totalSteps = 4; + const int currentStep = 2; + const double stepSize = 50; + const visibilityOptions = StepProgressVisibilityOptions.both; + final List completedIndices = []; + + await tester.pumpWidget( + TestThemeWrapper( + child: Scaffold( + body: HorizontalStepProgress( + totalSteps: totalSteps, + currentStep: currentStep, + stepSize: stepSize, + visibilityOptions: visibilityOptions, + reversed: true, + onStepLineAnimationCompleted: ({index = 0}) { + completedIndices.add(index); + }, + needsRebuildWidget: () {}, + ), + ), + ), + ); + + final stepLineFinder = find.byType(StepLine); + final stepLineWidgets = + tester.widgetList(stepLineFinder).toList(); + + for (var i = 0; i < stepLineWidgets.length; i++) { + stepLineWidgets[i].onStepLineAnimationCompleted?.call(); + } + + // Indices should still be [1, 2, 3] even when reversed. + expect(completedIndices.length, equals(totalSteps - 1)); + expect(completedIndices, containsAll([1, 2, 3])); + }, + ); }); } diff --git a/test/src/step_progress_widget/vertical_step_progress_test.dart b/test/src/step_progress_widget/vertical_step_progress_test.dart index b4b5873..43a4f1c 100644 --- a/test/src/step_progress_widget/vertical_step_progress_test.dart +++ b/test/src/step_progress_widget/vertical_step_progress_test.dart @@ -457,5 +457,121 @@ void main() { } }, ); + testWidgets( + 'VerticalStepProgress calls onStepLineAnimationCompleted for ' + 'correct index', (tester) async { + int? completedLineIndex; + final widget = TestThemeWrapper( + child: Scaffold( + body: VerticalStepProgress( + totalSteps: 4, + currentStep: 2, + stepSize: 30, + visibilityOptions: StepProgressVisibilityOptions.both, + nodeTitles: List.generate(4, (i) => 'Step ${i + 1}'), + nodeSubTitles: List.generate(4, (i) => 'Desc ${i + 1}'), + onStepNodeTapped: (_) {}, + onStepLineTapped: (_) {}, + nodeIconBuilder: (step, completedStepIndex) { + return Icon(Icons.circle, key: Key('node_$step')); + }, + onStepLineAnimationCompleted: ({index = 0}) { + completedLineIndex = index; + }, + needsRebuildWidget: () {}, + ), + ), + ); + + await tester.pumpWidget(widget); + + // Find all StepLine widgets + final stepLines = find.byType(StepLine); + final stepLineWidgets = tester.widgetList(stepLines).toList(); + + // Simulate animation completion by calling the callback manually + // (since StepLine is a custom widget, we assume it calls the callback + // when built) We'll trigger the callback for index 1 and verify + (stepLineWidgets[1].onStepLineAnimationCompleted ?? () {})(); + + expect(completedLineIndex, 2); // index passed to callback is index+1 + + // Also test for another index + (stepLineWidgets[0].onStepLineAnimationCompleted ?? () {})(); + expect(completedLineIndex, 1); + }); + + testWidgets( + 'VerticalStepProgress does not call onStepLineAnimationCompleted if not' + ' provided', (tester) async { + final widget = TestThemeWrapper( + child: Scaffold( + body: VerticalStepProgress( + totalSteps: 3, + currentStep: 1, + stepSize: 30, + visibilityOptions: StepProgressVisibilityOptions.both, + nodeTitles: List.generate(3, (i) => 'Step ${i + 1}'), + nodeSubTitles: List.generate(3, (i) => 'Desc ${i + 1}'), + onStepNodeTapped: (_) {}, + onStepLineTapped: (_) {}, + nodeIconBuilder: (step, completedStepIndex) { + return Icon(Icons.circle, key: Key('node_$step')); + }, + needsRebuildWidget: () {}, + ), + ), + ); + + await tester.pumpWidget(widget); + + // Find all StepLine widgets + final stepLines = find.byType(StepLine); + final stepLineWidgets = tester.widgetList(stepLines).toList(); + + // Should not throw if callback is not provided + expect(() { + (stepLineWidgets[0].onStepLineAnimationCompleted ?? () {})(); + }, returnsNormally); + }); + + testWidgets( + 'VerticalStepProgress onStepLineAnimationCompleted is called for' + ' each line', (tester) async { + final calledIndices = []; + final widget = TestThemeWrapper( + child: Scaffold( + body: VerticalStepProgress( + totalSteps: 5, + currentStep: 4, + stepSize: 30, + visibilityOptions: StepProgressVisibilityOptions.both, + nodeTitles: List.generate(5, (i) => 'Step ${i + 1}'), + nodeSubTitles: List.generate(5, (i) => 'Desc ${i + 1}'), + onStepNodeTapped: (_) {}, + onStepLineTapped: (_) {}, + nodeIconBuilder: (step, completedStepIndex) { + return Icon(Icons.circle, key: Key('node_$step')); + }, + onStepLineAnimationCompleted: ({index = 0}) { + calledIndices.add(index); + }, + needsRebuildWidget: () {}, + ), + ), + ); + + await tester.pumpWidget(widget); + + final stepLines = find.byType(StepLine); + final stepLineWidgets = tester.widgetList(stepLines).toList(); + + // Simulate animation completion for all lines + for (var i = 0; i < stepLineWidgets.length; i++) { + (stepLineWidgets[i].onStepLineAnimationCompleted ?? () {})(); + } + + expect(calledIndices, equals([1, 2, 3, 4])); + }); }); }