11import 'package:custom_refresh_indicator/custom_refresh_indicator.dart' ;
2+ import 'package:custom_refresh_indicator/src/custom_refresh_indicator.dart' ;
23import 'package:flutter/foundation.dart' ;
34import 'package:flutter/material.dart' ;
45
@@ -11,7 +12,7 @@ typedef MaterialIndicatorBuilder = Widget Function(
1112 IndicatorController controller,
1213);
1314
14- class CustomMaterialIndicator extends StatelessWidget {
15+ class CustomMaterialIndicator extends StatefulWidget {
1516 /// {@macro custom_refresh_indicator.child}
1617 final Widget child;
1718
@@ -53,18 +54,14 @@ class CustomMaterialIndicator extends StatelessWidget {
5354 final double elevation;
5455
5556 /// Builds the content for the indicator container
56- final MaterialIndicatorBuilder indicatorBuilder;
57+ final MaterialIndicatorBuilder ? indicatorBuilder;
5758
5859 /// A builder that constructs a scrollable widget, typically used for a list.
5960 ///
6061 /// This builder is responsible for building the scrollable widget ([child] )
6162 /// that can be animated during loading or other state changes.
6263 final IndicatorBuilder scrollableBuilder;
6364
64- /// When set to *true*, the indicator will rotate
65- /// in the [IndicatorState.loading] state.
66- final bool withRotation;
67-
6865 /// {@macro custom_refresh_indicator.notification_predicate}
6966 final ScrollNotificationPredicate notificationPredicate;
7067
@@ -96,17 +93,34 @@ class CustomMaterialIndicator extends StatelessWidget {
9693 /// Whether to display trailing scroll indicator
9794 final bool trailingScrollIndicatorVisible;
9895
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+
99114 const CustomMaterialIndicator ({
100115 super .key,
101116 required this .child,
102117 required this .onRefresh,
103- required this .indicatorBuilder,
118+ this .indicatorBuilder,
104119 this .scrollableBuilder = _defaultBuilder,
105120 this .notificationPredicate = CustomRefreshIndicator .defaultScrollNotificationPredicate,
106121 this .backgroundColor,
107122 this .displacement = 40.0 ,
108123 this .edgeOffset = 0.0 ,
109- this .withRotation = true ,
110124 this .elevation = 2.0 ,
111125 this .clipBehavior = Clip .none,
112126 this .autoRebuild = true ,
@@ -117,65 +131,178 @@ class CustomMaterialIndicator extends StatelessWidget {
117131 this .onStateChanged,
118132 this .leadingScrollIndicatorVisible = false ,
119133 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;
121144
122145 static Widget _defaultBuilder (BuildContext context, Widget child, IndicatorController controller) => child;
123146
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+
124243 @override
125244 Widget build (BuildContext context) {
245+ final indicatorBuilder = widget.indicatorBuilder ?? _defaultIndicatorBuilder;
246+
126247 return CustomRefreshIndicator (
127248 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,
132253 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,
137258 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+ }
141281
142282 return Stack (
143283 children: < Widget > [
144- scrollableBuilder (context, child, controller),
284+ widget. scrollableBuilder (context, child, controller),
145285 _PositionedIndicatorContainer (
146- edgeOffset: edgeOffset,
147- displacement: displacement,
286+ edgeOffset: widget. edgeOffset,
287+ displacement: widget. displacement,
148288 controller: controller,
149289 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,
171292 ),
172293 ),
173294 ],
174295 );
175296 },
176- child: child,
297+ child: widget. child,
177298 );
178299 }
300+
301+ @override
302+ void dispose () {
303+ _internalIndicatorController? .dispose ();
304+ super .dispose ();
305+ }
179306}
180307
181308class _PositionedIndicatorContainer extends StatelessWidget {
@@ -193,7 +320,7 @@ class _PositionedIndicatorContainer extends StatelessWidget {
193320 required this .edgeOffset,
194321 });
195322
196- Alignment _getAlignement (IndicatorSide side) {
323+ Alignment _getAlignment (IndicatorSide side) {
197324 switch (side) {
198325 case IndicatorSide .left:
199326 return Alignment .centerLeft;
@@ -204,7 +331,7 @@ class _PositionedIndicatorContainer extends StatelessWidget {
204331 case IndicatorSide .bottom:
205332 return Alignment .bottomCenter;
206333 case IndicatorSide .none:
207- throw UnsupportedError ('Cannot get alignement for "none" side.' );
334+ throw UnsupportedError ('Cannot get alignment for "none" side.' );
208335 }
209336 }
210337
@@ -271,7 +398,7 @@ class _PositionedIndicatorContainer extends StatelessWidget {
271398 child: Padding (
272399 padding: _getEdgeInsets (side),
273400 child: Align (
274- alignment: _getAlignement (side),
401+ alignment: _getAlignment (side),
275402 child: child,
276403 ),
277404 ),
0 commit comments