flutter聊天界面-自定义表情键盘实现
flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。
flutter开发基础腾讯IM的聊天应用,使用的是tencent_im_sdk_plugin插件。使用的是自定义表情。
这里使用自定义表情,表情列表如下
const emojiUrl = 'https://web.sdk.qcloud.com/im/assets/emoji/';
const emojiMap = {
'[NO]': '[email protected]',
'[OK]': '[email protected]',
'[下雨]': '[email protected]',
'[么么哒]': '[email protected]',
'[乒乓]': '[email protected]',
'[便便]': '[email protected]',
'[信封]': '[email protected]',
'[偷笑]': '[email protected]',
'[傲慢]': '[email protected]',
'[再见]': '[email protected]',
'[冷汗]': '[email protected]',
'[凋谢]': '[email protected]',
'[刀]': '[email protected]',
'[删除]': '[email protected]',
'[勾引]': '[email protected]',
'[发呆]': '[email protected]',
'[发抖]': '[email protected]',
'[可怜]': '[email protected]',
'[可爱]': '[email protected]',
'[右哼哼]': '[email protected]',
'[右太极]': '[email protected]',
'[右车头]': '[email protected]',
'[吐]': '[email protected]',
'[吓]': '[email protected]',
'[咒骂]': '[email protected]',
'[咖啡]': '[email protected]',
'[啤酒]': '[email protected]',
'[嘘]': '[email protected]',
'[回头]': '[email protected]',
'[困]': '[email protected]',
'[坏笑]': '[email protected]',
'[多云]': '[email protected]',
'[大兵]': '[email protected]',
'[大哭]': '[email protected]',
'[太阳]': '[email protected]',
'[奋斗]': '[email protected]',
'[奶瓶]': '[email protected]',
'[委屈]': '[email protected]',
'[害羞]': '[email protected]',
'[尴尬]': '[email protected]',
'[左哼哼]': '[email protected]',
'[左太极]': '[email protected]',
'[左车头]': '[email protected]',
'[差劲]': '[email protected]',
'[弱]': '[email protected]',
'[强]': '[email protected]',
'[彩带]': '[email protected]',
'[彩球]': '[email protected]',
'[得意]': '[email protected]',
'[微笑]': '[email protected]',
'[心碎了]': '[email protected]',
'[快哭了]': '[email protected]',
'[怄火]': '[email protected]',
'[怒]': '[email protected]',
'[惊恐]': '[email protected]',
'[惊讶]': '[email protected]',
'[憨笑]': '[email protected]',
'[手枪]': '[email protected]',
'[打哈欠]': '[email protected]',
'[抓狂]': '[email protected]',
'[折磨]': '[email protected]',
'[抠鼻]': '[email protected]',
'[抱抱]': '[email protected]',
'[抱拳]': '[email protected]',
'[拳头]': '[email protected]',
'[挥手]': '[email protected]',
'[握手]': '[email protected]',
'[撇嘴]': '[email protected]',
'[擦汗]': '[email protected]',
'[敲打]': '[email protected]',
'[晕]': '[email protected]',
'[月亮]': '[email protected]',
'[棒棒糖]': '[email protected]',
'[汽车]': '[email protected]',
'[沙发]': '[email protected]',
'[流汗]': '[email protected]',
'[流泪]': '[email protected]',
'[激动]': '[email protected]',
'[灯泡]': '[email protected]',
'[炸弹]': '[email protected]',
'[熊猫]': '[email protected]',
'[爆筋]': '[email protected]',
'[爱你]': '[email protected]',
'[爱心]': '[email protected]',
'[爱情]': '[email protected]',
'[猪头]': '[email protected]',
'[猫咪]': '[email protected]',
'[献吻]': '[email protected]',
'[玫瑰]': '[email protected]',
'[瓢虫]': '[email protected]',
'[疑问]': '[email protected]',
'[白眼]': '[email protected]',
'[皮球]': '[email protected]',
'[睡觉]': '[email protected]',
'[磕头]': '[email protected]',
'[示爱]': '[email protected]',
'[礼品袋]': '[email protected]',
'[礼物]': '[email protected]',
'[篮球]': '[email protected]',
'[米饭]': '[email protected]',
'[糗大了]': '[email protected]',
'[红双喜]': '[email protected]',
'[红灯笼]': '[email protected]',
'[纸巾]': '[email protected]',
'[胜利]': '[email protected]',
'[色]': '[email protected]',
'[药]': '[email protected]',
'[菜刀]': '[email protected]',
'[蛋糕]': '[email protected]',
'[蜡烛]': '[email protected]',
'[街舞]': '[email protected]',
'[衰]': '[email protected]',
'[西瓜]': '[email protected]',
'[调皮]': '[email protected]',
'[象棋]': '[email protected]',
'[跳绳]': '[email protected]',
'[跳跳]': '[email protected]',
'[车厢]': '[email protected]',
'[转圈]': '[email protected]',
'[鄙视]': '[email protected]',
'[酷]': '[email protected]',
'[钞票]': '[email protected]',
'[钻戒]': '[email protected]',
'[闪电]': '[email protected]',
'[闭嘴]': '[email protected]',
'[闹钟]': '[email protected]',
'[阴险]': '[email protected]',
'[难过]': '[email protected]',
'[雨伞]': '[email protected]',
'[青蛙]': '[email protected]',
'[面条]': '[email protected]',
'[鞭炮]': '[email protected]',
'[风车]': '[email protected]',
'[飞吻]': '[email protected]',
'[飞机]': '[email protected]',
'[饥饿]': '[email protected]',
'[香蕉]': '[email protected]',
'[骷髅]': '[email protected]',
'[麦克风]': '[email protected]',
'[麻将]': '[email protected]',
'[鼓掌]': '[email protected]',
'[龇牙]': '[email protected]',
};
const emojiName = [
'[龇牙]',
'[调皮]',
'[流汗]',
'[偷笑]',
'[再见]',
'[敲打]',
'[擦汗]',
'[猪头]',
'[玫瑰]',
'[流泪]',
'[大哭]',
'[嘘]',
'[酷]',
'[抓狂]',
'[委屈]',
'[便便]',
'[炸弹]',
'[菜刀]',
'[可爱]',
'[色]',
'[害羞]',
'[得意]',
'[吐]',
'[微笑]',
'[怒]',
'[尴尬]',
'[惊恐]',
'[冷汗]',
'[爱心]',
'[示爱]',
'[白眼]',
'[傲慢]',
'[难过]',
'[惊讶]',
'[疑问]',
'[困]',
'[么么哒]',
'[憨笑]',
'[爱情]',
'[衰]',
'[撇嘴]',
'[阴险]',
'[奋斗]',
'[发呆]',
'[右哼哼]',
'[抱抱]',
'[坏笑]',
'[飞吻]',
'[鄙视]',
'[晕]',
'[大兵]',
'[可怜]',
'[强]',
'[弱]',
'[握手]',
'[胜利]',
'[抱拳]',
'[凋谢]',
'[米饭]',
'[蛋糕]',
'[西瓜]',
'[啤酒]',
'[瓢虫]',
'[勾引]',
'[OK]',
'[爱你]',
'[咖啡]',
'[月亮]',
'[刀]',
'[发抖]',
'[差劲]',
'[拳头]',
'[心碎了]',
'[太阳]',
'[礼物]',
'[皮球]',
'[骷髅]',
'[挥手]',
'[闪电]',
'[饥饿]',
'[困]',
'[咒骂]',
'[折磨]',
'[抠鼻]',
'[鼓掌]',
'[糗大了]',
'[左哼哼]',
'[打哈欠]',
'[快哭了]',
'[吓]',
'[篮球]',
'[乒乓]',
'[NO]',
'[跳跳]',
'[怄火]',
'[转圈]',
'[磕头]',
'[回头]',
'[跳绳]',
'[激动]',
'[街舞]',
'[献吻]',
'[左太极]',
'[右太极]',
'[闭嘴]',
'[猫咪]',
'[红双喜]',
'[鞭炮]',
'[红灯笼]',
'[麻将]',
'[麦克风]',
'[礼品袋]',
'[信封]',
'[象棋]',
'[彩带]',
'[蜡烛]',
'[爆筋]',
'[棒棒糖]',
'[奶瓶]',
'[面条]',
'[香蕉]',
'[飞机]',
'[左车头]',
'[车厢]',
'[右车头]',
'[多云]',
'[下雨]',
'[钞票]',
'[熊猫]',
'[灯泡]',
'[风车]',
'[闹钟]',
'[雨伞]',
'[彩球]',
'[钻戒]',
'[沙发]',
'[纸巾]',
'[手枪]',
'[青蛙]',
];
这里定义自定义表情的数据类 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;
}
}
排列表情,使用的是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,
),
);
}
}
// 底部表情切换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,
),
),
],
),
);
}
}
当我们使用常见的聊天工具的时候,表情基本上都有预览功能,这里实现长按预览表情功能。
预览表情效果如下
具体代码
/// 表情长按预览功能
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));。
学习记录,每天不停进步。