Add adaptive RefreshIndicator (#121249)
This commit is contained in:
parent
9e6214f873
commit
cb67ecd97d
@ -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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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
|
|||||||
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
|
|||||||
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 @@
|
|||||||
|
|
||||||
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() {
|
|||||||
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));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user