feat(ui,train): a pull far enough to refresh ticks, lightly, as the navigation does
AppRefreshIndicator ticks (selectionClick, as a choice on the floating bar or the rail) once the pull commits: as it snaps back to refresh, or as it refreshes at once, whichever comes first, and not for a pull let go too early. Train's list of days pulled to read the band used Flutter's own RefreshIndicator, the older spinner; it takes the app's now, and the tick with it. Whats-New: Pulling a list down to refresh ticks lightly. Whats-New-De: Eine Liste zum Aktualisieren herunterziehen gibt ein leichtes Tippen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4239a1de24
commit
bcbefe38a3
3 changed files with 62 additions and 2 deletions
|
|
@ -55,7 +55,7 @@ class const DaysList({super.key}) extends StatelessWidget {
|
||||||
if (!state.holdsLoop) {
|
if (!state.holdsLoop) {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
return RefreshIndicator(
|
return AppRefreshIndicator(
|
||||||
onRefresh: () => context.read<DaysCubit>().readBand(),
|
onRefresh: () => context.read<DaysCubit>().readBand(),
|
||||||
child: list,
|
child: list,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The app's spinner for anything still loading: Material 3 Expressive's
|
/// The app's spinner for anything still loading: Material 3 Expressive's
|
||||||
|
|
@ -40,7 +43,8 @@ class const ButtonLoadingIndicator({super.key}) extends StatelessWidget {
|
||||||
/// meanwhile, as Material 3 Expressive's pull to refresh has it: in a small
|
/// meanwhile, as Material 3 Expressive's pull to refresh has it: in a small
|
||||||
/// round container that comes down with the pull and stays while it
|
/// round container that comes down with the pull and stays while it
|
||||||
/// refreshes. Flutter's own [RefreshIndicator] draws the older spinner, so
|
/// refreshes. Flutter's own [RefreshIndicator] draws the older spinner, so
|
||||||
/// it only does the pulling here (its `noSpinner` kind).
|
/// it only does the pulling here (its `noSpinner` kind). A pull far enough
|
||||||
|
/// to refresh ticks, lightly, as a choice on the navigation bar does.
|
||||||
class const AppRefreshIndicator({
|
class const AppRefreshIndicator({
|
||||||
required final RefreshCallback onRefresh,
|
required final RefreshCallback onRefresh,
|
||||||
required final Widget child,
|
required final Widget child,
|
||||||
|
|
@ -110,6 +114,15 @@ class _AppRefreshIndicatorState() extends State<AppRefreshIndicator> {
|
||||||
child: RefreshIndicator.noSpinner(
|
child: RefreshIndicator.noSpinner(
|
||||||
onRefresh: widget.onRefresh,
|
onRefresh: widget.onRefresh,
|
||||||
onStatusChange: (status) => setState(() {
|
onStatusChange: (status) => setState(() {
|
||||||
|
// Snapping back to refresh, or refreshing at once: let go
|
||||||
|
// far enough, whichever comes first.
|
||||||
|
const committed = {
|
||||||
|
RefreshIndicatorStatus.snap,
|
||||||
|
RefreshIndicatorStatus.refresh,
|
||||||
|
};
|
||||||
|
if (committed.contains(status) && !committed.contains(_status)) {
|
||||||
|
unawaited(HapticFeedback.selectionClick());
|
||||||
|
}
|
||||||
_status = status;
|
_status = status;
|
||||||
if (status == RefreshIndicatorStatus.done ||
|
if (status == RefreshIndicatorStatus.done ||
|
||||||
status == RefreshIndicatorStatus.canceled) {
|
status == RefreshIndicatorStatus.canceled) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
import 'package:expressive_loading_indicator/expressive_loading_indicator.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
@ -40,4 +41,50 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byType(ExpressiveLoadingIndicator), findsNothing);
|
expect(find.byType(ExpressiveLoadingIndicator), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('a light tick', () {
|
||||||
|
Future<List<Object?>> pulled(WidgetTester tester, double by) async {
|
||||||
|
final haptics = <Object?>[];
|
||||||
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
(call) async {
|
||||||
|
if (call.method == 'HapticFeedback.vibrate') {
|
||||||
|
haptics.add(call.arguments);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(
|
||||||
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.platform,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: appTheme(Brightness.light),
|
||||||
|
home: Scaffold(
|
||||||
|
body: AppRefreshIndicator(
|
||||||
|
onRefresh: () async {},
|
||||||
|
child: ListView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: const [Text('Japanese')],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.fling(find.text('Japanese'), Offset(0, by), 1000);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
return haptics;
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('as a pull far enough refreshes, once', (tester) async {
|
||||||
|
expect(await pulled(tester, 400), ['HapticFeedbackType.selectionClick']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('but none for a pull let go too early', (tester) async {
|
||||||
|
expect(await pulled(tester, 20), isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue