Merge pull request #376 from gaganyadav80/emoji_picker

feat: add emoji picker
This commit is contained in:
Nathan.fooo 2022-02-24 17:18:17 +08:00 committed by GitHub
commit 959b2db9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 4343 additions and 0 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,12.941176%,12.941176%);fill-opacity:1;" d="M 8 1.332031 C 11.683594 1.332031 14.667969 4.316406 14.667969 8 C 14.667969 11.683594 11.683594 14.667969 8 14.667969 C 4.316406 14.667969 1.332031 11.683594 1.332031 8 C 1.332031 4.316406 4.316406 1.332031 8 1.332031 Z M 8 2.332031 C 4.871094 2.332031 2.332031 4.871094 2.332031 8 C 2.332031 11.128906 4.871094 13.667969 8 13.667969 C 11.128906 13.667969 13.667969 11.128906 13.667969 8 C 13.667969 4.871094 11.128906 2.332031 8 2.332031 Z M 5.640625 9.855469 C 6.207031 10.574219 7.066406 11 8 11 C 8.929688 11 9.789062 10.574219 10.355469 9.859375 C 10.527344 9.640625 10.84375 9.605469 11.058594 9.777344 C 11.277344 9.949219 11.3125 10.261719 11.140625 10.476562 C 10.386719 11.433594 9.238281 12 8 12 C 6.757812 12 5.609375 11.429688 4.855469 10.476562 C 4.683594 10.257812 4.722656 9.945312 4.9375 9.773438 C 5.15625 9.601562 5.46875 9.640625 5.640625 9.855469 Z M 6 5.835938 C 6.460938 5.835938 6.832031 6.207031 6.832031 6.667969 C 6.832031 7.125 6.460938 7.5 6 7.5 C 5.539062 7.5 5.167969 7.125 5.167969 6.667969 C 5.167969 6.207031 5.539062 5.835938 6 5.835938 Z M 10 5.835938 C 10.460938 5.835938 10.832031 6.207031 10.832031 6.667969 C 10.832031 7.125 10.460938 7.5 10 7.5 C 9.539062 7.5 9.167969 7.125 9.167969 6.667969 C 9.167969 6.207031 9.539062 5.835938 10 5.835938 Z M 10 5.835938 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,12.941176%,12.941176%);fill-opacity:1;" d="M 11.667969 8 C 13.691406 8 15.332031 9.640625 15.332031 11.667969 C 15.332031 13.691406 13.691406 15.332031 11.667969 15.332031 C 9.640625 15.332031 8 13.691406 8 11.667969 C 8 9.640625 9.640625 8 11.667969 8 Z M 8 1.332031 C 11.683594 1.332031 14.667969 4.316406 14.667969 8 C 14.667969 8.175781 14.660156 8.351562 14.648438 8.523438 C 14.355469 8.246094 14.023438 8.007812 13.664062 7.820312 C 13.570312 4.773438 11.070312 2.332031 8 2.332031 C 4.871094 2.332031 2.332031 4.871094 2.332031 8 C 2.332031 11.070312 4.773438 13.570312 7.820312 13.664062 C 8.007812 14.027344 8.246094 14.355469 8.523438 14.648438 C 8.351562 14.660156 8.175781 14.667969 8 14.667969 C 4.316406 14.667969 1.332031 11.683594 1.332031 8 C 1.332031 4.316406 4.316406 1.332031 8 1.332031 Z M 11.667969 9.332031 L 11.605469 9.339844 C 11.46875 9.363281 11.363281 9.46875 11.339844 9.605469 L 11.332031 9.667969 L 11.332031 11.332031 L 9.664062 11.332031 L 9.605469 11.339844 C 9.46875 11.363281 9.363281 11.46875 9.335938 11.605469 L 9.332031 11.667969 L 9.335938 11.726562 C 9.363281 11.863281 9.46875 11.96875 9.605469 11.996094 L 9.664062 12 L 11.332031 12 L 11.335938 13.667969 L 11.339844 13.730469 C 11.363281 13.863281 11.472656 13.972656 11.609375 13.996094 L 11.667969 14.003906 L 11.726562 13.996094 C 11.863281 13.972656 11.96875 13.863281 11.996094 13.730469 L 12 13.667969 L 12 12 L 13.667969 12 L 13.730469 11.996094 C 13.867188 11.96875 13.972656 11.863281 13.996094 11.726562 L 14.003906 11.667969 L 13.996094 11.605469 C 13.972656 11.46875 13.867188 11.363281 13.730469 11.339844 L 13.667969 11.332031 L 12 11.332031 L 12 9.667969 L 11.996094 9.605469 C 11.96875 9.46875 11.863281 9.363281 11.726562 9.339844 Z M 5.640625 9.855469 C 6.082031 10.414062 6.703125 10.796875 7.394531 10.941406 C 7.355469 11.175781 7.332031 11.417969 7.332031 11.667969 C 7.332031 11.761719 7.335938 11.855469 7.34375 11.945312 C 6.359375 11.785156 5.472656 11.261719 4.855469 10.476562 C 4.683594 10.257812 4.722656 9.945312 4.9375 9.773438 C 5.15625 9.601562 5.46875 9.640625 5.640625 9.855469 Z M 6 5.835938 C 6.460938 5.835938 6.832031 6.207031 6.832031 6.667969 C 6.832031 7.125 6.460938 7.5 6 7.5 C 5.539062 7.5 5.167969 7.125 5.167969 6.667969 C 5.167969 6.207031 5.539062 5.835938 6 5.835938 Z M 10 5.835938 C 10.460938 5.835938 10.832031 6.207031 10.832031 6.667969 C 10.832031 7.125 10.460938 7.5 10 7.5 C 9.539062 7.5 9.167969 7.125 9.167969 6.667969 C 9.167969 6.207031 9.539062 5.835938 10 5.835938 Z M 10 5.835938 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math';
import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/history_button.dart';
import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter/material.dart';
@ -151,6 +152,11 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
controller: controller,
iconSize: toolbarIconSize,
),
FlowyEmojiStyleButton(
normalIcon: 'editor/insert_emoticon',
controller: controller,
tooltipText: "Emoji Picker",
),
],
);
}

View File

@ -0,0 +1,5 @@
export 'src/config.dart';
export 'src/models/emoji_model.dart';
export 'src/emoji_picker.dart';
export 'src/emoji_picker_builder.dart';
export 'src/emoji_button.dart';

View File

@ -0,0 +1,164 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'models/category_models.dart';
import 'emoji_picker.dart';
/// Config for customizations
class Config {
/// Constructor
const Config(
{this.columns = 7,
this.emojiSizeMax = 32.0,
this.verticalSpacing = 0,
this.horizontalSpacing = 0,
this.initCategory = Category.RECENT,
this.bgColor = const Color(0xFFEBEFF2),
this.indicatorColor = Colors.blue,
this.iconColor = Colors.grey,
this.iconColorSelected = Colors.blue,
this.progressIndicatorColor = Colors.blue,
this.backspaceColor = Colors.blue,
this.showRecentsTab = true,
this.recentsLimit = 28,
this.noRecentsText = 'No Recents',
this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26),
this.tabIndicatorAnimDuration = kTabScrollDuration,
this.categoryIcons = const CategoryIcons(),
this.buttonMode = ButtonMode.MATERIAL});
/// Number of emojis per row
final int columns;
/// Width and height the emoji will be maximal displayed
/// Can be smaller due to screen size and amount of columns
final double emojiSizeMax;
/// Verical spacing between emojis
final double verticalSpacing;
/// Horizontal spacing between emojis
final double horizontalSpacing;
/// The initial [Category] that will be selected
/// This [Category] will have its button in the bottombar darkened
final Category initCategory;
/// The background color of the Widget
final Color bgColor;
/// The color of the category indicator
final Color indicatorColor;
/// The color of the category icons
final Color iconColor;
/// The color of the category icon when selected
final Color iconColorSelected;
/// The color of the loading indicator during initalization
final Color progressIndicatorColor;
/// The color of the backspace icon button
final Color backspaceColor;
/// Show extra tab with recently used emoji
final bool showRecentsTab;
/// Limit of recently used emoji that will be saved
final int recentsLimit;
/// The text to be displayed if no recent emojis to display
final String noRecentsText;
/// The text style for [noRecentsText]
final TextStyle noRecentsStyle;
/// Duration of tab indicator to animate to next category
final Duration tabIndicatorAnimDuration;
/// Determines the icon to display for each [Category]
final CategoryIcons categoryIcons;
/// Change between Material and Cupertino button style
final ButtonMode buttonMode;
/// Get Emoji size based on properties and screen width
double getEmojiSize(double width) {
final maxSize = width / columns;
return min(maxSize, emojiSizeMax);
}
/// Returns the icon for the category
IconData getIconForCategory(Category category) {
switch (category) {
case Category.RECENT:
return categoryIcons.recentIcon;
case Category.SMILEYS:
return categoryIcons.smileyIcon;
case Category.ANIMALS:
return categoryIcons.animalIcon;
case Category.FOODS:
return categoryIcons.foodIcon;
case Category.TRAVEL:
return categoryIcons.travelIcon;
case Category.ACTIVITIES:
return categoryIcons.activityIcon;
case Category.OBJECTS:
return categoryIcons.objectIcon;
case Category.SYMBOLS:
return categoryIcons.symbolIcon;
case Category.FLAGS:
return categoryIcons.flagIcon;
case Category.SEARCH:
return categoryIcons.searchIcon;
default:
throw Exception('Unsupported Category');
}
}
@override
bool operator ==(other) {
return (other is Config) &&
other.columns == columns &&
other.emojiSizeMax == emojiSizeMax &&
other.verticalSpacing == verticalSpacing &&
other.horizontalSpacing == horizontalSpacing &&
other.initCategory == initCategory &&
other.bgColor == bgColor &&
other.indicatorColor == indicatorColor &&
other.iconColor == iconColor &&
other.iconColorSelected == iconColorSelected &&
other.progressIndicatorColor == progressIndicatorColor &&
other.backspaceColor == backspaceColor &&
other.showRecentsTab == showRecentsTab &&
other.recentsLimit == recentsLimit &&
other.noRecentsText == noRecentsText &&
other.noRecentsStyle == noRecentsStyle &&
other.tabIndicatorAnimDuration == tabIndicatorAnimDuration &&
other.categoryIcons == categoryIcons &&
other.buttonMode == buttonMode;
}
@override
int get hashCode =>
columns.hashCode ^
emojiSizeMax.hashCode ^
verticalSpacing.hashCode ^
horizontalSpacing.hashCode ^
initCategory.hashCode ^
bgColor.hashCode ^
indicatorColor.hashCode ^
iconColor.hashCode ^
iconColorSelected.hashCode ^
progressIndicatorColor.hashCode ^
backspaceColor.hashCode ^
showRecentsTab.hashCode ^
recentsLimit.hashCode ^
noRecentsText.hashCode ^
noRecentsStyle.hashCode ^
tabIndicatorAnimDuration.hashCode ^
categoryIcons.hashCode ^
buttonMode.hashCode;
}

