feat(android): better quick scroll/drag to scroll implementation

This commit is contained in:
Kingkor Roy Tirtho 2023-11-13 23:17:16 +06:00
parent 0a6b54da36
commit 2e2c44f0af
13 changed files with 330 additions and 340 deletions

View File

@ -163,6 +163,8 @@ class UserLocalTracks extends HookConsumerWidget {
final searchFocus = useFocusNode(); final searchFocus = useFocusNode();
final isFiltering = useState(false); final isFiltering = useState(false);
final controller = useScrollController();
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -256,7 +258,9 @@ class UserLocalTracks extends HookConsumerWidget {
ref.refresh(localTracksProvider); ref.refresh(localTracksProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length, itemCount: filteredTracks.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View File

@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_dialog.dart'; import 'package:spotube/components/playlist/playlist_create_dialog.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/playlist/playlist_card.dart'; import 'package:spotube/components/playlist/playlist_card.dart';
@ -81,6 +82,8 @@ class UserPlaylists extends HookConsumerWidget {
return RefreshIndicator( return RefreshIndicator(
onRefresh: playlistsQuery.refresh, onRefresh: playlistsQuery.refresh,
child: SafeArea( child: SafeArea(
child: InterScrollbar(
controller: controller,
child: CustomScrollView( child: CustomScrollView(
controller: controller, controller: controller,
slivers: [ slivers: [
@ -145,6 +148,7 @@ class UserPlaylists extends HookConsumerWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -44,6 +44,7 @@ class PlayerQueue extends HookConsumerWidget {
topRight: Radius.circular(10), topRight: Radius.circular(10),
); );
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color; final headlineColor = theme.textTheme.headlineSmall?.color;
final filteredTracks = useMemoized( final filteredTracks = useMemoized(
@ -108,8 +109,7 @@ class PlayerQueue extends HookConsumerWidget {
searchText.value = ''; searchText.value = '';
} }
}, },
child: LayoutBuilder(builder: (context, constraints) { child: Column(
return Column(
children: [ children: [
if (!floating) if (!floating)
Container( Container(
@ -125,7 +125,7 @@ class PlayerQueue extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (constraints.mdAndUp || !isSearching.value) ...[ if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
context.l10n.tracks_in_queue(tracks.length), context.l10n.tracks_in_queue(tracks.length),
@ -137,7 +137,7 @@ class PlayerQueue extends HookConsumerWidget {
), ),
const Spacer(), const Spacer(),
], ],
if (constraints.mdAndUp || isSearching.value) if (mediaQuery.mdAndUp || isSearching.value)
TextField( TextField(
onChanged: (value) { onChanged: (value) {
searchText.value = value; searchText.value = value;
@ -145,7 +145,7 @@ class PlayerQueue extends HookConsumerWidget {
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.search, hintText: context.l10n.search,
isDense: true, isDense: true,
prefixIcon: constraints.smAndDown prefixIcon: mediaQuery.smAndDown
? IconButton( ? IconButton(
icon: const Icon( icon: const Icon(
Icons.arrow_back_ios_new_outlined, Icons.arrow_back_ios_new_outlined,
@ -162,8 +162,8 @@ class PlayerQueue extends HookConsumerWidget {
: const Icon(SpotubeIcons.filter), : const Icon(SpotubeIcons.filter),
constraints: BoxConstraints( constraints: BoxConstraints(
maxHeight: 40, maxHeight: 40,
maxWidth: constraints.smAndDown maxWidth: mediaQuery.smAndDown
? constraints.maxWidth - 20 ? mediaQuery.size.width - 40
: 300, : 300,
), ),
), ),
@ -175,14 +175,13 @@ class PlayerQueue extends HookConsumerWidget {
isSearching.value = !isSearching.value; isSearching.value = !isSearching.value;
}, },
), ),
if (constraints.mdAndUp || !isSearching.value) ...[ if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
FilledButton( FilledButton(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: backgroundColor:
theme.scaffoldBackgroundColor.withOpacity(0.5), theme.scaffoldBackgroundColor.withOpacity(0.5),
foregroundColor: foregroundColor: theme.textTheme.headlineSmall?.color,
theme.textTheme.headlineSmall?.color,
), ),
child: Row( child: Row(
children: [ children: [
@ -203,8 +202,6 @@ class PlayerQueue extends HookConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
if (!isSearching.value && searchText.value.isEmpty) if (!isSearching.value && searchText.value.isEmpty)
Flexible( Flexible(
child: InterScrollbar(
controller: controller,
child: ReorderableListView.builder( child: ReorderableListView.builder(
onReorder: (oldIndex, newIndex) { onReorder: (oldIndex, newIndex) {
playlistNotifier.moveTrack(oldIndex, newIndex); playlistNotifier.moveTrack(oldIndex, newIndex);
@ -234,8 +231,7 @@ class PlayerQueue extends HookConsumerWidget {
leadingActions: [ leadingActions: [
ReorderableDragStartListener( ReorderableDragStartListener(
index: i, index: i,
child: child: const Icon(SpotubeIcons.dragHandle),
const Icon(SpotubeIcons.dragHandle),
), ),
], ],
), ),
@ -243,12 +239,13 @@ class PlayerQueue extends HookConsumerWidget {
); );
}, },
), ),
),
) )
else else
Flexible( Flexible(
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
itemCount: filteredTracks.length, itemCount: filteredTracks.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i); final track = filteredTracks.elementAt(i);
@ -271,8 +268,7 @@ class PlayerQueue extends HookConsumerWidget {
), ),
), ),
], ],
); ),
}),
), ),
), ),
), ),

View File

@ -56,6 +56,8 @@ class SiblingTracksSheet extends HookConsumerWidget {
useValueListenable(searchController).text, useValueListenable(searchController).text,
); );
final controller = useScrollController();
final searchRequest = useMemoized(() async { final searchRequest = useMemoized(() async {
if (searchTerm.trim().isEmpty) { if (searchTerm.trim().isEmpty) {
return <YoutubeVideoInfo>[]; return <YoutubeVideoInfo>[];
@ -204,8 +206,10 @@ class SiblingTracksSheet extends HookConsumerWidget {
transitionBuilder: (child, animation) => transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: switch (isSearching.value) { child: switch (isSearching.value) {
false => ListView.builder( false => ListView.builder(
controller: controller,
itemCount: siblings.length, itemCount: siblings.length,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
itemBuilder(siblings[index]), itemBuilder(siblings[index]),
@ -223,7 +227,9 @@ class SiblingTracksSheet extends HookConsumerWidget {
} }
return InterScrollbar( return InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
itemCount: snapshot.data!.length, itemCount: snapshot.data!.length,
itemBuilder: (context, index) => itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]), itemBuilder(snapshot.data![index]),

View File

@ -1,29 +1,16 @@
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
class InterScrollbar extends HookWidget { class InterScrollbar extends HookWidget {
final Widget child; final Widget child;
final ScrollController? controller; final ScrollController controller;
final bool? thumbVisibility;
final bool? trackVisibility;
final double? thickness;
final Radius? radius;
final bool Function(ScrollNotification)? notificationPredicate;
final bool? interactive;
final ScrollbarOrientation? scrollbarOrientation;
const InterScrollbar({ const InterScrollbar({
super.key, super.key,
required this.child, required this.child,
this.controller, required this.controller,
this.thumbVisibility,
this.trackVisibility,
this.thickness,
this.radius,
this.notificationPredicate,
this.interactive,
this.scrollbarOrientation,
}); });
@override @override
@ -32,38 +19,9 @@ class InterScrollbar extends HookWidget {
if (DesktopTools.platform.isDesktop) return child; if (DesktopTools.platform.isDesktop) return child;
return ScrollbarTheme( return DraggableScrollbar.semicircle(
data: theme.scrollbarTheme.copyWith(
crossAxisMargin: 10,
minThumbLength: 80,
thickness: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered) ||
states.contains(MaterialState.dragged) ||
states.contains(MaterialState.pressed)) {
return 40;
}
return 20;
}),
radius: const Radius.circular(20),
thumbColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.hovered) ||
states.contains(MaterialState.dragged)) {
return theme.colorScheme.onSurface.withOpacity(0.5);
}
return theme.colorScheme.onSurface.withOpacity(0.3);
}),
),
child: Scrollbar(
controller: controller, controller: controller,
thumbVisibility: thumbVisibility,
trackVisibility: trackVisibility,
thickness: thickness,
radius: radius,
notificationPredicate: notificationPredicate,
interactive: interactive ?? true,
scrollbarOrientation: scrollbarOrientation,
child: child, child: child,
),
); );
} }
} }

View File

@ -79,8 +79,6 @@ class GenrePage extends HookConsumerWidget {
const ShimmerCategories() const ShimmerCategories()
else else
Expanded( Expanded(
child: InterScrollbar(
controller: scrollController,
child: ListView.builder( child: ListView.builder(
controller: scrollController, controller: scrollController,
itemCount: categories.length, itemCount: categories.length,
@ -102,7 +100,6 @@ class GenrePage extends HookConsumerWidget {
}, },
), ),
), ),
),
], ],
), ),
), ),

View File

@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart'; import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
@ -47,9 +46,7 @@ class PersonalizedPage extends HookConsumerWidget {
[newReleases.pages], [newReleases.pages],
); );
return InterScrollbar( return ListView(
controller: controller,
child: ListView(
controller: controller, controller: controller,
children: [ children: [
if (!featuredPlaylistsQuery.hasPageData && if (!featuredPlaylistsQuery.hasPageData &&
@ -88,7 +85,6 @@ class PersonalizedPage extends HookConsumerWidget {
); );
}) })
], ],
),
); );
} }
} }

View File

@ -71,8 +71,13 @@ class SearchPage extends HookConsumerWidget {
searchTerm.isNotEmpty; searchTerm.isNotEmpty;
final resultWidget = HookBuilder( final resultWidget = HookBuilder(
builder: (context) => InterScrollbar( builder: (context) {
final controller = useScrollController();
return InterScrollbar(
controller: controller,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: controller,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: SafeArea( child: SafeArea(
@ -90,7 +95,8 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
), ),
), );
},
); );
return SafeArea( return SafeArea(

View File

@ -15,6 +15,7 @@ class BlackListPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final controller = useScrollController();
final blacklist = ref.watch(BlackListNotifier.provider); final blacklist = ref.watch(BlackListNotifier.provider);
final searchText = useState(""); final searchText = useState("");
@ -58,7 +59,9 @@ class BlackListPage extends HookConsumerWidget {
), ),
), ),
InterScrollbar( InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
shrinkWrap: true, shrinkWrap: true,
itemCount: filteredBlacklist.length, itemCount: filteredBlacklist.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View File

@ -52,6 +52,7 @@ class LogsPage extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = useScrollController();
final logs = useState<List<({DateTime? date, String body})>>([]); final logs = useState<List<({DateTime? date, String body})>>([]);
final rawLogs = useRef<String>(""); final rawLogs = useRef<String>("");
final path = useRef<File?>(null); final path = useRef<File?>(null);
@ -93,7 +94,9 @@ class LogsPage extends HookWidget {
), ),
body: SafeArea( body: SafeArea(
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView.builder( child: ListView.builder(
controller: controller,
itemCount: logs.value.length, itemCount: logs.value.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final log = logs.value[index]; final log = logs.value[index];

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
@ -20,6 +21,7 @@ class SettingsPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final controller = useScrollController();
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
return SafeArea( return SafeArea(
@ -36,7 +38,9 @@ class SettingsPage extends HookConsumerWidget {
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: InterScrollbar( child: InterScrollbar(
controller: controller,
child: ListView( child: ListView(
controller: controller,
children: [ children: [
const SettingsAccountSection(), const SettingsAccountSection(),
const SettingsLanguageRegionSection(), const SettingsLanguageRegionSection(),

View File

@ -465,6 +465,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
draggable_scrollbar:
dependency: "direct main"
description:
path: "."
ref: cfd570035bf393de541d32e9b28808b5d7e602df
resolved-ref: cfd570035bf393de541d32e9b28808b5d7e602df
url: "https://github.com/thielepaul/flutter-draggable-scrollbar.git"
source: git
version: "0.1.0"
duration: duration:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -106,6 +106,10 @@ dependencies:
simple_icons: ^7.10.0 simple_icons: ^7.10.0
audio_service_mpris: ^0.1.0 audio_service_mpris: ^0.1.0
file_picker: ^6.0.0 file_picker: ^6.0.0
draggable_scrollbar:
git:
url: https://github.com/thielepaul/flutter-draggable-scrollbar.git
ref: cfd570035bf393de541d32e9b28808b5d7e602df
dev_dependencies: dev_dependencies:
build_runner: ^2.3.2 build_runner: ^2.3.2