flutter聊天界面-自定义表情键盘实现
flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。
flutter开发基础腾讯IM的聊天应用,使用的是tencent_im_sdk_plugin插件。使用的是自定义表情。
一、使用的表情
1.1、自定义表情
这里使用自定义表情,表情列表如下
const emojiUrl = 'https://web.sdk.qcloud.com/im/assets/emoji/';
const emojiMap = {
'[NO]': 'emoji_0@2x.png',
'[OK]': 'emoji_1@2x.png',
'[下雨]': 'emoji_2@2x.png',
'[么么哒]': 'emoji_3@2x.png',
'[乒乓]': 'emoji_4@2x.png',
'[便便]': 'emoji_5@2x.png',
'[信封]': 'emoji_6@2x.png',
'[偷笑]': 'emoji_7@2x.png',
'[傲慢]': 'emoji_8@2x.png',
'[再见]': 'emoji_9@2x.png',
'[冷汗]': 'emoji_10@2x.png',
'[凋谢]': 'emoji_11@2x.png',
'[刀]': 'emoji_12@2x.png',
'[删除]': 'emoji_13@2x.png',
'[勾引]': 'emoji_14@2x.png',
'[发呆]': 'emoji_15@2x.png',
'[发抖]': 'emoji_16@2x.png',
'[可怜]': 'emoji_17@2x.png',
'[可爱]': 'emoji_18@2x.png',
'[右哼哼]': 'emoji_19@2x.png',
'[右太极]': 'emoji_20@2x.png',
'[右车头]': 'emoji_21@2x.png',
'[吐]': 'emoji_22@2x.png',
'[吓]': 'emoji_23@2x.png',
'[咒骂]': 'emoji_24@2x.png',
'[咖啡]': 'emoji_25@2x.png',
'[啤酒]': 'emoji_26@2x.png',
'[嘘]': 'emoji_27@2x.png',
'[回头]': 'emoji_28@2x.png',
'[困]': 'emoji_29@2x.png',
'[坏笑]': 'emoji_30@2x.png',
'[多云]': 'emoji_31@2x.png',
'[大兵]': 'emoji_32@2x.png',
'[大哭]': 'emoji_33@2x.png',
'[太阳]': 'emoji_34@2x.png',
'[奋斗]': 'emoji_35@2x.png',
'[奶瓶]': 'emoji_36@2x.png',
'[委屈]': 'emoji_37@2x.png',
'[害羞]': 'emoji_38@2x.png',
'[尴尬]': 'emoji_39@2x.png',
'[左哼哼]': 'emoji_40@2x.png',
'[左太极]': 'emoji_41@2x.png',
'[左车头]': 'emoji_42@2x.png',
'[差劲]': 'emoji_43@2x.png',
'[弱]': 'emoji_44@2x.png',
'[强]': 'emoji_45@2x.png',
'[彩带]': 'emoji_46@2x.png',
'[彩球]': 'emoji_47@2x.png',
'[得意]': 'emoji_48@2x.png',
'[微笑]': 'emoji_49@2x.png',
'[心碎了]': 'emoji_50@2x.png',
'[快哭了]': 'emoji_51@2x.png',
'[怄火]': 'emoji_52@2x.png',
'[怒]': 'emoji_53@2x.png',
'[惊恐]': 'emoji_54@2x.png',
'[惊讶]': 'emoji_55@2x.png',
'[憨笑]': 'emoji_56@2x.png',
'[手枪]': 'emoji_57@2x.png',
'[打哈欠]': 'emoji_58@2x.png',
'[抓狂]': 'emoji_59@2x.png',
'[折磨]': 'emoji_60@2x.png',
'[抠鼻]': 'emoji_61@2x.png',
'[抱抱]': 'emoji_62@2x.png',
'[抱拳]': 'emoji_63@2x.png',
'[拳头]': 'emoji_64@2x.png',
'[挥手]': 'emoji_65@2x.png',
'[握手]': 'emoji_66@2x.png',
'[撇嘴]': 'emoji_67@2x.png',
'[擦汗]': 'emoji_68@2x.png',
'[敲打]': 'emoji_69@2x.png',
'[晕]': 'emoji_70@2x.png',
'[月亮]': 'emoji_71@2x.png',
'[棒棒糖]': 'emoji_72@2x.png',
'[汽车]': 'emoji_73@2x.png',
'[沙发]': 'emoji_74@2x.png',
'[流汗]': 'emoji_75@2x.png',
'[流泪]': 'emoji_76@2x.png',
'[激动]': 'emoji_77@2x.png',
'[灯泡]': 'emoji_78@2x.png',
'[炸弹]': 'emoji_79@2x.png',
'[熊猫]': 'emoji_80@2x.png',
'[爆筋]': 'emoji_81@2x.png',
'[爱你]': 'emoji_82@2x.png',
'[爱心]': 'emoji_83@2x.png',
'[爱情]': 'emoji_84@2x.png',
'[猪头]': 'emoji_85@2x.png',
'[猫咪]': 'emoji_86@2x.png',
'[献吻]': 'emoji_87@2x.png',
'[玫瑰]': 'emoji_88@2x.png',
'[瓢虫]': 'emoji_89@2x.png',
'[疑问]': 'emoji_90@2x.png',
'[白眼]': 'emoji_91@2x.png',
'[皮球]': 'emoji_92@2x.png',
'[睡觉]': 'emoji_93@2x.png',
'[磕头]': 'emoji_94@2x.png',
'[示爱]': 'emoji_95@2x.png',
'[礼品袋]': 'emoji_96@2x.png',
'[礼物]': 'emoji_97@2x.png',
'[篮球]': 'emoji_98@2x.png',
'[米饭]': 'emoji_99@2x.png',
'[糗大了]': 'emoji_100@2x.png',
'[红双喜]': 'emoji_101@2x.png',
'[红灯笼]': 'emoji_102@2x.png',
'[纸巾]': 'emoji_103@2x.png',
'[胜利]': 'emoji_104@2x.png',
'[色]': 'emoji_105@2x.png',
'[药]': 'emoji_106@2x.png',
'[菜刀]': 'emoji_107@2x.png',
'[蛋糕]': 'emoji_108@2x.png',
'[蜡烛]': 'emoji_109@2x.png',
'[街舞]': 'emoji_110@2x.png',
'[衰]': 'emoji_111@2x.png',
'[西瓜]': 'emoji_112@2x.png',
'[调皮]': 'emoji_113@2x.png',
'[象棋]': 'emoji_114@2x.png',
'[跳绳]': 'emoji_115@2x.png',
'[跳跳]': 'emoji_116@2x.png',
'[车厢]': 'emoji_117@2x.png',
'[转圈]': 'emoji_118@2x.png',
'[鄙视]': 'emoji_119@2x.png',
'[酷]': 'emoji_120@2x.png',
'[钞票]': 'emoji_121@2x.png',
'[钻戒]': 'emoji_122@2x.png',
'[闪电]': 'emoji_123@2x.png',
'[闭嘴]': 'emoji_124@2x.png',
'[闹钟]': 'emoji_125@2x.png',
'[阴险]': 'emoji_126@2x.png',
'[难过]': 'emoji_127@2x.png',
'[雨伞]': 'emoji_128@2x.png',
'[青蛙]': 'emoji_129@2x.png',
'[面条]': 'emoji_130@2x.png',
'[鞭炮]': 'emoji_131@2x.png',
'[风车]': 'emoji_132@2x.png',
'[飞吻]': 'emoji_133@2x.png',
'[飞机]': 'emoji_134@2x.png',
'[饥饿]': 'emoji_135@2x.png',
'[香蕉]': 'emoji_136@2x.png',
'[骷髅]': 'emoji_137@2x.png',
'[麦克风]': 'emoji_138@2x.png',
'[麻将]': 'emoji_139@2x.png',
'[鼓掌]': 'emoji_140@2x.png',
'[龇牙]': 'emoji_141@2x.png',
};
const emojiName = [
'[龇牙]',
'[调皮]',
'[流汗]',
'[偷笑]',
'[再见]',
'[敲打]',
'[擦汗]',
'[猪头]',
'[玫瑰]',
'[流泪]',
'[大哭]',
'[嘘]',
'[酷]',
'[抓狂]',
'[委屈]',
'[便便]',
'[炸弹]',
'[菜刀]',
'[可爱]',
'[色]',
'[害羞]',
'[得意]',
'[吐]',
'[微笑]',
'[怒]',
'[尴尬]',
'[惊恐]',
'[冷汗]',
'[爱心]',
'[示爱]',
'[白眼]',
'[傲慢]',
'[难过]',
'[惊讶]',
'[疑问]',
'[困]',
'[么么哒]',
'[憨笑]',
'[爱情]',
'[衰]',
'[撇嘴]',
'[阴险]',
'[奋斗]',
'[发呆]',
'[右哼哼]',
'[抱抱]',
'[坏笑]',
'[飞吻]',
'[鄙视]',
'[晕]',
'[大兵]',
'[可怜]',
'[强]',
'[弱]',
'[握手]',
'[胜利]',
'[抱拳]',
'[凋谢]',
'[米饭]',
'[蛋糕]',
'[西瓜]',
'[啤酒]',
'[瓢虫]',
'[勾引]',
'[OK]',
'[爱你]',
'[咖啡]',
'[月亮]',
'[刀]',
'[发抖]',
'[差劲]',
'[拳头]',
'[心碎了]',
'[太阳]',
'[礼物]',
'[皮球]',
'[骷髅]',
'[挥手]',
'[闪电]',
'[饥饿]',
'[困]',
'[咒骂]',
'[折磨]',
'[抠鼻]',
'[鼓掌]',
'[糗大了]',
'[左哼哼]',
'[打哈欠]',
'[快哭了]',
'[吓]',
'[篮球]',
'[乒乓]',
'[NO]',
'[跳跳]',
'[怄火]',
'[转圈]',
'[磕头]',
'[回头]',
'[跳绳]',
'[激动]',
'[街舞]',
'[献吻]',
'[左太极]',
'[右太极]',
'[闭嘴]',
'[猫咪]',
'[红双喜]',
'[鞭炮]',
'[红灯笼]',
'[麻将]',
'[麦克风]',
'[礼品袋]',
'[信封]',
'[象棋]',
'[彩带]',
'[蜡烛]',
'[爆筋]',
'[棒棒糖]',
'[奶瓶]',
'[面条]',
'[香蕉]',
'[飞机]',
'[左车头]',
'[车厢]',
'[右车头]',
'[多云]',
'[下雨]',
'[钞票]',
'[熊猫]',
'[灯泡]',
'[风车]',
'[闹钟]',
'[雨伞]',
'[彩球]',
'[钻戒]',
'[沙发]',
'[纸巾]',
'[手枪]',
'[青蛙]',
];
1.2、定义自定义表情数据类
这里定义自定义表情的数据类 CommonChatEmoji
class CommonChatEmojiItem {
String? emojiName;
String? url;
CommonChatEmojiItem({required this.emojiName, required this.url});
}
class CommonChatEmoji {
static List<CommonChatEmojiItem> emojiUrlList() {
return emojiName
.map((item) => CommonChatEmojiItem(
emojiName: item, url: emojiUrl + emojiMap[item]!))
.toList();
}
static bool emojiIsContain(String emojiName) {
bool isContain = false;
CommonChatEmojiItem? emojiItem = CommonChatEmoji.findEmojiItem(emojiName);
if (emojiName.contains(emojiName) && emojiItem != null) {
isContain = true;
}
return isContain;
}
static CommonChatEmojiItem? findEmojiItem(String emojiName) {
List<CommonChatEmojiItem> emojiItemList = CommonChatEmoji.emojiUrlList();
CommonChatEmojiItem? emojiItem;
for(CommonChatEmojiItem item in emojiItemList) {
if (emojiName == item.emojiName) {
emojiItem = item;
break;
}
}
return emojiItem;
}
}
二、聊天表情键盘
2.1、实现表情排列Panel键盘
排列表情,使用的是GridView.builder,GridView网格布局是一种常见的布局类型,GridView 组件正是实现了网格布局的组件,
SliverGridDelegate是一个抽象类,定义了GridView Layout相关接口,子类需要通过实现它们来实现具体的布局算法。Flutter中提供了两个SliverGridDelegate的子类SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent,
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7, //每行三列
childAspectRatio: 1.0, //显示区域宽高相等
),
itemCount: CommonChatEmoji.emojiUrlList().length,
itemBuilder: (context, index) {
CommonChatEmojiItem emojiItem =
CommonChatEmoji.emojiUrlList()[index];
return ChatInputEmojiButton(
emojiItem: emojiItem,
size: itemSize,
onEmojiLongPressed: widget.onEmojiLongPressed,
onEmojiTapPressed: widget.onEmojiTapPressed,
);
},
padding: EdgeInsets.only(
bottom: deleteBarHeight,
),
),
排列效果如图所示
聊天界面的表情Panel的布局完整代码
// 表情输入
class ChatInputEmojiPanel extends StatefulWidget {
const ChatInputEmojiPanel({
Key? key,
required this.emojiPanelHeight,
required this.chatInputBarController,
required this.onTextFieldDelete,
required this.onEmojiTapPressed,
required this.onEmojiLongPressed,
required this.onTextFieldSend,
}) : super(key: key);
final double emojiPanelHeight;
final ChatInputBarController chatInputBarController;
final Function onTextFieldDelete;
final Function onTextFieldSend;
final Function(CommonChatEmojiItem emojiItem) onEmojiTapPressed;
final Function(CommonChatEmojiItem emojiItem, Offset globalPosition) onEmojiLongPressed;
State<ChatInputEmojiPanel> createState() => _ChatInputEmojiPanelState();
}
class _ChatInputEmojiPanelState extends State<ChatInputEmojiPanel> {
void initState() {
// TODO: implement initState
super.initState();
}
void dispose() {
// TODO: implement dispose
super.dispose();
}
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
int crossAxisCount = 7;
double itemSize = screenSize.width / crossAxisCount;
EdgeInsets viewPadding = MediaQuery.of(context).viewPadding;
double emojiCateBarHeight = 50.0 + viewPadding.bottom;
double deleteBarHeight = 50.0;
return Container(
width: screenSize.width,
height: widget.emojiPanelHeight,
decoration: BoxDecoration(
color: ColorUtil.hexColor(0xf7f7f7),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Stack(
children: [
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7, //每行三列
childAspectRatio: 1.0, //显示区域宽高相等
),
itemCount: CommonChatEmoji.emojiUrlList().length,
itemBuilder: (context, index) {
CommonChatEmojiItem emojiItem =
CommonChatEmoji.emojiUrlList()[index];
return ChatInputEmojiButton(
emojiItem: emojiItem,
size: itemSize,
onEmojiLongPressed: widget.onEmojiLongPressed,
onEmojiTapPressed: widget.onEmojiTapPressed,
);
},
padding: EdgeInsets.only(
bottom: deleteBarHeight,
),
),
Positioned(
bottom: 0.0,
right: 0.0,
child: ChatInputEmojiDeleteBar(
height: deleteBarHeight,
onTextFieldDelete: widget.onTextFieldDelete,
),
),
],
),
),
ChatInputEmojiCateBar(
height: emojiCateBarHeight,
onTextFieldSend: widget.onTextFieldSend,
),
],
),
);
}
}
// 显示表情Emoji图片
class ChatInputEmojiButton extends StatelessWidget {
const ChatInputEmojiButton({
Key? key,
required this.emojiItem,
required this.size,
required this.onEmojiTapPressed,
required this.onEmojiLongPressed,
}) : super(key: key);
final CommonChatEmojiItem emojiItem;
final double size;
final Function(CommonChatEmojiItem emojiItem) onEmojiTapPressed;
final Function(CommonChatEmojiItem emojiItem, Offset globalPosition) onEmojiLongPressed;
Widget build(BuildContext context) {
double iconSize = size;
if (iconSize > 36.0) {
iconSize = 36.0;
}
return ButtonWidget(
width: size,
height: size,
onLongPressStart: (LongPressStartDetails details) {
onEmojiLongPressed(emojiItem, details.globalPosition);
},
onPressed: () {
onEmojiTapPressed(emojiItem);
},
child: ImageHelper.imageNetwork(
imageUrl: "${emojiItem.url}",
fit: BoxFit.cover,
width: iconSize,
height: iconSize,
),
);
}
}
2.2、表情Panel的布局代码。
// 底部表情切换bar与发送按钮
class ChatInputEmojiCateBar extends StatefulWidget {
const ChatInputEmojiCateBar({
Key? key,
required this.height,
required this.onTextFieldSend,
}) : super(key: key);
final double height;
final Function onTextFieldSend;
State<ChatInputEmojiCateBar> createState() => _ChatInputEmojiCateBarState();
}
class _ChatInputEmojiCateBarState extends State<ChatInputEmojiCateBar> {
Widget build(BuildContext context) {
EdgeInsets viewPadding = MediaQuery.of(context).viewPadding;
Size screenSize = MediaQuery.of(context).size;
print("ChatInputEmojiCateBar viewPadding bottom:${viewPadding.bottom}");
return Container(
width: screenSize.width,
height: widget.height,
decoration: BoxDecoration(
color: ColorUtil.hexColor(0xf7f7f7),
border: Border(
bottom: BorderSide(width: 0.0, color: ColorUtil.hexColor(0xffffff)),
left: BorderSide(width: 0.0, color: ColorUtil.hexColor(0xffffff)),
right: BorderSide(width: 0.0, color: ColorUtil.hexColor(0xffffff)),
top: BorderSide(width: 1.0, color: ColorUtil.hexColor(0xf0f0f0)),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ButtonWidget(
margin: EdgeInsets.only(left: 10.0),
width: 36.0,
onPressed: () {},
child: ImageHelper.wrapAssetAtImages(
"icons/ic_custom_emoji_cate.png",
fit: BoxFit.cover,
width: 32.0,
height: 32.0,
),
),
Expanded(
child: Container(),
),
ButtonWidget(
margin: const EdgeInsets.only(left: 10.0),
width: 70.0,
bgColor: ColorUtil.hexColor(0xf7f7f7),
bgHighlightedColor: ColorUtil.hexColor(0x3b93ff, alpha: 0.35),
onPressed: () {
widget.onTextFieldSend();
},
child: Text(
"发送",
textAlign: TextAlign.center,
maxLines: 1000,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: ColorUtil.hexColor(0x3b93ff),
decoration: TextDecoration.none,
),
),
),
],
),
),
SizedBox(
height: viewPadding.bottom,
),
],
),
);
}
}
删除输入的表情的删除按钮
// 表情键盘底部发送及删除按钮
class ChatInputEmojiDeleteBar extends StatefulWidget {
const ChatInputEmojiDeleteBar({
Key? key,
required this.height,
required this.onTextFieldDelete,
}) : super(key: key);
final double height;
final Function onTextFieldDelete;
State<ChatInputEmojiDeleteBar> createState() =>
_ChatInputEmojiDeleteBarState();
}
class _ChatInputEmojiDeleteBarState extends State<ChatInputEmojiDeleteBar> {
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(right: 10.0),
color: Colors.transparent,
height: widget.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ButtonWidget(
onPressed: () {
widget.onTextFieldDelete();
},
child: ImageHelper.wrapAssetAtImages(
"icons/ic_backspace.png",
fit: BoxFit.cover,
width: 42.0,
height: 42.0,
),
),
],
),
);
}
}
2.3、长按预览表情
当我们使用常见的聊天工具的时候,表情基本上都有预览功能,这里实现长按预览表情功能。
预览表情效果如下
具体代码
/// 表情长按预览功能
class ChatInputEmojiPreview extends StatefulWidget {
const ChatInputEmojiPreview({
Key? key,
required this.emojiItem,
required this.width,
required this.height,
}) : super(key: key);
final CommonChatEmojiItem emojiItem;
final double width;
final double height;
State<ChatInputEmojiPreview> createState() => _ChatInputEmojiPreviewState();
}
class _ChatInputEmojiPreviewState extends State<ChatInputEmojiPreview> {
Widget build(BuildContext context) {
return Container(
child: ChatInputEmojiShowEmoji(
emojiItem: widget.emojiItem,
width: widget.width,
height: widget.height,
),
);
}
}
// 显示预览的内容
class ChatInputEmojiShowEmoji extends StatelessWidget {
const ChatInputEmojiShowEmoji({
Key? key,
required this.emojiItem,
required this.width,
required this.height,
}) : super(key: key);
final CommonChatEmojiItem emojiItem;
final double width;
final double height;
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
child: Stack(
children: [
ImageHelper.wrapAssetAtImages(
"icons/bg_emoji-preview.png",
width: width,
height: height,
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 25.0,
),
ImageHelper.imageNetwork(
imageUrl: "${emojiItem.url}",
fit: BoxFit.cover,
width: 60,
height: 60,
),
SizedBox(
height: 3.0,
),
Text(
"${emojiItem.emojiName}",
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: ColorUtil.hexColor(0x555555),
decoration: TextDecoration.none,
),
),
Expanded(
child: Container(),
),
],
),
],
),
);
}
}
三、小结
flutter聊天界面-自定义表情键盘实现,主要实现GridView布局表情,自定义预览功能,使用GestureDetector长按功能得到LongPressStartDetails details获得长按的位置,展示表情预览、表情的图片和文本富文本展示-Text.rich(TextSpan(children: textSapns));。
学习记录,每天不停进步。