View File

@ -0,0 +1,276 @@
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'models/category_models.dart';
import 'config.dart';
import 'models/emoji_model.dart';
import 'emoji_picker.dart';
import 'emoji_picker_builder.dart';
import 'emoji_view_state.dart';
class DefaultEmojiPickerView extends EmojiPickerBuilder {
const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) : super(config, state, key: key);
@override
_DefaultEmojiPickerViewState createState() => _DefaultEmojiPickerViewState();
}
class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView> with TickerProviderStateMixin {
PageController? _pageController;
TabController? _tabController;
final TextEditingController _emojiController = TextEditingController();
final FocusNode _emojiFocusNode = FocusNode();
final CategoryEmoji _categoryEmoji = CategoryEmoji(Category.SEARCH, List.empty(growable: true));
CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, <Emoji>[]);
@override
void initState() {
var initCategory =
widget.state.categoryEmoji.indexWhere((element) => element.category == widget.config.initCategory);
if (initCategory == -1) {
initCategory = 0;
}
_tabController = TabController(initialIndex: initCategory, length: widget.state.categoryEmoji.length, vsync: this);
_pageController = PageController(initialPage: initCategory);
_emojiFocusNode.requestFocus();
_emojiController.addListener(() {
String query = _emojiController.text.toLowerCase();
if (query.isEmpty) {
searchEmojiList.emoji.clear();
_pageController!.jumpToPage(
_tabController!.index,
);
} else {
searchEmojiList.emoji.clear();
for (var element in widget.state.categoryEmoji) {
searchEmojiList.emoji.addAll(
element.emoji.where((item) {
return item.name.toLowerCase().contains(query);
}).toList(),
);
}
}
setState(() {});
});
super.initState();
}
@override
void dispose() {
_emojiController.dispose();
_emojiFocusNode.dispose();
super.dispose();
}
Widget _buildBackspaceButton() {
if (widget.state.onBackspacePressed != null) {
return Material(
type: MaterialType.transparency,
child: IconButton(
padding: const EdgeInsets.only(bottom: 2),
icon: Icon(
Icons.backspace,
color: widget.config.backspaceColor,
),
onPressed: () {
widget.state.onBackspacePressed!();
}),
);
}
return Container();
}
bool isEmojiSearching() {
bool result = searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty;
return result;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
return Container(
color: widget.config.bgColor,
padding: const EdgeInsets.all(5.0),
child: Column(
children: [
SizedBox(
height: 25.0,
child: TextField(
controller: _emojiController,
focusNode: _emojiFocusNode,
autofocus: true,
style: const TextStyle(fontSize: 14.0),
cursorWidth: 1.0,
cursorColor: Colors.black,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 5.0),
hintText: "Search emoji",
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4.0),
borderSide: const BorderSide(),
gapPadding: 0.0,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4.0),
borderSide: const BorderSide(),
gapPadding: 0.0,
),
filled: true,
fillColor: Colors.white,
hoverColor: Colors.white,
),
),
),
Row(
children: [
Expanded(
child: TabBar(
labelColor: widget.config.iconColorSelected,
unselectedLabelColor: widget.config.iconColor,
controller: isEmojiSearching() ? TabController(length: 1, vsync: this) : _tabController,
labelPadding: EdgeInsets.zero,
indicatorColor: widget.config.indicatorColor,
padding: const EdgeInsets.symmetric(vertical: 5.0),
indicator: BoxDecoration(
border: Border.all(color: Colors.transparent),
borderRadius: BorderRadius.circular(4.0),
color: Colors.grey.withOpacity(0.5),
),
onTap: (index) {
_pageController!.animateToPage(
index,
duration: widget.config.tabIndicatorAnimDuration,
curve: Curves.ease,
);
},
tabs: isEmojiSearching()
? [_buildCategory(Category.SEARCH, emojiSize)]
: widget.state.categoryEmoji
.asMap()
.entries
.map<Widget>((item) => _buildCategory(item.value.category, emojiSize))
.toList(),
),
),
_buildBackspaceButton(),
],
),
Flexible(
child: PageView.builder(
itemCount: searchEmojiList.emoji.isNotEmpty ? 1 : widget.state.categoryEmoji.length,
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
// onPageChanged: (index) {
// _tabController!.animateTo(
// index,
// duration: widget.config.tabIndicatorAnimDuration,
// );
// },
itemBuilder: (context, index) {
CategoryEmoji catEmoji = isEmojiSearching() ? searchEmojiList : widget.state.categoryEmoji[index];
return _buildPage(emojiSize, catEmoji);
},
),
),
],
),
);
},
);
}
Widget _buildCategory(Category category, double categorySize) {
return Tab(
height: categorySize,
child: Icon(
widget.config.getIconForCategory(category),
size: categorySize / 1.3,
),
);
}
Widget _buildButtonWidget({required VoidCallback onPressed, required Widget child}) {
if (widget.config.buttonMode == ButtonMode.MATERIAL) {
return TextButton(
onPressed: onPressed,
child: child,
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)),
);
}
return CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child);
}
Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
// Display notice if recent has no entries yet
final scrollController = ScrollController();
if (categoryEmoji.category == Category.RECENT && categoryEmoji.emoji.isEmpty) {
return _buildNoRecent();
} else if (categoryEmoji.category == Category.SEARCH && categoryEmoji.emoji.isEmpty) {
return const Center(child: Text("No Emoji Found"));
}
// Build page normally
return ScrollbarListStack(
axis: Axis.vertical,
controller: scrollController,
barSize: 4.0,
scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0),
handleColor: const Color(0xffDFE0E0),
trackColor: const Color(0xffDFE0E0),
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: GridView.count(
scrollDirection: Axis.vertical,
physics: const ScrollPhysics(),
controller: scrollController,
shrinkWrap: true,
// primary: true,
padding: const EdgeInsets.all(0),
crossAxisCount: widget.config.columns,
mainAxisSpacing: widget.config.verticalSpacing,
crossAxisSpacing: widget.config.horizontalSpacing,
children: _categoryEmoji.emoji.isNotEmpty
? _categoryEmoji.emoji.map<Widget>((e) => _buildEmoji(emojiSize, categoryEmoji, e)).toList()
: categoryEmoji.emoji.map<Widget>((item) => _buildEmoji(emojiSize, categoryEmoji, item)).toList(),
),
),
);
}
Widget _buildEmoji(
double emojiSize,
CategoryEmoji categoryEmoji,
Emoji emoji,
) {
return _buildButtonWidget(
onPressed: () {
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
},
child: FittedBox(
fit: BoxFit.fill,
child: Text(
emoji.emoji,
textScaleFactor: 1.0,
style: TextStyle(
fontSize: emojiSize,
backgroundColor: Colors.transparent,
),
),
));
}
Widget _buildNoRecent() {
return Center(
child: Text(
widget.config.noRecentsText,
style: widget.config.noRecentsStyle,
textAlign: TextAlign.center,
));
}
}

