mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-27 20:02:01 +02:00
fix(download): make picker drag dismissal natural
This commit is contained in:
@@ -40,6 +40,39 @@ class AppSheetHandle extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A modal-sheet surface whose height and inner scroll position share one drag.
|
||||||
|
///
|
||||||
|
/// The child must attach the supplied [ScrollController] to its primary
|
||||||
|
/// vertical scroll view. Pulling down at the top then moves the whole surface;
|
||||||
|
/// releasing either restores it or dismisses it at the minimum extent.
|
||||||
|
class AppDraggableSheet extends StatelessWidget {
|
||||||
|
const AppDraggableSheet({
|
||||||
|
super.key,
|
||||||
|
required this.builder,
|
||||||
|
this.initialChildSize = 0.88,
|
||||||
|
this.minChildSize = 0.25,
|
||||||
|
this.maxChildSize = 0.88,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScrollableWidgetBuilder builder;
|
||||||
|
final double initialChildSize;
|
||||||
|
final double minChildSize;
|
||||||
|
final double maxChildSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
snap: true,
|
||||||
|
shouldCloseOnMinExtent: true,
|
||||||
|
initialChildSize: initialChildSize,
|
||||||
|
minChildSize: minChildSize,
|
||||||
|
maxChildSize: maxChildSize,
|
||||||
|
builder: builder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Standard chrome for modal sheet content: drag handle, optional title block,
|
/// Standard chrome for modal sheet content: drag handle, optional title block,
|
||||||
/// height cap, keyboard inset and bottom safe area, so each sheet only supplies
|
/// height cap, keyboard inset and bottom safe area, so each sheet only supplies
|
||||||
/// its own body.
|
/// its own body.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
|
|||||||
final String? coverUrl;
|
final String? coverUrl;
|
||||||
final void Function(String quality, String service) onSelect;
|
final void Function(String quality, String service) onSelect;
|
||||||
final String? recommendedService;
|
final String? recommendedService;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
const DownloadServicePicker({
|
const DownloadServicePicker({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -21,6 +22,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
|
|||||||
this.coverUrl,
|
this.coverUrl,
|
||||||
required this.onSelect,
|
required this.onSelect,
|
||||||
this.recommendedService,
|
this.recommendedService,
|
||||||
|
this.scrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,12 +44,16 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
|
|||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
backgroundColor: colorScheme.surfaceContainerHigh,
|
backgroundColor: colorScheme.surfaceContainerHigh,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
builder: (context) => DownloadServicePicker(
|
enableDrag: false,
|
||||||
trackName: trackName,
|
builder: (context) => AppDraggableSheet(
|
||||||
artistName: artistName,
|
builder: (context, scrollController) => DownloadServicePicker(
|
||||||
coverUrl: coverUrl,
|
trackName: trackName,
|
||||||
onSelect: onSelect,
|
artistName: artistName,
|
||||||
recommendedService: recommendedService,
|
coverUrl: coverUrl,
|
||||||
|
onSelect: onSelect,
|
||||||
|
recommendedService: recommendedService,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -120,6 +126,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
|
|||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
controller: widget.scrollController,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -259,6 +259,64 @@ void main() {
|
|||||||
expect(find.text('body'), findsOneWidget);
|
expect(find.text('body'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('draggable content moves as one surface and dismisses', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
const sheetKey = ValueKey<String>('scrollable-sheet');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: AppTheme.light(),
|
||||||
|
home: Scaffold(
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) => ElevatedButton(
|
||||||
|
onPressed: () => showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
// Disable the route recognizer so the draggable surface,
|
||||||
|
// rather than the modal's fallback gesture, is under test.
|
||||||
|
enableDrag: false,
|
||||||
|
builder: (_) => AppDraggableSheet(
|
||||||
|
builder: (_, scrollController) => Material(
|
||||||
|
key: sheetKey,
|
||||||
|
child: ListView(
|
||||||
|
controller: scrollController,
|
||||||
|
children: const [
|
||||||
|
SizedBox(height: 800, child: Text('sheet body')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(sheetKey), findsOneWidget);
|
||||||
|
|
||||||
|
final initialTop = tester.getTopLeft(find.byKey(sheetKey)).dy;
|
||||||
|
final gesture = await tester.startGesture(
|
||||||
|
tester.getCenter(find.byKey(sheetKey)),
|
||||||
|
);
|
||||||
|
await gesture.moveBy(const Offset(0, 160));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.getTopLeft(find.byKey(sheetKey)).dy,
|
||||||
|
greaterThan(initialTop + 100),
|
||||||
|
);
|
||||||
|
|
||||||
|
await gesture.moveBy(const Offset(0, 260));
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(sheetKey), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('sheet shape comes from the token scale', (tester) async {
|
testWidgets('sheet shape comes from the token scale', (tester) async {
|
||||||
final shape =
|
final shape =
|
||||||
AppTheme.light().bottomSheetTheme.shape! as RoundedRectangleBorder;
|
AppTheme.light().bottomSheetTheme.shape! as RoundedRectangleBorder;
|
||||||
|
|||||||
Reference in New Issue
Block a user