Unverified Commit cb67ecd9 authored by Mitchell Goodwin's avatar Mitchell Goodwin Committed by GitHub

Add adaptive RefreshIndicator (#121249)

parent 9e6214f8
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/widgets.dart';
import 'debug.dart'; import 'debug.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
...@@ -59,6 +59,8 @@ enum RefreshIndicatorTriggerMode { ...@@ -59,6 +59,8 @@ enum RefreshIndicatorTriggerMode {
onEdge, onEdge,
} }
enum _IndicatorType { material, adaptive }
/// A widget that supports the Material "swipe to refresh" idiom. /// A widget that supports the Material "swipe to refresh" idiom.
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM} /// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
...@@ -138,7 +140,38 @@ class RefreshIndicator extends StatefulWidget { ...@@ -138,7 +140,38 @@ class RefreshIndicator extends StatefulWidget {
this.semanticsValue, this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth, this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge, this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}); }) : _indicatorType = _IndicatorType.material;
/// Creates an adaptive [RefreshIndicator] based on whether the target
/// platform is iOS or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// When the descendant overscrolls, a different spinning progress indicator
/// is shown depending on platform. On iOS and macOS,
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
/// [CircularProgressIndicator] appears.
///
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
///
/// Noteably the scrollable widget itself will have slightly different behavior
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
const RefreshIndicator.adaptive({
super.key,
required this.child,
this.displacement = 40.0,
this.edgeOffset = 0.0,
required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.adaptive;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
/// ///
...@@ -207,6 +240,8 @@ class RefreshIndicator extends StatefulWidget { ...@@ -207,6 +240,8 @@ class RefreshIndicator extends StatefulWidget {
/// By default, the value of [strokeWidth] is 2.0 pixels. /// By default, the value of [strokeWidth] is 2.0 pixels.
final double strokeWidth; final double strokeWidth;
final _IndicatorType _indicatorType;
/// Defines how this [RefreshIndicator] can be triggered when users overscroll. /// Defines how this [RefreshIndicator] can be triggered when users overscroll.
/// ///
/// The [RefreshIndicator] can be pulled out in two cases, /// The [RefreshIndicator] can be pulled out in two cases,
...@@ -555,7 +590,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -555,7 +590,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _positionController, animation: _positionController,
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
return RefreshProgressIndicator( final Widget materialIndicator = RefreshProgressIndicator(
semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context).refreshIndicatorSemanticLabel, semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue, semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator ? null : _value.value, value: showIndeterminateIndicator ? null : _value.value,
...@@ -563,6 +598,29 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -563,6 +598,29 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth, strokeWidth: widget.strokeWidth,
); );
final Widget cupertinoIndicator = CupertinoActivityIndicator(
color: widget.color,
);
switch(widget._indicatorType) {
case _IndicatorType.material:
return materialIndicator;
case _IndicatorType.adaptive: {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return materialIndicator;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return cupertinoIndicator;
}
}
}
}, },
), ),
), ),
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -792,6 +793,48 @@ void main() { ...@@ -792,6 +793,48 @@ void main() {
expect(refreshCalled, false); expect(refreshCalled, false);
}); });
testWidgets('RefreshIndicator.adaptive', (WidgetTester tester) async {
Widget buildFrame(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: RefreshIndicator.adaptive(
onRefresh: refresh,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
return SizedBox(
height: 200.0,
child: Text(item),
);
}).toList(),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildFrame(platform));
await tester.pumpAndSettle(); // Finish the theme change animation.
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
await tester.pump();
expect(find.byType(CupertinoActivityIndicator), findsOneWidget);
expect(find.byType(RefreshProgressIndicator), findsNothing);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(buildFrame(platform));
await tester.pumpAndSettle(); // Finish the theme change animation.
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
await tester.pump();
expect(tester.getSemantics(find.byType(RefreshProgressIndicator)), matchesSemantics(
label: 'Refresh',
));
expect(find.byType(CupertinoActivityIndicator), findsNothing);
}
});
testWidgets('RefreshIndicator color defaults to ColorScheme.primary', (WidgetTester tester) async { testWidgets('RefreshIndicator color defaults to ColorScheme.primary', (WidgetTester tester) async {
const Color primaryColor = Color(0xff4caf50); const Color primaryColor = Color(0xff4caf50);
final ThemeData theme = ThemeData.from(colorScheme: const ColorScheme.light().copyWith(primary: primaryColor)); final ThemeData theme = ThemeData.from(colorScheme: const ColorScheme.light().copyWith(primary: primaryColor));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment