use a sliver-based approach
This commit is contained in:
parent
bac0a5df23
commit
67a31eb1fd
3 changed files with 163 additions and 123 deletions
|
|
@ -1,55 +1,35 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_emoji_picker/src/controllers/emoji.dart";
|
||||
import "package:material_emoji_picker/src/models/emoji_category.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
final class const EmojiPicker({
|
||||
final IList<EmojiCategory> prependCategories = const IList.empty(),
|
||||
final IList<EmojiCategory> appendCategories = const IList.empty(),
|
||||
required final FutureOr<void> Function(String value) onSelection,
|
||||
final bool allowFreeText = false,
|
||||
class EmojiPicker extends HookConsumerWidget {
|
||||
const EmojiPicker({
|
||||
this.prependCategories = const IList.empty(),
|
||||
this.appendCategories = const IList.empty(),
|
||||
required this.onSelection,
|
||||
this.allowFreeText = false,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
});
|
||||
|
||||
final IList<EmojiCategory> prependCategories;
|
||||
final IList<EmojiCategory> appendCategories;
|
||||
final FutureOr<void> Function(String value) onSelection;
|
||||
final bool allowFreeText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedCategory = useState<int?>(null);
|
||||
final selectedCategory = useState<int>(0);
|
||||
final search = useState("");
|
||||
|
||||
final scrollController = useScrollController();
|
||||
final listController = useMemoized(ListController.new);
|
||||
final scrollViewKey = useMemoized(() => GlobalKey());
|
||||
|
||||
final categoryScrollController = useScrollController();
|
||||
final categoryListController = useMemoized(ListController.new);
|
||||
|
||||
useEffect(() {
|
||||
void onChange() {
|
||||
final range = listController.visibleRange;
|
||||
if (range != null) {
|
||||
final firstVisible = range.$1;
|
||||
if (selectedCategory.value != firstVisible) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
selectedCategory.value = firstVisible;
|
||||
categoryListController.animateToItem(
|
||||
index: firstVisible,
|
||||
scrollController: categoryScrollController,
|
||||
alignment: 0,
|
||||
duration: (_) => Duration(milliseconds: 300),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listController.addListener(onChange);
|
||||
return () => listController.removeListener(onChange);
|
||||
}, [listController]);
|
||||
|
||||
return Container(
|
||||
padding: .all(16),
|
||||
|
|
@ -64,6 +44,63 @@ final class const EmojiPicker({
|
|||
prependCategories.addAll(categories).addAll(appendCategories),
|
||||
);
|
||||
|
||||
final headerKeys = useMemoized(
|
||||
() =>
|
||||
List.generate(combined.length, (_) => GlobalKey()).toIList(),
|
||||
[combined],
|
||||
);
|
||||
|
||||
final chipKeys = useMemoized(
|
||||
() =>
|
||||
List.generate(combined.length, (_) => GlobalKey()).toIList(),
|
||||
[combined],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
void syncSelectedCategoryWithScroll() async {
|
||||
final viewport =
|
||||
scrollViewKey.currentContext?.findRenderObject()
|
||||
as RenderBox?;
|
||||
if (viewport == null) return;
|
||||
|
||||
var firstVisibleIndex = 0;
|
||||
|
||||
for (var index = 0; index < headerKeys.length; index++) {
|
||||
final header = headerKeys[index].currentContext
|
||||
?.findRenderObject();
|
||||
if (header is! RenderBox) continue;
|
||||
|
||||
final headerTop = header
|
||||
.localToGlobal(Offset.zero, ancestor: viewport)
|
||||
.dy;
|
||||
|
||||
if (headerTop > 1) break;
|
||||
|
||||
firstVisibleIndex = index;
|
||||
}
|
||||
|
||||
if (firstVisibleIndex != selectedCategory.value) {
|
||||
selectedCategory.value = firstVisibleIndex;
|
||||
final chipContext =
|
||||
chipKeys[firstVisibleIndex].currentContext;
|
||||
|
||||
if (chipContext != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
chipContext,
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(syncSelectedCategoryWithScroll);
|
||||
return () => scrollController.removeListener(
|
||||
syncSelectedCategoryWithScroll,
|
||||
);
|
||||
}, [scrollController, headerKeys]);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SearchBar(
|
||||
|
|
@ -85,14 +122,14 @@ final class const EmojiPicker({
|
|||
height: 48,
|
||||
child: Scrollbar(
|
||||
controller: categoryScrollController,
|
||||
child: SuperListView(
|
||||
child: ListView(
|
||||
padding: .only(bottom: 12),
|
||||
scrollDirection: .horizontal,
|
||||
controller: categoryScrollController,
|
||||
listController: categoryListController,
|
||||
children: combined
|
||||
.mapIndexed(
|
||||
(index, category) => Padding(
|
||||
children: [
|
||||
for (final (index, category) in combined.indexed)
|
||||
Padding(
|
||||
key: chipKeys[index],
|
||||
padding: .symmetric(horizontal: 4),
|
||||
child: FilterChip(
|
||||
label: Text(category.name),
|
||||
|
|
@ -101,30 +138,32 @@ final class const EmojiPicker({
|
|||
showCheckmark: false,
|
||||
onSelected: (_) {
|
||||
selectedCategory.value = index;
|
||||
|
||||
listController.animateToItem(
|
||||
index: index,
|
||||
scrollController: scrollController,
|
||||
curve: (_) => Curves.easeInOut,
|
||||
duration: (_) =>
|
||||
Duration(milliseconds: 300),
|
||||
final headerContext =
|
||||
headerKeys[index].currentContext;
|
||||
if (headerContext != null) {
|
||||
Scrollable.ensureVisible(
|
||||
headerContext,
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: 0,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SuperListView.builder(
|
||||
listController: listController,
|
||||
child: CustomScrollView(
|
||||
key: scrollViewKey,
|
||||
controller: scrollController,
|
||||
itemCount: combined.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = combined[index];
|
||||
slivers: [
|
||||
for (final (index, category) in combined.indexed)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final headerKey = headerKeys[index];
|
||||
final emojis = search.value.isEmpty
|
||||
? category.emojis
|
||||
: category.emojis
|
||||
|
|
@ -136,28 +175,36 @@ final class const EmojiPicker({
|
|||
emoji.description.contains(
|
||||
search.value,
|
||||
) ||
|
||||
emoji.tags.join().contains(search.value),
|
||||
emoji.tags.join().contains(
|
||||
search.value,
|
||||
),
|
||||
)
|
||||
.toIList();
|
||||
|
||||
if (emojis.isEmpty) return SizedBox.shrink();
|
||||
if (emojis.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox.shrink(key: headerKey),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
key: headerKey,
|
||||
category.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
GridView.builder(
|
||||
),
|
||||
SliverGrid.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 56,
|
||||
mainAxisExtent: 56,
|
||||
),
|
||||
itemCount: emojis.length,
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
final emoji = emojis[index];
|
||||
|
||||
|
|
@ -171,11 +218,13 @@ final class const EmojiPicker({
|
|||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 4)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -581,14 +581,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
super_sliver_list:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: super_sliver_list
|
||||
sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ dependencies:
|
|||
json_annotation: ^4.12.0
|
||||
flutter_hooks: ^0.21.3+1
|
||||
http: ^1.6.0
|
||||
super_sliver_list: ^0.4.1
|
||||
material_ui: ^1.2.0
|
||||
|
||||
dev_dependencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue