Skip to content

Commit ae78af4

Browse files
committed
feat: redesign CustomMaterialIndicator
1 parent ac97c84 commit ae78af4

File tree

1 file changed

+175
-48
lines changed

1 file changed

+175
-48
lines changed

lib/src/widgets/custom_material_indicator.dart

Lines changed: 175 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
2+
import 'package:custom_refresh_indicator/src/custom_refresh_indicator.dart';
23
import 'package:flutter/foundation.dart';
34
import '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

181308
class _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

Comments
 (0)