1
1
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart' ;
2
+ import 'package:custom_refresh_indicator/src/custom_refresh_indicator.dart' ;
2
3
import 'package:flutter/foundation.dart' ;
3
4
import 'package:flutter/material.dart' ;
4
5
@@ -11,7 +12,7 @@ typedef MaterialIndicatorBuilder = Widget Function(
11
12
IndicatorController controller,
12
13
);
13
14
14
- class CustomMaterialIndicator extends StatelessWidget {
15
+ class CustomMaterialIndicator extends StatefulWidget {
15
16
/// {@macro custom_refresh_indicator.child}
16
17
final Widget child;
17
18
@@ -53,18 +54,14 @@ class CustomMaterialIndicator extends StatelessWidget {
53
54
final double elevation;
54
55
55
56
/// Builds the content for the indicator container
56
- final MaterialIndicatorBuilder indicatorBuilder;
57
+ final MaterialIndicatorBuilder ? indicatorBuilder;
57
58
58
59
/// A builder that constructs a scrollable widget, typically used for a list.
59
60
///
60
61
/// This builder is responsible for building the scrollable widget ([child] )
61
62
/// that can be animated during loading or other state changes.
62
63
final IndicatorBuilder scrollableBuilder;
63
64
64
- /// When set to *true*, the indicator will rotate
65
- /// in the [IndicatorState.loading] state.
66
- final bool withRotation;
67
-
68
65
/// {@macro custom_refresh_indicator.notification_predicate}
69
66
final ScrollNotificationPredicate notificationPredicate;
70
67
@@ -96,17 +93,34 @@ class CustomMaterialIndicator extends StatelessWidget {
96
93
/// Whether to display trailing scroll indicator
97
94
final bool trailingScrollIndicatorVisible;
98
95
96
+ /// Defines [strokeWidth] for `RefreshIndicator` .
97
+ ///
98
+ /// By default, the value of [strokeWidth] is 2.5 pixels.
99
+ final double strokeWidth;
100
+
101
+ /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel}
102
+ ///
103
+ /// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]
104
+ /// if it is null.
105
+ final String ? semanticsLabel;
106
+
107
+ /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue}
108
+ final String ? semanticsValue;
109
+
110
+ /// The progress indicator's foreground color. The current theme's
111
+ /// [ColorScheme.primary] by default.
112
+ final Color ? color;
113
+
99
114
const CustomMaterialIndicator ({
100
115
super .key,
101
116
required this .child,
102
117
required this .onRefresh,
103
- required this .indicatorBuilder,
118
+ this .indicatorBuilder,
104
119
this .scrollableBuilder = _defaultBuilder,
105
120
this .notificationPredicate = CustomRefreshIndicator .defaultScrollNotificationPredicate,
106
121
this .backgroundColor,
107
122
this .displacement = 40.0 ,
108
123
this .edgeOffset = 0.0 ,
109
- this .withRotation = true ,
110
124
this .elevation = 2.0 ,
111
125
this .clipBehavior = Clip .none,
112
126
this .autoRebuild = true ,
@@ -117,65 +131,178 @@ class CustomMaterialIndicator extends StatelessWidget {
117
131
this .onStateChanged,
118
132
this .leadingScrollIndicatorVisible = false ,
119
133
this .trailingScrollIndicatorVisible = true ,
120
- });
134
+ double ? strokeWidth,
135
+ this .semanticsLabel,
136
+ this .semanticsValue,
137
+ this .color,
138
+ }) : assert (
139
+ indicatorBuilder == null ||
140
+ (color == null && semanticsValue == null && semanticsLabel == null && strokeWidth == null ),
141
+ 'When a custom indicatorBuilder is provided, the parameters color, semanticsValue, semanticsLabel and strokeWidth are unused and can be safely removed.' ,
142
+ ),
143
+ strokeWidth = strokeWidth ?? RefreshProgressIndicator .defaultStrokeWidth;
121
144
122
145
static Widget _defaultBuilder (BuildContext context, Widget child, IndicatorController controller) => child;
123
146
147
+ @override
148
+ State <CustomMaterialIndicator > createState () => _CustomMaterialIndicatorState ();
149
+ }
150
+
151
+ class _CustomMaterialIndicatorState extends State <CustomMaterialIndicator > {
152
+ IndicatorController ? _internalIndicatorController;
153
+ IndicatorController get controller => widget.controller ?? (_internalIndicatorController ?? = IndicatorController ());
154
+
155
+ @override
156
+ void didUpdateWidget (covariant CustomMaterialIndicator oldWidget) {
157
+ super .didUpdateWidget (oldWidget);
158
+
159
+ // When a new background color is provided.
160
+ if (oldWidget.backgroundColor != widget.backgroundColor) {
161
+ _backgroundColor = _getBackgroundColor ();
162
+ }
163
+
164
+ // When a new controller is provided externally.
165
+ if (oldWidget.controller != widget.controller) {
166
+ if (widget.controller != null ) {
167
+ // Dispose and remove the current internal controller, if it exists
168
+ _internalIndicatorController? .dispose ();
169
+ _internalIndicatorController = null ;
170
+ }
171
+
172
+ // Update animations/listeners.
173
+ _setupMaterialIndicator ();
174
+ } else if (oldWidget.color != widget.color) {
175
+ // Update color animation.
176
+ _setupMaterialIndicator ();
177
+ }
178
+
179
+ assert (
180
+ widget.controller == null || (widget.controller != null && _internalIndicatorController == null ),
181
+ 'An internal indicator should not exist when an external indicator is provided.' ,
182
+ );
183
+ }
184
+
185
+ Widget _defaultIndicatorBuilder (BuildContext context, IndicatorController controller) {
186
+ final bool showIndeterminateIndicator = controller.isLoading || controller.isComplete || controller.isFinalizing;
187
+
188
+ return RefreshProgressIndicator (
189
+ semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations .of (context).refreshIndicatorSemanticLabel,
190
+ semanticsValue: widget.semanticsValue,
191
+ value: showIndeterminateIndicator ? null : _valueAnimation.value,
192
+ valueColor: _colorAnimation,
193
+ backgroundColor: _backgroundColor,
194
+ strokeWidth: widget.strokeWidth,
195
+ );
196
+ }
197
+
198
+ late Animation <double > _valueAnimation;
199
+ late Animation <Color ?> _colorAnimation;
200
+ late Color _indicatorColor;
201
+ late Color _backgroundColor;
202
+
203
+ @override
204
+ void didChangeDependencies () {
205
+ _setupMaterialIndicator ();
206
+ super .didChangeDependencies ();
207
+ }
208
+
209
+ Color _getBackgroundColor () {
210
+ return widget.backgroundColor ??
211
+ ProgressIndicatorTheme .of (context).refreshBackgroundColor ??
212
+ Theme .of (context).canvasColor;
213
+ }
214
+
215
+ Color _getIndicatorColor () {
216
+ return widget.color ?? Theme .of (context).colorScheme.primary;
217
+ }
218
+
219
+ void _setupMaterialIndicator () {
220
+ _valueAnimation = controller.normalize ();
221
+ // Reset the current color.
222
+ _backgroundColor = _getBackgroundColor ();
223
+ _indicatorColor = _getIndicatorColor ();
224
+ final Color color = _indicatorColor;
225
+ if (color.alpha == 0x00 ) {
226
+ // Set an always stopped animation instead of a driven tween.
227
+ _colorAnimation = AlwaysStoppedAnimation <Color >(color);
228
+ } else {
229
+ // Respect the alpha of the given color.
230
+ _colorAnimation = _valueAnimation.drive (
231
+ ColorTween (
232
+ begin: color.withAlpha (0 ),
233
+ end: color.withAlpha (color.alpha),
234
+ ).chain (
235
+ CurveTween (
236
+ curve: const Interval (0.0 , 1.0 / 1.5 ),
237
+ ),
238
+ ),
239
+ );
240
+ }
241
+ }
242
+
124
243
@override
125
244
Widget build (BuildContext context) {
245
+ final indicatorBuilder = widget.indicatorBuilder ?? _defaultIndicatorBuilder;
246
+
126
247
return CustomRefreshIndicator (
127
248
autoRebuild: false ,
128
- notificationPredicate: notificationPredicate,
129
- onRefresh: onRefresh,
130
- trigger: trigger,
131
- triggerMode: triggerMode,
249
+ notificationPredicate: widget. notificationPredicate,
250
+ onRefresh: widget. onRefresh,
251
+ trigger: widget. trigger,
252
+ triggerMode: widget. triggerMode,
132
253
controller: controller,
133
- durations: durations,
134
- onStateChanged: onStateChanged,
135
- trailingScrollIndicatorVisible: trailingScrollIndicatorVisible,
136
- leadingScrollIndicatorVisible: leadingScrollIndicatorVisible,
254
+ durations: widget. durations,
255
+ onStateChanged: widget. onStateChanged,
256
+ trailingScrollIndicatorVisible: widget. trailingScrollIndicatorVisible,
257
+ leadingScrollIndicatorVisible: widget. leadingScrollIndicatorVisible,
137
258
builder: (context, child, controller) {
138
- final Color backgroundColor = this .backgroundColor ??
139
- ProgressIndicatorTheme .of (context).refreshBackgroundColor ??
140
- Theme .of (context).canvasColor;
259
+ Widget indicator = widget.autoRebuild
260
+ ? AnimatedBuilder (
261
+ animation: controller,
262
+ builder: (context, _) => indicatorBuilder (context, controller),
263
+ )
264
+ : indicatorBuilder (context, controller);
265
+
266
+ /// If indicatorBuilder is not provided
267
+ if (widget.indicatorBuilder != null ) {
268
+ indicator = Container (
269
+ width: 41 ,
270
+ height: 41 ,
271
+ margin: const EdgeInsets .all (4.0 ),
272
+ child: Material (
273
+ type: MaterialType .circle,
274
+ clipBehavior: widget.clipBehavior,
275
+ color: _backgroundColor,
276
+ elevation: widget.elevation,
277
+ child: indicator,
278
+ ),
279
+ );
280
+ }
141
281
142
282
return Stack (
143
283
children: < Widget > [
144
- scrollableBuilder (context, child, controller),
284
+ widget. scrollableBuilder (context, child, controller),
145
285
_PositionedIndicatorContainer (
146
- edgeOffset: edgeOffset,
147
- displacement: displacement,
286
+ edgeOffset: widget. edgeOffset,
287
+ displacement: widget. displacement,
148
288
controller: controller,
149
289
child: ScaleTransition (
150
- scale: controller.isFinalizing ? controller.clamp (0.0 , 1.0 ) : const AlwaysStoppedAnimation (1.0 ),
151
- child: Container (
152
- width: 41 ,
153
- height: 41 ,
154
- margin: const EdgeInsets .all (4.0 ),
155
- child: Material (
156
- type: MaterialType .circle,
157
- clipBehavior: clipBehavior,
158
- color: backgroundColor,
159
- elevation: elevation,
160
- child: _InfiniteRotation (
161
- running: withRotation && controller.isLoading,
162
- child: autoRebuild
163
- ? AnimatedBuilder (
164
- animation: controller,
165
- builder: (context, _) => indicatorBuilder (context, controller),
166
- )
167
- : indicatorBuilder (context, controller),
168
- ),
169
- ),
170
- ),
290
+ scale: controller.isFinalizing ? _valueAnimation : const AlwaysStoppedAnimation (1.0 ),
291
+ child: indicator,
171
292
),
172
293
),
173
294
],
174
295
);
175
296
},
176
- child: child,
297
+ child: widget. child,
177
298
);
178
299
}
300
+
301
+ @override
302
+ void dispose () {
303
+ _internalIndicatorController? .dispose ();
304
+ super .dispose ();
305
+ }
179
306
}
180
307
181
308
class _PositionedIndicatorContainer extends StatelessWidget {
@@ -193,7 +320,7 @@ class _PositionedIndicatorContainer extends StatelessWidget {
193
320
required this .edgeOffset,
194
321
});
195
322
196
- Alignment _getAlignement (IndicatorSide side) {
323
+ Alignment _getAlignment (IndicatorSide side) {
197
324
switch (side) {
198
325
case IndicatorSide .left:
199
326
return Alignment .centerLeft;
@@ -204,7 +331,7 @@ class _PositionedIndicatorContainer extends StatelessWidget {
204
331
case IndicatorSide .bottom:
205
332
return Alignment .bottomCenter;
206
333
case IndicatorSide .none:
207
- throw UnsupportedError ('Cannot get alignement for "none" side.' );
334
+ throw UnsupportedError ('Cannot get alignment for "none" side.' );
208
335
}
209
336
}
210
337
@@ -271,7 +398,7 @@ class _PositionedIndicatorContainer extends StatelessWidget {
271
398
child: Padding (
272
399
padding: _getEdgeInsets (side),
273
400
child: Align (
274
- alignment: _getAlignement (side),
401
+ alignment: _getAlignment (side),
275
402
child: child,
276
403
),
277
404
),
0 commit comments