[
61 | SizedBox(height: 16),
62 | SizedBox(
63 | height: 240,
64 | child: PageView.builder(
65 | controller: controller,
66 | // itemCount: pages.length,
67 | itemBuilder: (_, index) {
68 | return pages[index % pages.length];
69 | },
70 | ),
71 | ),
72 | Padding(
73 | padding: const EdgeInsets.only(top: 24, bottom: 12),
74 | child: Text(
75 | 'Worm',
76 | style: TextStyle(color: Colors.black54),
77 | ),
78 | ),
79 | SmoothPageIndicator(
80 | controller: controller,
81 | count: pages.length,
82 | // effect: const WormEffect(
83 | // dotHeight: 16,
84 | // dotWidth: 16,
85 | // type: WormType.thinUnderground,
86 | // ),
87 | ),
88 | Padding(
89 | padding: const EdgeInsets.only(top: 16, bottom: 8),
90 | child: Text(
91 | 'Jumping Dot',
92 | style: TextStyle(color: Colors.black54),
93 | ),
94 | ),
95 | SmoothPageIndicator(
96 | controller: controller,
97 | count: pages.length,
98 | effect: JumpingDotEffect(
99 | dotHeight: 16,
100 | dotWidth: 16,
101 | jumpScale: .7,
102 | verticalOffset: 15,
103 | ),
104 | ),
105 | Padding(
106 | padding: const EdgeInsets.only(top: 16, bottom: 12),
107 | child: Text(
108 | 'Scrolling Dots',
109 | style: TextStyle(color: Colors.black54),
110 | ),
111 | ),
112 | SmoothPageIndicator(
113 | controller: controller,
114 | count: pages.length,
115 | effect: ScrollingDotsEffect(
116 | activeStrokeWidth: 2.6,
117 | activeDotScale: 1.3,
118 | maxVisibleDots: 5,
119 | radius: 8,
120 | spacing: 10,
121 | dotHeight: 12,
122 | dotWidth: 12,
123 | )),
124 | Padding(
125 | padding: const EdgeInsets.only(top: 16, bottom: 16),
126 | child: Text(
127 | 'Customizable Effect',
128 | style: TextStyle(color: Colors.black54),
129 | ),
130 | ),
131 | SmoothPageIndicator(
132 | controller: controller,
133 | count: pages.length,
134 | effect: CustomizableEffect(
135 | activeDotDecoration: DotDecoration(
136 | width: 32,
137 | height: 12,
138 | color: Colors.indigo,
139 | rotationAngle: 180,
140 | verticalOffset: -10,
141 | borderRadius: BorderRadius.circular(24),
142 | // dotBorder: DotBorder(
143 | // padding: 2,
144 | // width: 2,
145 | // color: Colors.indigo,
146 | // ),
147 | ),
148 | dotDecoration: DotDecoration(
149 | width: 24,
150 | height: 12,
151 | color: Colors.grey,
152 | // dotBorder: DotBorder(
153 | // padding: 2,
154 | // width: 2,
155 | // color: Colors.grey,
156 | // ),
157 | // borderRadius: BorderRadius.only(
158 | // topLeft: Radius.circular(2),
159 | // topRight: Radius.circular(16),
160 | // bottomLeft: Radius.circular(16),
161 | // bottomRight: Radius.circular(2)),
162 | borderRadius: BorderRadius.circular(16),
163 | verticalOffset: 0,
164 | ),
165 | spacing: 6.0,
166 | // activeColorOverride: (i) => colors[i],
167 | inActiveColorOverride: (i) => colors[i],
168 | ),
169 | ),
170 | const SizedBox(height: 32.0),
171 | ],
172 | ),
173 | ),
174 | ),
175 | );
176 | }
177 | }
178 |
179 | final colors = const [
180 | Colors.red,
181 | Colors.green,
182 | Colors.greenAccent,
183 | Colors.amberAccent,
184 | Colors.blue,
185 | Colors.amber,
186 | ];
187 |
--------------------------------------------------------------------------------
/test/src/painters/indicator_painter_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_test/flutter_test.dart';
3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart';
4 | import 'package:smooth_page_indicator/src/painters/worm_painter.dart';
5 |
6 | void main() {
7 | group('BasicIndicatorPainter', () {
8 | test('distance calculation is correct', () {
9 | const effect = WormEffect(
10 | dotWidth: 16,
11 | spacing: 8,
12 | );
13 | final painter = WormPainter(
14 | effect: effect,
15 | count: 5,
16 | offset: 0.0,
17 | indicatorColors: DefaultIndicatorColors.defaults,
18 | );
19 |
20 | // distance = dotWidth + spacing = 16 + 8 = 24
21 | expect(painter.distance, 24.0);
22 | });
23 |
24 | test('dotRadius is set correctly', () {
25 | const effect = WormEffect(radius: 8);
26 | final painter = WormPainter(
27 | effect: effect,
28 | count: 5,
29 | offset: 0.0,
30 | indicatorColors: DefaultIndicatorColors.defaults,
31 | );
32 |
33 | expect(painter.dotRadius, const Radius.circular(8));
34 | });
35 |
36 | test('dotPaint is configured correctly with fill style', () {
37 | const effect = WormEffect(
38 | dotColor: Colors.red,
39 | paintStyle: PaintingStyle.fill,
40 | strokeWidth: 2.0,
41 | );
42 | final painter = WormPainter(
43 | effect: effect,
44 | count: 5,
45 | offset: 0.0,
46 | indicatorColors: DefaultIndicatorColors.defaults,
47 | );
48 |
49 | expect(painter.dotPaint.color.toARGB32(), Colors.red.toARGB32());
50 | expect(painter.dotPaint.style, PaintingStyle.fill);
51 | expect(painter.dotPaint.strokeWidth, 2.0);
52 | });
53 |
54 | test('dotPaint is configured correctly with stroke style', () {
55 | const effect = WormEffect(
56 | dotColor: Colors.blue,
57 | paintStyle: PaintingStyle.stroke,
58 | strokeWidth: 3.0,
59 | );
60 | final painter = WormPainter(
61 | effect: effect,
62 | count: 5,
63 | offset: 0.0,
64 | indicatorColors: DefaultIndicatorColors.defaults,
65 | );
66 |
67 | expect(painter.dotPaint.color.toARGB32(), Colors.blue.toARGB32());
68 | expect(painter.dotPaint.style, PaintingStyle.stroke);
69 | expect(painter.dotPaint.strokeWidth, 3.0);
70 | });
71 |
72 | testWidgets('paintStillDots renders correct number of dots',
73 | (tester) async {
74 | const effect = WormEffect();
75 |
76 | await tester.pumpWidget(
77 | MaterialApp(
78 | home: Scaffold(
79 | body: CustomPaint(
80 | size: effect.calculateSize(5),
81 | painter: WormPainter(
82 | effect: effect,
83 | count: 5,
84 | offset: 0.0,
85 | indicatorColors: DefaultIndicatorColors.defaults,
86 | ),
87 | ),
88 | ),
89 | ),
90 | );
91 |
92 | expect(find.byType(CustomPaint), findsWidgets);
93 | });
94 |
95 | testWidgets('buildStillDot creates correct RRect', (tester) async {
96 | const effect = WormEffect(
97 | dotWidth: 16,
98 | dotHeight: 16,
99 | spacing: 8,
100 | radius: 8,
101 | );
102 |
103 | await tester.pumpWidget(
104 | MaterialApp(
105 | home: Scaffold(
106 | body: CustomPaint(
107 | size: effect.calculateSize(5),
108 | painter: WormPainter(
109 | effect: effect,
110 | count: 5,
111 | offset: 0.0,
112 | indicatorColors: DefaultIndicatorColors.defaults,
113 | ),
114 | ),
115 | ),
116 | ),
117 | );
118 |
119 | expect(find.byType(CustomPaint), findsWidgets);
120 | });
121 |
122 | testWidgets('maskStillDots works with underground effects', (tester) async {
123 | const effect = WormEffect(type: WormType.underground);
124 |
125 | await tester.pumpWidget(
126 | MaterialApp(
127 | home: Scaffold(
128 | body: CustomPaint(
129 | size: effect.calculateSize(5),
130 | painter: WormPainter(
131 | effect: effect,
132 | count: 5,
133 | offset: 1.5,
134 | indicatorColors: DefaultIndicatorColors.defaults,
135 | ),
136 | ),
137 | ),
138 | ),
139 | );
140 |
141 | expect(find.byType(CustomPaint), findsWidgets);
142 | });
143 |
144 | testWidgets('calcPortalTravel renders portal travel animation',
145 | (tester) async {
146 | const effect = WormEffect();
147 |
148 | await tester.pumpWidget(
149 | MaterialApp(
150 | home: Scaffold(
151 | body: CustomPaint(
152 | size: effect.calculateSize(5),
153 | painter: WormPainter(
154 | effect: effect,
155 | count: 5,
156 | offset: 4.5, // Triggers portal travel
157 | indicatorColors: DefaultIndicatorColors.defaults,
158 | ),
159 | ),
160 | ),
161 | ),
162 | );
163 |
164 | expect(find.byType(CustomPaint), findsWidgets);
165 | });
166 | });
167 |
168 | group('IndicatorPainter', () {
169 | test('shouldRepaint returns true when offset changes', () {
170 | const effect = WormEffect();
171 | final painter1 = WormPainter(
172 | effect: effect,
173 | count: 5,
174 | offset: 0.0,
175 | indicatorColors: DefaultIndicatorColors.defaults);
176 | final painter2 = WormPainter(
177 | effect: effect,
178 | count: 5,
179 | offset: 1.0,
180 | indicatorColors: DefaultIndicatorColors.defaults);
181 |
182 | expect(painter1.shouldRepaint(painter2), isTrue);
183 | });
184 |
185 | test('shouldRepaint returns false when offset is same', () {
186 | const effect = WormEffect();
187 | final painter1 = WormPainter(
188 | effect: effect,
189 | count: 5,
190 | offset: 0.0,
191 | indicatorColors: DefaultIndicatorColors.defaults);
192 | final painter2 = WormPainter(
193 | effect: effect,
194 | count: 5,
195 | offset: 0.0,
196 | indicatorColors: DefaultIndicatorColors.defaults);
197 |
198 | expect(painter1.shouldRepaint(painter2), isFalse);
199 | });
200 |
201 | test('offset is stored correctly', () {
202 | const effect = WormEffect();
203 | final painter = WormPainter(
204 | effect: effect,
205 | count: 5,
206 | offset: 2.5,
207 | indicatorColors: DefaultIndicatorColors.defaults);
208 |
209 | expect(painter.offset, 2.5);
210 | });
211 | });
212 | }
213 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | alchemist:
5 | dependency: "direct dev"
6 | description:
7 | name: alchemist
8 | sha256: "73e6ea7108897b51af27cf2e27f1a7281d0c7a296c31025ae0c367f09c7236c6"
9 | url: "https://pub.dev"
10 | source: hosted
11 | version: "0.13.0"
12 | async:
13 | dependency: transitive
14 | description:
15 | name: async
16 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
17 | url: "https://pub.dev"
18 | source: hosted
19 | version: "2.12.0"
20 | boolean_selector:
21 | dependency: transitive
22 | description:
23 | name: boolean_selector
24 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
25 | url: "https://pub.dev"
26 | source: hosted
27 | version: "2.1.2"
28 | characters:
29 | dependency: transitive
30 | description:
31 | name: characters
32 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
33 | url: "https://pub.dev"
34 | source: hosted
35 | version: "1.4.0"
36 | clock:
37 | dependency: transitive
38 | description:
39 | name: clock
40 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
41 | url: "https://pub.dev"
42 | source: hosted
43 | version: "1.1.2"
44 | collection:
45 | dependency: transitive
46 | description:
47 | name: collection
48 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
49 | url: "https://pub.dev"
50 | source: hosted
51 | version: "1.19.1"
52 | equatable:
53 | dependency: transitive
54 | description:
55 | name: equatable
56 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
57 | url: "https://pub.dev"
58 | source: hosted
59 | version: "2.0.7"
60 | fake_async:
61 | dependency: transitive
62 | description:
63 | name: fake_async
64 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
65 | url: "https://pub.dev"
66 | source: hosted
67 | version: "1.3.3"
68 | flutter:
69 | dependency: "direct main"
70 | description: flutter
71 | source: sdk
72 | version: "0.0.0"
73 | flutter_lints:
74 | dependency: "direct dev"
75 | description:
76 | name: flutter_lints
77 | sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
78 | url: "https://pub.dev"
79 | source: hosted
80 | version: "6.0.0"
81 | flutter_test:
82 | dependency: "direct dev"
83 | description: flutter
84 | source: sdk
85 | version: "0.0.0"
86 | leak_tracker:
87 | dependency: transitive
88 | description:
89 | name: leak_tracker
90 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
91 | url: "https://pub.dev"
92 | source: hosted
93 | version: "11.0.2"
94 | leak_tracker_flutter_testing:
95 | dependency: transitive
96 | description:
97 | name: leak_tracker_flutter_testing
98 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
99 | url: "https://pub.dev"
100 | source: hosted
101 | version: "3.0.10"
102 | leak_tracker_testing:
103 | dependency: transitive
104 | description:
105 | name: leak_tracker_testing
106 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
107 | url: "https://pub.dev"
108 | source: hosted
109 | version: "3.0.2"
110 | lints:
111 | dependency: transitive
112 | description:
113 | name: lints
114 | sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
115 | url: "https://pub.dev"
116 | source: hosted
117 | version: "6.0.0"
118 | matcher:
119 | dependency: transitive
120 | description:
121 | name: matcher
122 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
123 | url: "https://pub.dev"
124 | source: hosted
125 | version: "0.12.17"
126 | material_color_utilities:
127 | dependency: transitive
128 | description:
129 | name: material_color_utilities
130 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
131 | url: "https://pub.dev"
132 | source: hosted
133 | version: "0.11.1"
134 | meta:
135 | dependency: transitive
136 | description:
137 | name: meta
138 | sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
139 | url: "https://pub.dev"
140 | source: hosted
141 | version: "1.17.0"
142 | path:
143 | dependency: transitive
144 | description:
145 | name: path
146 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
147 | url: "https://pub.dev"
148 | source: hosted
149 | version: "1.9.1"
150 | sky_engine:
151 | dependency: transitive
152 | description: flutter
153 | source: sdk
154 | version: "0.0.0"
155 | source_span:
156 | dependency: transitive
157 | description:
158 | name: source_span
159 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
160 | url: "https://pub.dev"
161 | source: hosted
162 | version: "1.10.1"
163 | stack_trace:
164 | dependency: transitive
165 | description:
166 | name: stack_trace
167 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
168 | url: "https://pub.dev"
169 | source: hosted
170 | version: "1.12.1"
171 | stream_channel:
172 | dependency: transitive
173 | description:
174 | name: stream_channel
175 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
176 | url: "https://pub.dev"
177 | source: hosted
178 | version: "2.1.4"
179 | string_scanner:
180 | dependency: transitive
181 | description:
182 | name: string_scanner
183 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
184 | url: "https://pub.dev"
185 | source: hosted
186 | version: "1.4.1"
187 | term_glyph:
188 | dependency: transitive
189 | description:
190 | name: term_glyph
191 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
192 | url: "https://pub.dev"
193 | source: hosted
194 | version: "1.2.2"
195 | test_api:
196 | dependency: transitive
197 | description:
198 | name: test_api
199 | sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
200 | url: "https://pub.dev"
201 | source: hosted
202 | version: "0.7.7"
203 | vector_math:
204 | dependency: transitive
205 | description:
206 | name: vector_math
207 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
208 | url: "https://pub.dev"
209 | source: hosted
210 | version: "2.2.0"
211 | vm_service:
212 | dependency: transitive
213 | description:
214 | name: vm_service
215 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
216 | url: "https://pub.dev"
217 | source: hosted
218 | version: "14.3.1"
219 | sdks:
220 | dart: ">=3.8.0 <4.0.0"
221 | flutter: ">=3.32.0"
222 |
--------------------------------------------------------------------------------
/lib/src/effects/customizable_effect.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 | import 'dart:ui' as ui show lerpDouble;
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:smooth_page_indicator/src/painters/customizable_painter.dart';
6 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart';
7 | import 'package:smooth_page_indicator/src/theme_defaults.dart';
8 |
9 | import 'indicator_effect.dart';
10 |
11 | /// Signature for a function that returns color
12 | /// for each [index]
13 | typedef ColorBuilder = Color Function(int index);
14 |
15 | /// Holds painting configuration to be used by [CustomizablePainter]
16 | class CustomizableEffect extends IndicatorEffect {
17 | /// Holds painting decoration for inactive dots
18 | final DotDecoration dotDecoration;
19 |
20 | /// Holds painting decoration for active dots
21 | final DotDecoration activeDotDecoration;
22 |
23 | /// Builds dynamic colors for active dot
24 | final ColorBuilder? activeColorOverride;
25 |
26 | /// Builds dynamic colors for inactive dots
27 | final ColorBuilder? inActiveColorOverride;
28 |
29 | /// The space between two dots
30 | final double spacing;
31 |
32 | /// Default constructor
33 | const CustomizableEffect({
34 | required this.dotDecoration,
35 | required this.activeDotDecoration,
36 | this.activeColorOverride,
37 | this.spacing = 8,
38 | this.inActiveColorOverride,
39 | });
40 |
41 | @override
42 | Size calculateSize(int count) {
43 | final activeDotWidth =
44 | activeDotDecoration.width + activeDotDecoration.dotBorder.neededSpace;
45 | final dotWidth = dotDecoration.width + dotDecoration.dotBorder.neededSpace;
46 |
47 | final maxWidth =
48 | dotWidth * (count - 1) + (spacing * count) + activeDotWidth;
49 |
50 | final offsetSpace =
51 | (dotDecoration.verticalOffset - activeDotDecoration.verticalOffset)
52 | .abs();
53 | final maxHeight = max(
54 | dotDecoration.height + offsetSpace + dotDecoration.dotBorder.neededSpace,
55 | activeDotDecoration.height +
56 | offsetSpace +
57 | activeDotDecoration.dotBorder.neededSpace,
58 | );
59 | return Size(maxWidth, maxHeight);
60 | }
61 |
62 | @override
63 | IndicatorPainter buildPainter(
64 | int count, double offset, DefaultIndicatorColors indicatorColors) {
65 | return CustomizablePainter(count: count, offset: offset, effect: this);
66 | }
67 |
68 | @override
69 | int hitTestDots(double dx, int count, double current) {
70 | var anchor = -spacing / 2;
71 | for (var index = 0; index < count; index++) {
72 | var dotWidth = dotDecoration.width + dotDecoration.dotBorder.neededSpace;
73 | if (index == current) {
74 | dotWidth = activeDotDecoration.width +
75 | activeDotDecoration.dotBorder.neededSpace;
76 | }
77 |
78 | var widthBound = dotWidth + spacing;
79 | if (dx <= (anchor += widthBound)) {
80 | return index;
81 | }
82 | }
83 | return -1;
84 | }
85 |
86 | @override
87 | CustomizableEffect lerp(covariant CustomizableEffect? other, double t) {
88 | if (other == null) return this;
89 | return CustomizableEffect(
90 | dotDecoration: DotDecoration.lerp(dotDecoration, other.dotDecoration, t),
91 | activeDotDecoration:
92 | DotDecoration.lerp(activeDotDecoration, other.activeDotDecoration, t),
93 | activeColorOverride:
94 | t < 0.5 ? activeColorOverride : other.activeColorOverride,
95 | inActiveColorOverride:
96 | t < 0.5 ? inActiveColorOverride : other.inActiveColorOverride,
97 | spacing: ui.lerpDouble(spacing, other.spacing, t)!,
98 | );
99 | }
100 | }
101 |
102 | /// Holds dot painting specs
103 | class DotDecoration {
104 | /// The border radius of the dot
105 | final BorderRadius borderRadius;
106 |
107 | /// The color of the dot
108 | final Color color;
109 |
110 | /// The dotBorder configuration of the dot
111 | final DotBorder dotBorder;
112 |
113 | /// The vertical offset of the dot
114 | final double verticalOffset;
115 |
116 | /// The rotation angle of the dot
117 | final double rotationAngle;
118 |
119 | /// The width of the dot
120 | final double width;
121 |
122 | /// the height of the dot
123 | final double height;
124 |
125 | /// Default constructor
126 | const DotDecoration(
127 | {this.borderRadius = BorderRadius.zero,
128 | this.color = Colors.white,
129 | this.dotBorder = DotBorder.none,
130 | this.verticalOffset = 0.0,
131 | this.rotationAngle = 0.0,
132 | this.width = 8,
133 | this.height = 8});
134 |
135 | /// Lerps the value between active dot and prev-active dot
136 | static DotDecoration lerp(DotDecoration a, DotDecoration b, double t) {
137 | return DotDecoration(
138 | borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t)!,
139 | width: ui.lerpDouble(a.width, b.width, t) ?? 0.0,
140 | height: ui.lerpDouble(a.height, b.height, t) ?? 0.0,
141 | color: Color.lerp(a.color, b.color, t)!,
142 | dotBorder: DotBorder.lerp(a.dotBorder, b.dotBorder, t),
143 | verticalOffset:
144 | ui.lerpDouble(a.verticalOffset, b.verticalOffset, t) ?? 0.0,
145 | rotationAngle:
146 | ui.lerpDouble(a.rotationAngle, b.rotationAngle, t) ?? 0.0);
147 | }
148 |
149 | /// Builds a new instance with the given
150 | /// override values
151 | DotDecoration copyWith({
152 | BorderRadius? borderRadius,
153 | double? width,
154 | double? height,
155 | Color? color,
156 | DotBorder? dotBorder,
157 | double? verticalOffset,
158 | double? rotationAngle,
159 | }) {
160 | return DotDecoration(
161 | borderRadius: borderRadius ?? this.borderRadius,
162 | width: width ?? this.width,
163 | height: height ?? this.height,
164 | color: color ?? this.color,
165 | dotBorder: dotBorder ?? this.dotBorder,
166 | verticalOffset: verticalOffset ?? this.verticalOffset,
167 | rotationAngle: rotationAngle ?? this.rotationAngle,
168 | );
169 | }
170 | }
171 |
172 | /// The variants of dot borders
173 | enum DotBorderType {
174 | /// Draw a sold border
175 | solid,
176 |
177 | /// Draw nothing
178 | none
179 | }
180 |
181 | /// Holds dot-border painting specs
182 | class DotBorder {
183 | /// The thinness of the border line
184 | final double width;
185 |
186 | /// The color of the border
187 | final Color color;
188 |
189 | /// The padding between the dot and the border
190 | final double padding;
191 |
192 | /// The border variant
193 | final DotBorderType type;
194 |
195 | /// Default constructor
196 | const DotBorder({
197 | this.width = 1.0,
198 | this.color = Colors.black87,
199 | this.padding = 0.0,
200 | this.type = DotBorderType.solid,
201 | });
202 |
203 | /// Calculates the needed gap based on [type]
204 | double get neededSpace =>
205 | type == DotBorderType.none ? 0.0 : (width / 2 + (padding * 2));
206 |
207 | /// Builds an instance with type [DotBorderType.none]
208 | static const none = DotBorder._none();
209 |
210 | const DotBorder._none()
211 | : width = 0.0,
212 | color = Colors.transparent,
213 | padding = 0.0,
214 | type = DotBorderType.none;
215 |
216 | /// Lerps the value between active dot border and prev-active dot's border
217 | static DotBorder lerp(DotBorder a, DotBorder b, double t) {
218 | if (t == 0.0) {
219 | return a;
220 | }
221 | if (t == 1.0) {
222 | return b;
223 | }
224 | return DotBorder(
225 | color: Color.lerp(a.color, b.color, t)!,
226 | width: ui.lerpDouble(a.width, b.width, t)!,
227 | padding: ui.lerpDouble(a.padding, b.padding, t)!);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # smooth_page_indicator
3 |
4 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Introduction
16 | Page indicators are a crucial part of any app that involves multiple pages. They help users to
17 | understand the number of pages and their current position. `SmoothPageIndicator` is a Flutter
18 | package that provides a set of animated page indicators with a variety of effects.
19 |
20 | 
21 |
22 |
23 | ## Effects
24 | `SmoothPageIndicator` comes with a set of built-in effects that you can use to animate the active dot,
25 | you can also customize each effect to your liking.
26 |
27 | for more specific customization, try the `CustomizableEffect` which allows for more customization.
28 |
29 | | Effect | Preview |
30 | |:------------------------------------------|:------------------------------------------------------------------------------------------------------------------------:|
31 | | Worm |  |
32 | | Worm style = WormStyle.thin [v1.0.0] |  |
33 | | Expanding Dots |  |
34 | | Jumping dot |  |
35 | | Jumping dot with vertical offset [v1.0.0] |  |
36 | | Scrolling Dots |  |
37 | | Slide |  |
38 | | Scale |  |
39 | | Swap |  |
40 | | Swap type = SwapType.yRotation [v1.0.0] |  |
41 | | Color Transition [0.1.2] |  |
42 | | Customizable demo-1 [v1.0.0] |  |
43 | | Customizable demo-2 [v1.0.0] |  |
44 | | Customizable demo-3 [v1.0.0] |  |
45 | | Customizable demo-4 [v1.0.0] |  |
46 |
47 | ## Usage
48 | `SmoothPageIndicator` uses the PageController's scroll offset to animate the active dot.
49 |
50 | ```dart
51 | SmoothPageIndicator(
52 | controller: controller, // PageController
53 | count: 6,
54 | effect: WormEffect(), // your preferred effect
55 | onDotClicked: (index){
56 | }
57 | )
58 |
59 | ```
60 | ## Usage without a PageController
61 | Unlike `SmoothPageIndicator`, `AnimatedSmoothIndicator` is self animated and all it needs is the
62 | active index.
63 |
64 | ```dart
65 | AnimatedSmoothIndicator(
66 | activeIndex: yourActiveIndex,
67 | count: 6,
68 | effect: WormEffect(),
69 | )
70 | ```
71 | ## Vertical layout support
72 | Smooth page indicator supports both horizontal and vertical layouts.
73 |
74 | ```dart
75 | SmoothPageIndicator(
76 | controller: controller, // PageController
77 | count: 6,
78 | axisDirection: Axis.vertical,
79 | effect: WormEffect(),
80 | )
81 | ```
82 | 
83 |
84 | ## Scrolling dots effect
85 | Smooth page indicator comes with a shipped it scrolling dots effect, (similar to the one used in instagram), it's useful when you have a large number of pages.
86 |
87 | 
88 |
89 | ## Customization
90 | Each effect comes with its own set of properties that you can customize to your liking.
91 | for example, you can customize direction, width, height, radius, spacing, paint style, color and more... of `SlideEffect` like follows:
92 |
93 | ```dart
94 | SmoothPageIndicator(
95 | controller: controller,
96 | count: 6,
97 | axisDirection: Axis.vertical,
98 | effect: SlideEffect(
99 | spacing: 8.0,
100 | radius: 4.0,
101 | dotWidth: 24.0,
102 | dotHeight: 16.0,
103 | paintStyle: PaintingStyle.stroke,
104 | strokeWidth: 1.5,
105 | dotColor: Colors.grey,
106 | activeDotColor: Colors.indigo
107 | ),
108 | )
109 |
110 | ```
111 |
112 | ## Theme-based Colors
113 | By default, `SmoothPageIndicator` derives its colors from the app theme:
114 | - `activeDotColor` defaults to `Theme.of(context).primaryColor`
115 | - `dotColor` defaults to `Theme.of(context).unselectedWidgetColor` with reduced opacity
116 |
117 | This means the indicator automatically adapts to your app's color scheme. You can override these by explicitly setting `dotColor` and `activeDotColor` in any effect.
118 |
119 | ## SmoothPageIndicatorTheme
120 | You can configure default settings for all `SmoothPageIndicator` and `AnimatedSmoothIndicator` widgets app-wide using `SmoothPageIndicatorTheme`.
121 |
122 | ```dart
123 | MaterialApp(
124 | theme: ThemeData.light().copyWith(
125 | extensions: [
126 | SmoothPageIndicatorTheme(
127 | effect: ExpandingDotsEffect(), // default effect when none is specified
128 | defaultColors: DefaultIndicatorColors(
129 | active: Colors.blue,
130 | inactive: Colors.grey,
131 | ),
132 | ),
133 | ],
134 | ),
135 | )
136 | ```
137 |
138 | - `defaultColors`: Applies to **all** indicator effects across the app (unless overridden by the effect itself)
139 | - `effect`: The default effect used when no effect is specified in the widget. Colors set within this effect (e.g., `activeDotColor`) only apply to this default effect.
140 |
141 | This is useful when you want consistent indicator styling across your entire app without repeating the same configuration.
142 |
143 | ## Support the Library
144 |
145 | You can support the library by liking it on pub, staring in on Github and reporting any bugs you
146 | encounter.
--------------------------------------------------------------------------------