diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d83213..a7fb966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,14 +17,3 @@ Publicly export EmojiController for use as needed. ## 1.0.3 Add scrollbar to category list, for more intuitive scrolling on desktop. - -## 1.1.0 - -- Minify emoji json for a smaller bundle -- Use a Sliver-based approach for rendering the emoji view, resulting in much better performance -- Add more padding to search bar -- Filter out empty categories, helpful for search - -## 1.1.1 - -Wait for end of frame before highlighting categories, fixing an issue where category highlights could be one scroll outdated. diff --git a/README.md b/README.md index 35031f3..d2cc493 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,9 @@ A generic but configurable emoji picker for Flutter. Defaults to including only - Up-to-date emojis, sourced from gemoji - The ability to prepend or append custom categories with custom emoji - Support for free text selection -- High performance due to using slivers plus a custom listener - Sleek M3 design: -![A screenshot of the emoji picker, showing a searchbar, category selection, and two categories of emojis](https://git.federated.nexus/Henry-Hiles/material_emoji_picker/raw/branch/main/assets/screenshots/main.png) +![A screenshot of the emoji picker, showing a searchbar, category selection, and two categories of emojis](https://git.federated.nexus/Henry-Hiles/material_emoji_picker/raw/branch/main/assets/screenshot.png) ## Getting started diff --git a/assets/screenshot.png b/assets/screenshot.png new file mode 100644 index 0000000..d0315fe Binary files /dev/null and b/assets/screenshot.png differ diff --git a/assets/screenshots/main.png b/assets/screenshots/main.png deleted file mode 100644 index ba8a51a..0000000 Binary files a/assets/screenshots/main.png and /dev/null differ diff --git a/lib/src/widgets/emoji_picker.dart b/lib/src/widgets/emoji_picker.dart index 5695a05..6980b9a 100644 --- a/lib/src/widgets/emoji_picker.dart +++ b/lib/src/widgets/emoji_picker.dart @@ -7,30 +7,49 @@ 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"; -class EmojiPicker extends HookConsumerWidget { - const EmojiPicker({ - this.prependCategories = const IList.empty(), - this.appendCategories = const IList.empty(), - required this.onSelection, - this.allowFreeText = false, - super.key, - }); - - final IList prependCategories; - final IList appendCategories; - final FutureOr Function(String value) onSelection; - final bool allowFreeText; - +final class const EmojiPicker({ + final IList prependCategories = const IList.empty(), + final IList appendCategories = const IList.empty(), + required final FutureOr Function(String value) onSelection, + final bool allowFreeText = false, + super.key, +}) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedCategory = useState(0); + final selectedCategory = useState(null); final search = useState(""); final scrollController = useScrollController(); - final scrollViewKey = useMemoized(() => GlobalKey()); + final listController = useMemoized(ListController.new); 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), @@ -45,67 +64,10 @@ class EmojiPicker extends HookConsumerWidget { 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(() { - Future syncSelectedCategoryWithScroll() async { - await WidgetsBinding.instance.endOfFrame; - - final viewport = scrollViewKey.currentContext - ?.findRenderObject(); - if (viewport is! RenderBox) return; - - final selectedIndex = headerKeys.lastIndexWhere((headerKey) { - final renderObject = headerKey.currentContext - ?.findRenderObject(); - - if (renderObject is! RenderBox) return false; - - final top = renderObject - .localToGlobal(Offset.zero, ancestor: viewport) - .dy; - - return top <= 20; - }); - - if (selectedIndex != selectedCategory.value) { - selectedCategory.value = selectedIndex; - final chipContext = chipKeys[selectedIndex].currentContext; - - if (chipContext != null && chipContext.mounted) { - 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( hintText: "Search emoji", - padding: const WidgetStatePropertyAll( - .symmetric(horizontal: 16.0), - ), leading: Icon(Icons.search), onChanged: (value) => search.value = value, trailing: [ @@ -123,14 +85,14 @@ class EmojiPicker extends HookConsumerWidget { height: 48, child: Scrollbar( controller: categoryScrollController, - child: ListView( + child: SuperListView( padding: .only(bottom: 12), scrollDirection: .horizontal, controller: categoryScrollController, + listController: categoryListController, children: combined .mapIndexed( (index, category) => Padding( - key: chipKeys[index], padding: .symmetric(horizontal: 4), child: FilterChip( label: Text(category.name), @@ -139,16 +101,15 @@ class EmojiPicker extends HookConsumerWidget { showCheckmark: false, onSelected: (_) { selectedCategory.value = index; - final headerContext = - headerKeys[index].currentContext; - if (headerContext != null) { - Scrollable.ensureVisible( - headerContext, - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - alignment: 0, - ); - } + + listController.animateToItem( + index: index, + scrollController: scrollController, + curve: (_) => Curves.easeInOut, + duration: (_) => + Duration(milliseconds: 300), + alignment: 0, + ); }, ), ), @@ -158,78 +119,62 @@ class EmojiPicker extends HookConsumerWidget { ), ), Expanded( - child: CustomScrollView( - key: scrollViewKey, + child: SuperListView.builder( + listController: listController, controller: scrollController, - slivers: combined - .mapIndexed( - (index, category) => Builder( - builder: (context) { - final headerKey = headerKeys[index]; - final emojis = search.value.isEmpty - ? category.emojis - : category.emojis - .where( - (emoji) => - emoji.aliases.join().contains( - search.value, - ) || - emoji.description.contains( - search.value, - ) || - emoji.tags.join().contains( - search.value, - ), - ) - .toIList(); + itemCount: combined.length, + itemBuilder: (context, index) { + final category = combined[index]; + final emojis = search.value.isEmpty + ? category.emojis + : category.emojis + .where( + (emoji) => + emoji.aliases.join().contains( + search.value, + ) || + emoji.description.contains( + search.value, + ) || + emoji.tags.join().contains(search.value), + ) + .toIList(); - if (emojis.isEmpty) { - return SliverToBoxAdapter( - child: SizedBox.shrink(key: headerKey), - ); - } + if (emojis.isEmpty) return SizedBox.shrink(); - return SliverMainAxisGroup( - slivers: [ - SliverToBoxAdapter( - child: Text( - key: headerKey, - category.name, - style: Theme.of(context) - .textTheme - .titleMedium, - ), - ), - SliverGrid.builder( - gridDelegate: - const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 56, - mainAxisExtent: 56, - ), - itemCount: emojis.length, - itemBuilder: (context, index) { - final emoji = emojis[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.name, + style: Theme.of(context).textTheme.titleMedium, + ), + GridView.builder( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 56, + mainAxisExtent: 56, + ), + itemCount: emojis.length, + shrinkWrap: true, + controller: scrollController, + itemBuilder: (context, index) { + final emoji = emojis[index]; - return IconButton( - padding: EdgeInsets.zero, - onPressed: () => - onSelection(emoji.value), - icon: SizedBox.square( - dimension: 36, - child: FittedBox(child: emoji.widget), - ), - ); - }, - ), - SliverToBoxAdapter( - child: SizedBox(height: 4), - ), - ], + return IconButton( + padding: EdgeInsets.zero, + onPressed: () => onSelection(emoji.value), + icon: SizedBox.square( + dimension: 36, + child: FittedBox(child: emoji.widget), + ), ); }, ), - ) - .toList(), + SizedBox(height: 4), + ], + ); + }, ), ), ], diff --git a/pubspec.lock b/pubspec.lock index 0b0d9e3..2bf7a40 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -581,6 +581,14 @@ 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: diff --git a/pubspec.yaml b/pubspec.yaml index 10845a2..99398c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: material_emoji_picker description: A generic but configurable emoji picker for Flutter -version: 1.1.1+2 +version: 1.0.3 repository: https://git.federated.nexus/Henry-Hiles/material_emoji_picker issue_tracker: https://git.federated.nexus/Henry-Hiles/material_emoji_picker/issues @@ -25,6 +25,7 @@ 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: