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
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
+
+
+
+ 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_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..d43a1b3 100644
--- a/example/lib/example_twenty_one.dart
+++ b/example/lib/example_twenty_one.dart
@@ -7,33 +7,30 @@ 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,
+ autoStartProgress: true,
+ onStepChanged: (currentIndex) {
+ // Notice that the currentIndex starts from 1 in the LineOnly mode
+ debugPrint('Current step changed to: $currentIndex');
+ },
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,
+ animationDuration: Duration(seconds: 3),
+ borderRadius: Radius.circular(5),
),
- activeForegroundColor: Color(0xFF181818),
- defaultForegroundColor: Color(0xff4c4c4c),
),
),
bottomNavigationBar: SafeArea(
@@ -43,11 +40,27 @@ class ExampleTwentyOne extends StatelessWidget {
children: [
ElevatedButton(
onPressed: stepProgressController.previousStep,
- child: const Text('Prev'),
+ child: const Icon(Icons.arrow_back, size: 20),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ 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),
),
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:
diff --git a/lib/src/step_line/step_line.dart b/lib/src/step_line/step_line.dart
index f8d6129..baa9a2c 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,16 +26,32 @@ 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 [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`.
///
+/// 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,
+/// isCurrentStep: true,
+/// isAutoStepChange: false,
+/// controller: myStepProgressController,
/// stepLineStyle: StepLineStyle(
/// color: Colors.blue,
/// thickness: 2.0,
@@ -42,14 +59,21 @@ import 'package:step_progress/step_progress.dart';
/// onTap: () {
/// print('Step line tapped');
/// },
+/// onStepLineAnimationCompleted: () {
+/// print('Line animation completed');
+/// },
/// )
/// ```
class StepLine extends StatelessWidget {
const StepLine({
+ this.isCurrentStep = false,
+ this.isAutoStepChange = false,
this.axis = Axis.horizontal,
this.stepLineStyle,
this.highlighted = false,
this.isReversed = false,
+ this.onStepLineAnimationCompleted,
+ this.controller,
this.onTap,
super.key,
});
@@ -69,6 +93,18 @@ 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;
+
+ /// Indicates whether this step is the current active step.
+ final bool isCurrentStep;
+
+ /// 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;
@@ -76,6 +112,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,
@@ -106,21 +144,37 @@ class StepLine extends StatelessWidget {
width: lineSize.width,
height: lineSize.height,
decoration: containerDecoration,
- alignment: AlignmentDirectional.centerStart,
- 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),
- ),
- curve: Curves.fastLinearToSlowEaseIn,
- duration: style.animationDuration ?? theme.stepAnimationDuration,
- ),
+ alignment: _isHorizontal
+ ? (isReversed
+ ? AlignmentDirectional.centerEnd
+ : AlignmentDirectional.centerStart)
+ : (isReversed
+ ? AlignmentDirectional.bottomEnd
+ : AlignmentDirectional.topStart),
+ child: isCurrentStep && isAutoStepChange
+ ? StepValueLine(
+ controller: controller,
+ 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..007e94c
--- /dev/null
+++ b/lib/src/step_line/step_value_line.dart
@@ -0,0 +1,120 @@
+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.
+/// 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,
+ this.controller,
+ 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;
+
+ /// Optional controller to manage and update the step progress state.
+ final StepProgressController? controller;
+
+ @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);
+ }
+ widget.controller?.playAnimation = playAnimation;
+ widget.controller?.pauseAnimation = pauseAnimation;
+ widget.controller?.isAnimating = () => animationController.isAnimating;
+ 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();
+ }
+
+ /// 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(
+ 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 e6c38f1..36a4d10 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(
@@ -241,12 +248,19 @@ class StepProgress extends StatefulWidget {
class _StepProgressState extends State
with SingleTickerProviderStateMixin {
late int _currentStep = _getCurrentStep;
+ late int _previousStep = _getPreviousStep;
@override
void initState() {
widget.controller?.addListener(() {
_changeStep(widget.controller!.currentStep);
});
+ if (widget.autoStartProgress) {
+ // Because the first step has no line, we start from 1
+ if (_currentStep <= 0) {
+ _currentStep = 1;
+ }
+ }
super.initState();
}
@@ -258,8 +272,6 @@ class _StepProgressState extends State
/// is empty.
DataCache().clearCache();
- /// Disposes of the controller if it is not null.
- widget.controller?.dispose();
super.dispose();
}
@@ -289,6 +301,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
@@ -305,10 +323,13 @@ class _StepProgressState extends State
newStep >= widget.totalSteps) {
return;
}
+ _previousStep = _currentStep;
+ _currentStep = newStep;
if (mounted) {
- setState(() {
- _currentStep = newStep;
- });
+ setState(() {});
+ }
+ if (_currentStep != widget.controller?.currentStep) {
+ widget.controller?.setCurrentStep(_currentStep);
}
widget.onStepChanged?.call(_currentStep);
}
@@ -323,6 +344,22 @@ class _StepProgressState extends State
}
}
+ /// Handles automatic step changes based on the current index and direction.
+ void _onStepAnimationCompleted({
+ int index = 0,
+ }) {
+ if (!widget.autoStartProgress) {
+ return;
+ } else {
+ final isForward = _currentStep > _previousStep;
+ if (isForward) {
+ _changeStep(index + 1);
+ } else {
+ _changeStep(index - 1);
+ }
+ }
+ }
+
@override
Widget build(BuildContext context) {
return StepProgressTheme(
@@ -337,10 +374,13 @@ class _StepProgressState extends State
padding: widget.padding,
child: widget.axis == Axis.horizontal
? HorizontalStepProgress(
+ controller: widget.controller,
totalSteps: widget.totalSteps,
currentStep: _currentStep,
+ isAutoStepChange: widget.autoStartProgress,
reversed: widget.reversed,
highlightOptions: widget.highlightOptions,
+ onStepLineAnimationCompleted: _onStepAnimationCompleted,
needsRebuildWidget: _needsRebuildWidget,
nodeTitles: widget.nodeTitles,
nodeSubTitles: widget.nodeSubTitles,
@@ -355,10 +395,13 @@ class _StepProgressState extends State
nodeLabelBuilder: widget.nodeLabelBuilder,
)
: VerticalStepProgress(
+ controller: widget.controller,
totalSteps: widget.totalSteps,
currentStep: _currentStep,
+ isAutoStepChange: widget.autoStartProgress,
reversed: widget.reversed,
highlightOptions: widget.highlightOptions,
+ 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..5e7a27a 100644
--- a/lib/src/step_progress_controller.dart
+++ b/lib/src/step_progress_controller.dart
@@ -25,6 +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(
@@ -32,6 +41,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 +53,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 +63,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 adf6b63..95dfc0a 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({
@@ -50,7 +52,10 @@ class HorizontalStepProgress extends StepProgressWidget {
required super.stepSize,
required super.visibilityOptions,
required super.needsRebuildWidget,
+ super.onStepLineAnimationCompleted,
+ super.controller,
super.highlightOptions,
+ super.isAutoStepChange,
super.reversed,
super.nodeTitles,
super.nodeSubTitles,
@@ -128,8 +133,15 @@ 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 + 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 ab459af..32f253d 100644
--- a/lib/src/step_progress_widgets/step_progress_widget.dart
+++ b/lib/src/step_progress_widgets/step_progress_widget.dart
@@ -4,10 +4,19 @@ 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';
+/// 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.
+typedef OnStepLineAnimationCompleted = void Function({
+ int index,
+});
+
/// An abstract class representing a step progress widget.
///
/// This widget displays a progress indicator with multiple steps, allowing for
@@ -25,24 +34,28 @@ 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.
+/// - [controller]: Optional controller to manage and update the step progress
+/// state.
abstract class StepProgressWidget extends StatelessWidget {
const StepProgressWidget({
required this.totalSteps,
@@ -53,6 +66,9 @@ abstract class StepProgressWidget extends StatelessWidget {
required this.needsRebuildWidget,
this.highlightOptions =
StepProgressHighlightOptions.highlightCompletedNodesAndLines,
+ this.controller,
+ this.onStepLineAnimationCompleted,
+ this.isAutoStepChange = false,
this.reversed = false,
this.nodeTitles,
this.nodeSubTitles,
@@ -87,6 +103,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;
@@ -133,6 +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 0b5a54f..8448603 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'],
@@ -89,7 +93,10 @@ class VerticalStepProgress extends StepProgressWidget {
required super.stepSize,
required super.visibilityOptions,
required super.needsRebuildWidget,
+ super.onStepLineAnimationCompleted,
+ super.controller,
super.highlightOptions,
+ super.isAutoStepChange,
super.reversed,
super.nodeTitles,
super.nodeSubTitles,
@@ -164,9 +171,16 @@ 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,
+ isAutoStepChange: isAutoStepChange,
highlighted: isHighlightedStepLine(index),
+ onStepLineAnimationCompleted: () =>
+ onStepLineAnimationCompleted?.call(
+ index: index + 1,
+ ),
onTap: () => onStepLineTapped?.call(index),
);
});
diff --git a/test/src/step_line/step_line_test.dart b/test/src/step_line/step_line_test.dart
index 8fdd9d3..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(
@@ -39,18 +40,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 +78,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 +118,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 +157,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));
@@ -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_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
+ });
+ });
+}
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);
+ });
+ });
}
diff --git a/test/src/step_progress_test.dart b/test/src/step_progress_test.dart
index 108d22f..113482d 100644
--- a/test/src/step_progress_test.dart
+++ b/test/src/step_progress_test.dart
@@ -600,4 +600,137 @@ 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));
+ });
+ },
+ );
}
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]));
+ });
});
}