View File

@ -0,0 +1,174 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/toolbar_icon_button.dart';
import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
class FlowyEmojiStyleButton extends StatefulWidget {
// final Attribute attribute;
final String normalIcon;
final double iconSize;
final QuillController controller;
final String tooltipText;
const FlowyEmojiStyleButton({
// required this.attribute,
required this.normalIcon,
required this.controller,
required this.tooltipText,
this.iconSize = defaultIconSize,
Key? key,
}) : super(key: key);
@override
_EmojiStyleButtonState createState() => _EmojiStyleButtonState();
}
class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
bool _isToggled = false;
// Style get _selectionStyle => widget.controller.getSelectionStyle();
final GlobalKey emojiButtonKey = GlobalKey();
OverlayEntry _entry = OverlayEntry(builder: (context) => Container());
// final FocusNode _keyFocusNode = FocusNode();
@override
void initState() {
super.initState();
// _isToggled = _getIsToggled(_selectionStyle.attributes);
// widget.controller.addListener(_didChangeEditingValue);
}
@override
Widget build(BuildContext context) {
// debugPrint(MediaQuery.of(context).size.width.toString());
// debugPrint(MediaQuery.of(context).size.height.toString());
return ToolbarIconButton(
key: emojiButtonKey,
onPressed: _toggleAttribute,
width: widget.iconSize * kIconButtonFactor,
isToggled: _isToggled,
iconName: widget.normalIcon,
tooltipText: widget.tooltipText,
);
}
// @override
// void didUpdateWidget(covariant FlowyEmojiStyleButton oldWidget) {
// super.didUpdateWidget(oldWidget);
// if (oldWidget.controller != widget.controller) {
// oldWidget.controller.removeListener(_didChangeEditingValue);
// widget.controller.addListener(_didChangeEditingValue);
// _isToggled = _getIsToggled(_selectionStyle.attributes);
// }
// }
// @override
// void dispose() {
// widget.controller.removeListener(_didChangeEditingValue);
// super.dispose();
// }
// void _didChangeEditingValue() {
// setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
// }
// bool _getIsToggled(Map<String, Attribute> attrs) {
// return _entry.mounted;
// }
void _toggleAttribute() {
if (_entry.mounted) {
_entry.remove();
setState(() => _isToggled = false);
} else {
RenderBox box = emojiButtonKey.currentContext?.findRenderObject() as RenderBox;
Offset position = box.localToGlobal(Offset.zero);
// final window = await getWindowInfo();
_entry = OverlayEntry(
builder: (BuildContext context) => BuildEmojiPickerView(
controller: widget.controller,
offset: position,
),
);
Overlay.of(context)!.insert(_entry);
setState(() => _isToggled = true);
}
//TODO @gaganyadav80: INFO: throws error when using TextField with FlowyOverlay.
// FlowyOverlay.of(context).insertWithRect(
// widget: BuildEmojiPickerView(controller: widget.controller),
// identifier: 'overlay_emoji_picker',
// anchorPosition: Offset(position.dx + 40, position.dy - 10),
// anchorSize: window.frame.size,
// anchorDirection: AnchorDirection.topLeft,
// style: FlowyOverlayStyle(blur: true),
// );
}
}
class BuildEmojiPickerView extends StatefulWidget {
const BuildEmojiPickerView({Key? key, required this.controller, this.offset}) : super(key: key);
final QuillController controller;
final Offset? offset;
@override
State<BuildEmojiPickerView> createState() => _BuildEmojiPickerViewState();
}
class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
//TODO @gaganyadav80: Not sure about the calculated position.
top: widget.offset!.dy - MediaQuery.of(context).size.height / 2.83 - 30,
left: widget.offset!.dx - MediaQuery.of(context).size.width / 3.92 + 40,
child: Material(
borderRadius: BorderRadius.circular(8.0),
child: SizedBox(
//TODO @gaganyadav80: FIXIT: Gets too large when fullscreen.
height: MediaQuery.of(context).size.height / 2.83 + 20,
width: MediaQuery.of(context).size.width / 3.92,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: EmojiPicker(
onEmojiSelected: (category, emoji) => insertEmoji(emoji),
config: const Config(
columns: 8,
emojiSizeMax: 28,
bgColor: Color(0xffF2F2F2),
iconColor: Colors.grey,
iconColorSelected: Color(0xff333333),
indicatorColor: Color(0xff333333),
progressIndicatorColor: Color(0xff333333),
buttonMode: ButtonMode.CUPERTINO,
initCategory: Category.RECENT,
),
),
),
),
),
),
],
);
}
void insertEmoji(Emoji emoji) {
final baseOffset = widget.controller.selection.baseOffset;
final extentOffset = widget.controller.selection.extentOffset;
final replaceLen = extentOffset - baseOffset;
final selection = widget.controller.selection.copyWith(
baseOffset: baseOffset + emoji.emoji.length,
extentOffset: baseOffset + emoji.emoji.length,
);
widget.controller.replaceText(baseOffset, replaceLen, emoji.emoji, selection);
}
}

