Skip to content

Commit cb67ecd

Browse files
Add adaptive RefreshIndicator (#121249)
1 parent 9e6214f commit cb67ecd

File tree

2 files changed

+104
-3
lines changed

2 files changed

+104
-3
lines changed

packages/flutter/lib/src/material/refresh_indicator.dart

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import 'dart:async';
66
import 'dart:math' as math;
77

8+
import 'package:flutter/cupertino.dart';
89
import 'package:flutter/foundation.dart' show clampDouble;
9-
import 'package:flutter/widgets.dart';
1010

1111
import 'debug.dart';
1212
import 'material_localizations.dart';
@@ -59,6 +59,8 @@ enum RefreshIndicatorTriggerMode {
5959
onEdge,
6060
}
6161

62+
enum _IndicatorType { material, adaptive }
63+
6264
/// A widget that supports the Material "swipe to refresh" idiom.
6365
///
6466
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
@@ -138,7 +140,38 @@ class RefreshIndicator extends StatefulWidget {
138140
this.semanticsValue,
139141
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
140142
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
141-
});
143+
}) : _indicatorType = _IndicatorType.material;
144+
145+
/// Creates an adaptive [RefreshIndicator] based on whether the target
146+
/// platform is iOS or macOS, following Material design's
147+
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
148+
///
149+
/// When the descendant overscrolls, a different spinning progress indicator
150+
/// is shown depending on platform. On iOS and macOS,
151+
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
152+
/// [CircularProgressIndicator] appears.
153+
///
154+
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
155+
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
156+
///
157+
/// The target platform is based on the current [Theme]: [ThemeData.platform].
158+
///
159+
/// Noteably the scrollable widget itself will have slightly different behavior
160+
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
161+
const RefreshIndicator.adaptive({
162+
super.key,
163+
required this.child,
164+
this.displacement = 40.0,
165+
this.edgeOffset = 0.0,
166+
required this.onRefresh,
167+
this.color,
168+
this.backgroundColor,
169+
this.notificationPredicate = defaultScrollNotificationPredicate,
170+
this.semanticsLabel,
171+
this.semanticsValue,
172+
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
173+
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
174+
}) : _indicatorType = _IndicatorType.adaptive;
142175

143176
/// The widget below this widget in the tree.
144177
///
@@ -207,6 +240,8 @@ class RefreshIndicator extends StatefulWidget {
207240
/// By default, the value of [strokeWidth] is 2.0 pixels.
208241
final double strokeWidth;
209242

243+
final _IndicatorType _indicatorType;
244+
210245
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
211246
///
212247
/// The [RefreshIndicator] can be pulled out in two cases,
@@ -555,14 +590,37 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
555590
child: AnimatedBuilder(
556591
animation: _positionController,
557592
builder: (BuildContext context, Widget? child) {
558-
return RefreshProgressIndicator(
593+
final Widget materialIndicator = RefreshProgressIndicator(
559594
semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
560595
semanticsValue: widget.semanticsValue,
561596
value: showIndeterminateIndicator ? null : _value.value,
562597
valueColor: _valueColor,
563598
backgroundColor: widget.backgroundColor,
564599
strokeWidth: widget.strokeWidth,
565600
);
601+
602+
final Widget cupertinoIndicator = CupertinoActivityIndicator(
603+
color: widget.color,
604+
);
605+
606+
switch(widget._indicatorType) {
607+
case _IndicatorType.material:
608+
return materialIndicator;
609+
610+
case _IndicatorType.adaptive: {
611+
final ThemeData theme = Theme.of(context);
612+
switch (theme.platform) {
613+
case TargetPlatform.android:
614+
case TargetPlatform.fuchsia:
615+
case TargetPlatform.linux:
616+
case TargetPlatform.windows:
617+
return materialIndicator;
618+
case TargetPlatform.iOS:
619+
case TargetPlatform.macOS:
620+
return cupertinoIndicator;
621+
}
622+
}
623+
}
566624
},
567625
),
568626
),

packages/flutter/test/material/refresh_indicator_test.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:flutter/cupertino.dart';
78
import 'package:flutter/foundation.dart';
89
import 'package:flutter/material.dart';
910
import 'package:flutter_test/flutter_test.dart';
@@ -792,6 +793,48 @@ void main() {
792793
expect(refreshCalled, false);
793794
});
794795

796+
testWidgets('RefreshIndicator.adaptive', (WidgetTester tester) async {
797+
Widget buildFrame(TargetPlatform platform) {
798+
return MaterialApp(
799+
theme: ThemeData(platform: platform),
800+
home: RefreshIndicator.adaptive(
801+
onRefresh: refresh,
802+
child: ListView(
803+
physics: const AlwaysScrollableScrollPhysics(),
804+
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
805+
return SizedBox(
806+
height: 200.0,
807+
child: Text(item),
808+
);
809+
}).toList(),
810+
),
811+
),
812+
);
813+
}
814+
815+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
816+
await tester.pumpWidget(buildFrame(platform));
817+
await tester.pumpAndSettle(); // Finish the theme change animation.
818+
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
819+
await tester.pump();
820+
821+
expect(find.byType(CupertinoActivityIndicator), findsOneWidget);
822+
expect(find.byType(RefreshProgressIndicator), findsNothing);
823+
}
824+
825+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
826+
await tester.pumpWidget(buildFrame(platform));
827+
await tester.pumpAndSettle(); // Finish the theme change animation.
828+
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
829+
await tester.pump();
830+
831+
expect(tester.getSemantics(find.byType(RefreshProgressIndicator)), matchesSemantics(
832+
label: 'Refresh',
833+
));
834+
expect(find.byType(CupertinoActivityIndicator), findsNothing);
835+
}
836+
});
837+
795838
testWidgets('RefreshIndicator color defaults to ColorScheme.primary', (WidgetTester tester) async {
796839
const Color primaryColor = Color(0xff4caf50);
797840
final ThemeData theme = ThemeData.from(colorScheme: const ColorScheme.light().copyWith(primary: primaryColor));

0 commit comments

Comments
 (0)