View File

@ -0,0 +1,292 @@
// ignore_for_file: constant_identifier_names
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'models/category_models.dart';
import 'config.dart';
import 'default_emoji_picker_view.dart';
import 'models/emoji_model.dart';
import 'emoji_lists.dart' as emoji_list;
import 'emoji_view_state.dart';
import 'models/recent_emoji_model.dart';
/// All the possible categories that [Emoji] can be put into
///
/// All [Category] are shown in the category bar
enum Category {
/// Searched emojis
SEARCH,
/// Recent emojis
RECENT,
/// Smiley emojis
SMILEYS,
/// Animal emojis
ANIMALS,
/// Food emojis
FOODS,
/// Activity emojis
ACTIVITIES,
/// Travel emojis
TRAVEL,
/// Ojects emojis
OBJECTS,
/// Sumbol emojis
SYMBOLS,
/// Flag emojis
FLAGS,
}
/// Enum to alter the keyboard button style
enum ButtonMode {
/// Android button style - gives the button a splash color with ripple effect
MATERIAL,
/// iOS button style - gives the button a fade out effect when pressed
CUPERTINO
}
/// Callback function for when emoji is selected
///
/// The function returns the selected [Emoji] as well
/// as the [Category] from which it originated
typedef OnEmojiSelected = void Function(Category category, Emoji emoji);
/// Callback function for backspace button
typedef OnBackspacePressed = void Function();
/// Callback function for custom view
typedef EmojiViewBuilder = Widget Function(Config config, EmojiViewState state);
/// The Emoji Keyboard widget
///
/// This widget displays a grid of [Emoji] sorted by [Category]
/// which the user can horizontally scroll through.
///
/// There is also a bottombar which displays all the possible [Category]
/// and allow the user to quickly switch to that [Category]
class EmojiPicker extends StatefulWidget {
/// EmojiPicker for flutter
const EmojiPicker({
Key? key,
required this.onEmojiSelected,
this.onBackspacePressed,
this.config = const Config(),
this.customWidget,
}) : super(key: key);
/// Custom widget
final EmojiViewBuilder? customWidget;
/// The function called when the emoji is selected
final OnEmojiSelected onEmojiSelected;
/// The function called when backspace button is pressed
final OnBackspacePressed? onBackspacePressed;
/// Config for customizations
final Config config;
@override
_EmojiPickerState createState() => _EmojiPickerState();
}
class _EmojiPickerState extends State<EmojiPicker> {
static const platform = MethodChannel('emoji_picker_flutter');
List<CategoryEmoji> categoryEmoji = List.empty(growable: true);
List<RecentEmoji> recentEmoji = List.empty(growable: true);
late Future<void> updateEmojiFuture;
// Prevent emojis to be reloaded with every build
bool loaded = false;
@override
void initState() {
super.initState();
updateEmojiFuture = _updateEmojis();
}
@override
void didUpdateWidget(covariant EmojiPicker oldWidget) {
if (oldWidget.config != widget.config) {
// Config changed - rebuild EmojiPickerView completely
loaded = false;
updateEmojiFuture = _updateEmojis();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
if (!loaded) {
// Load emojis
updateEmojiFuture.then(
(value) => WidgetsBinding.instance!.addPostFrameCallback((_) {
if (!mounted) return;
setState(() {
loaded = true;
});
}),
);
// Show loading indicator
return const Center(child: CircularProgressIndicator());
}
if (widget.config.showRecentsTab) {
categoryEmoji[0].emoji = recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
}
var state = EmojiViewState(
categoryEmoji,
_getOnEmojiListener(),
widget.onBackspacePressed,
);
// Build
return widget.customWidget == null
? DefaultEmojiPickerView(widget.config, state)
: widget.customWidget!(widget.config, state);
}
// Add recent emoji handling to tap listener
OnEmojiSelected _getOnEmojiListener() {
return (category, emoji) {
if (widget.config.showRecentsTab) {
_addEmojiToRecentlyUsed(emoji).then((value) {
if (category != Category.RECENT && mounted) {
setState(() {
// rebuild to update recent emoji tab
// when it is not current tab
});
}
});
}
widget.onEmojiSelected(category, emoji);
};
}
// Initalize emoji data
Future<void> _updateEmojis() async {
categoryEmoji.clear();
if (widget.config.showRecentsTab) {
recentEmoji = await _getRecentEmojis();
final List<Emoji> recentEmojiMap = recentEmoji.map((e) => e.emoji).toList().cast<Emoji>();
categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
}
categoryEmoji.addAll([
CategoryEmoji(Category.SMILEYS, await _getAvailableEmojis(emoji_list.smileys, title: 'smileys')),
CategoryEmoji(Category.ANIMALS, await _getAvailableEmojis(emoji_list.animals, title: 'animals')),
CategoryEmoji(Category.FOODS, await _getAvailableEmojis(emoji_list.foods, title: 'foods')),
CategoryEmoji(Category.ACTIVITIES, await _getAvailableEmojis(emoji_list.activities, title: 'activities')),
CategoryEmoji(Category.TRAVEL, await _getAvailableEmojis(emoji_list.travel, title: 'travel')),
CategoryEmoji(Category.OBJECTS, await _getAvailableEmojis(emoji_list.objects, title: 'objects')),
CategoryEmoji(Category.SYMBOLS, await _getAvailableEmojis(emoji_list.symbols, title: 'symbols')),
CategoryEmoji(Category.FLAGS, await _getAvailableEmojis(emoji_list.flags, title: 'flags'))
]);
}
// Get available emoji for given category title
Future<List<Emoji>> _getAvailableEmojis(Map<String, String> map, {required String title}) async {
Map<String, String>? newMap;
// Get Emojis cached locally if available
newMap = await _restoreFilteredEmojis(title);
if (newMap == null) {
// Check if emoji is available on this platform
newMap = await _getPlatformAvailableEmoji(map);
// Save available Emojis to local storage for faster loading next time
if (newMap != null) {
await _cacheFilteredEmojis(title, newMap);
}
}
// Map to Emoji Object
return newMap!.entries.map<Emoji>((entry) => Emoji(entry.key, entry.value)).toList();
}
// Check if emoji is available on current platform
Future<Map<String, String>?> _getPlatformAvailableEmoji(Map<String, String> emoji) async {
if (Platform.isAndroid) {
Map<String, String>? filtered = {};
var delimiter = '|';
try {
var entries = emoji.values.join(delimiter);
var keys = emoji.keys.join(delimiter);
var result = (await platform
.invokeMethod<String>('checkAvailability', {'emojiKeys': keys, 'emojiEntries': entries})) as String;
var resultKeys = result.split(delimiter);
for (var i = 0; i < resultKeys.length; i++) {
filtered[resultKeys[i]] = emoji[resultKeys[i]]!;
}
} on PlatformException catch (_) {
filtered = null;
}
return filtered;
} else {
return emoji;
}
}
// Restore locally cached emoji
Future<Map<String, String>?> _restoreFilteredEmojis(String title) async {
final prefs = await SharedPreferences.getInstance();
var emojiJson = prefs.getString(title);
if (emojiJson == null) {
return null;
}
var emojis = Map<String, String>.from(jsonDecode(emojiJson) as Map<String, dynamic>);
return emojis;
}
// Stores filtered emoji locally for faster access next time
Future<void> _cacheFilteredEmojis(String title, Map<String, String> emojis) async {
final prefs = await SharedPreferences.getInstance();
var emojiJson = jsonEncode(emojis);
prefs.setString(title, emojiJson);
}
// Returns list of recently used emoji from cache
Future<List<RecentEmoji>> _getRecentEmojis() async {
final prefs = await SharedPreferences.getInstance();
var emojiJson = prefs.getString('recent');
if (emojiJson == null) {
return [];
}
var json = jsonDecode(emojiJson) as List<dynamic>;
return json.map<RecentEmoji>(RecentEmoji.fromJson).toList();
}
// Add an emoji to recently used list or increase its counter
Future<void> _addEmojiToRecentlyUsed(Emoji emoji) async {
final prefs = await SharedPreferences.getInstance();
var recentEmojiIndex = recentEmoji.indexWhere((element) => element.emoji.emoji == emoji.emoji);
if (recentEmojiIndex != -1) {
// Already exist in recent list
// Just update counter
recentEmoji[recentEmojiIndex].counter++;
} else {
recentEmoji.add(RecentEmoji(emoji, 1));
}
// Sort by counter desc
recentEmoji.sort((a, b) => b.counter - a.counter);
// Limit entries to recentsLimit
recentEmoji = recentEmoji.sublist(0, min(widget.config.recentsLimit, recentEmoji.length));
// save locally
prefs.setString('recent', jsonEncode(recentEmoji));
}
}

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'config.dart';
import 'emoji_view_state.dart';
/// Template class for custom implementation
/// Inhert this class to create your own EmojiPicker
abstract class EmojiPickerBuilder extends StatefulWidget {
/// Constructor
const EmojiPickerBuilder(this.config, this.state, {Key? key}) : super(key: key);
/// Config for customizations
final Config config;
/// State that holds current emoji data
final EmojiViewState state;
}

View File

@ -0,0 +1,21 @@
import 'models/category_models.dart';
import 'emoji_picker.dart';
/// State that holds current emoji data
class EmojiViewState {
/// Constructor
EmojiViewState(
this.categoryEmoji,
this.onEmojiSelected,
this.onBackspacePressed,
);
/// List of all category including their emoji
final List<CategoryEmoji> categoryEmoji;
/// Callback when pressed on emoji
final OnEmojiSelected onEmojiSelected;
/// Callback when pressed on backspace
final OnBackspacePressed? onBackspacePressed;
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'emoji_model.dart';
import '../emoji_picker.dart';
/// Container for Category and their emoji
class CategoryEmoji {
/// Constructor
CategoryEmoji(this.category, this.emoji);
/// Category instance
final Category category;
/// List of emoji of this category
List<Emoji> emoji;
@override
String toString() {
return 'Name: $category, Emoji: $emoji';
}
}
/// Class that defines the icon representing a [Category]
class CategoryIcon {
/// Icon of Category
const CategoryIcon({
required this.icon,
this.color = const Color.fromRGBO(211, 211, 211, 1),
this.selectedColor = const Color.fromRGBO(178, 178, 178, 1),
});
/// The icon to represent the category
final IconData icon;
/// The default color of the icon
final Color color;
/// The color of the icon once the category is selected
final Color selectedColor;
}
/// Class used to define all the [CategoryIcon] shown for each [Category]
///
/// This allows the keyboard to be personalized by changing icons shown.
/// If a [CategoryIcon] is set as null or not defined during initialization,
/// the default icons will be used instead
class CategoryIcons {
/// Constructor
const CategoryIcons({
this.recentIcon = Icons.access_time,
this.smileyIcon = Icons.tag_faces,
this.animalIcon = Icons.pets,
this.foodIcon = Icons.fastfood,
this.activityIcon = Icons.directions_run,
this.travelIcon = Icons.location_city,
this.objectIcon = Icons.lightbulb_outline,
this.symbolIcon = Icons.emoji_symbols,
this.flagIcon = Icons.flag,
this.searchIcon = Icons.search,
});
/// Icon for [Category.RECENT]
final IconData recentIcon;
/// Icon for [Category.SMILEYS]
final IconData smileyIcon;
/// Icon for [Category.ANIMALS]
final IconData animalIcon;
/// Icon for [Category.FOODS]
final IconData foodIcon;
/// Icon for [Category.ACTIVITIES]
final IconData activityIcon;
/// Icon for [Category.TRAVEL]
final IconData travelIcon;
/// Icon for [Category.OBJECTS]
final IconData objectIcon;
/// Icon for [Category.SYMBOLS]
final IconData symbolIcon;
/// Icon for [Category.FLAGS]
final IconData flagIcon;
/// Icon for [Category.SEARCH]
final IconData searchIcon;
}

View File

@ -0,0 +1,32 @@
/// A class to store data for each individual emoji
class Emoji {
/// Emoji constructor
const Emoji(this.name, this.emoji);
/// The name or description for this emoji
final String name;
/// The unicode string for this emoji
///
/// This is the string that should be displayed to view the emoji
final String emoji;
@override
String toString() {
// return 'Name: $name, Emoji: $emoji';
return name;
}
/// Parse Emoji from json
static Emoji fromJson(Map<String, dynamic> json) {
return Emoji(json['name'] as String, json['emoji'] as String);
}
/// Encode Emoji to json
Map<String, dynamic> toJson() {
return {
'name': name,
'emoji': emoji,
};
}
}

View File

@ -0,0 +1,30 @@
import 'emoji_model.dart';
/// Class that holds an recent emoji
/// Recent Emoji has an instance of the emoji
/// And a counter, which counts how often this emoji
/// has been used before
class RecentEmoji {
/// Constructor
RecentEmoji(this.emoji, this.counter);
/// Emoji instance
final Emoji emoji;
/// Counter how often emoji has been used before
int counter = 0;
/// Parse RecentEmoji from json
static RecentEmoji fromJson(dynamic json) {
return RecentEmoji(
Emoji.fromJson(json['emoji'] as Map<String, dynamic>),
json['counter'] as int,
);
}
/// Encode RecentEmoji to json
Map<String, dynamic> toJson() => {
'emoji': emoji,
'counter': counter,
};
}