flutter聊天界面-自定义表情键盘实现

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));。

学习记录,每天不停进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/36168.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

css设计表格圆角最简单的方法

代码如下&#xff1a; table {width: 100%;/* border-collapse: collapse; */background-color: #FBFBFB; /* 背景颜色; */border-collapse: separate; /* 让border-radius有效 */border-spacing: 0; /*表格中每个格边距设为0*/border: 1px solid #DFDFDF;/*边框*/border-radi…

ETHERNET/IP 转ETHERCAT连接倍福和欧姆龙PLC的配置方法

ETHERNET/IP和ETHERCAT是两种不同的协议&#xff0c;它们在工业生产中都有广泛的应用。然而&#xff0c;由于协议不同&#xff0c;这两种设备之间无法通讯&#xff0c;这给工业生产带来了很大的麻烦。而远创智控YC-EIP-ECT网关应运而生&#xff0c;它能够连接到ETHERNET/IP总线…

【Linux之拿捏信号3】阻塞信号

文章目录 相关概念原理sigset_t信号集信号集操作函数sigprocmask系统调用sigpending 相关概念 实际执行信号的处理动作——信号递达Delivery&#xff08;例如自定义捕捉动作&#xff0c;core&#xff0c;Term终止进程的动作&#xff09;。信号从产生到递达之间的状态——信号未…

Anaconda配置可视化绘图库seaborn的方法

本文介绍在Anaconda的环境中&#xff0c;安装Python语言中&#xff0c;常用的一个绘图库seaborn模块的方法。 seaborn模块是基于Matplotlib的数据可视化库&#xff0c;它提供了一种更简单、更漂亮的界面来创建各种统计图形。seaborn模块主要用于数据探索、数据分析和数据可视化…

换零钱——最小钱币张数(贪心算法)

贪心算法&#xff1a;根据给定钱币面值列表&#xff0c;输出给定钱币金额的最小张数。 (本笔记适合学完python基本数据结构&#xff0c;初通 Python 的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣…

C/C++编程安全标准GJB-8114解读——声明定义类

软件检测实验室在建立软件测试体系或申请cnas/cma相关资质时&#xff0c;需要依据相关标准&#xff0c;使用有效的方法开展检验检测活动&#xff0c;GJB-8114是一部嵌入式软件安全测试相关的国家标准&#xff0c;本系列文章我们就针对GJB-8114《C/C语言编程安全子集》的具体内容…

Android 热修复一

一、什么是热修复&#xff1f; 在我们应用上线后出现bug需要及时修复时&#xff0c;不用再发新的安装包&#xff0c;只需要发布补丁包&#xff0c;在客户无感知下修复掉bug。 实现效果&#xff1a; Demo源码&#xff1a; https://gitee.com/sziitjim/hotfix 二、怎么进行热修…

一文了解Docker之网络模型

目录 1.Docker网络 1.1 Docker网络模型概述 1.2 Docker网络驱动程序 1.2.1 host模式 1.2.2 bridge模式 1.2.3 container模式 1.2.4 none模式 1.3 Docker网络命令示例 1.3.1 创建一个自定义网络 1.3.2 列出所有网络 1.3.3 连接容器到网络 1.3.4 断开容器与网络的连接…

如何与ChatGPT愉快地聊天

原文链接&#xff1a;https://mp.weixin.qq.com/s/ui-O4CnT_W51_zqW4krtcQ 人工智能的发展已经走到了一个新的阶段&#xff0c;在这个阶段&#xff0c;人工智能可以像人一样与我们进行深度的文本交互。其中&#xff0c;OpenAI的ChatGPT是一个具有代表性的模型。然而&#xff0…

【ARM Coresight 系列文章 3.1 - ARM Coresight DP 对 AP 的访问 1】

文章目录 1.1 DP 中相关寄存器的介绍1.1.1 DPACC and APACC 寄存器1.1.2 DP SELECT 寄存器1.1.3 AP CSW寄存器1.1.4 AP TAR 寄存器1.1.5 AP DRW寄存器1.1.6 AP Banked Data registers 1.1 DP 中相关寄存器的介绍 如果DAP接入的是JTAG接口&#xff0c;那么将会通过APACC寄存器来…

[VUE学习]权限管理系统前端vue实现8-右上角用户头像显示实现

1.现在有个问题 我们再没有token情况下通过url可以直接访问页面 这不可以 所以我们需要添加路由守卫 拦截 2.permission.js的代码 import router from "/router/index" import store from "/store"router.beforeEach((to,from,next)>{const whiteList…

React类组件

1. React组件 将页面按照界面功能进行拆分&#xff0c;每一块界面都拥有自己的独立逻辑&#xff0c;这样可以提高项目代码的可维护性。其中React组件分为两种&#xff0c;一种是类式组件&#xff0c;一种是函数式组件。这里我们将的是比较常用的类式组件&#xff0c;但是在后续…

括号生成(力扣)递归 JAVA

目录 题目描述&#xff1a;纯递归解法&#xff1a;递归 回溯&#xff1a; 题目描述&#xff1a; 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a…

子集 (力扣)数学推理 JAVA

给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[],[1],[2],[1,2],[3],[…

Unity/Shader 零碎知识点

坐标系 Unity使用的是左手坐标系&#xff1b;观察空间&#xff0c;通俗来讲就是以摄像机为原点的坐标系&#xff0c;摄像机的前向是z轴的负方向&#xff0c;与模型和世界空间中的定义相反&#xff0c;z轴的坐标减少意味着场景深度的增加 点积 abba|a||b|cos<a,b> 结果为常…

邮票面值-2022年全国青少年信息素养大赛Python国赛第5题

[导读]&#xff1a;超平老师计划推出《全国青少年信息素养大赛Python编程真题解析》50讲&#xff0c;这是超平老师解读Python编程挑战赛真题系列的第7讲。 全国青少年信息素养大赛&#xff08;原全国青少年电子信息智能创新大赛&#xff09;是“世界机器人大会青少年机器人设计…

Spring Boot原理分析 | SpringApplication、Yaml、Properties

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Spring Boot Spring开源框架&#xff0c;轻量级的Java开发框架&#xff0c;解决企业级应用开发的复杂性而创建&#xff0c;简化开发 基于POJO的轻量级和最小侵入型编程…

Kafka入门,漏消费和重复消费, 消费者事务,数据积压(二十四)

漏消费和重复消费 重复消费&#xff1a;已经消费了数据&#xff0c;但是offset没提交。 漏消费&#xff1a;先提交offset后消费&#xff0c;有可能会造成数据得漏消费 消费者事务 如果向完成consumer端得进准一次性消费&#xff0c;那么需要Kafka消费端将消费过程和提交offs…

【Accumulate】Gitee解决每次推送输入账户密码问题

【前言】 每次建立私人仓库后&#xff0c;一推送就得输入账户密码&#xff0c;真的巨烦人啊。 【解决】 step1&#xff1a; 绑定私匙&#xff1a; 配置Git_犟小孩的博客-CSDN博客 step2&#xff1a; 每次绑定远程仓库的时候&#xff0c;使用SSH绑定 如果已经绑定过了&…

YoloV2

时间线 Motivation Yolo-v1是在检测精度尚可的前提下达到了实时检测&#xff0c;同年的SSD检测速度略慢但检测精度远高于Yolo-v1&#xff0c;因此&#xff0c;Yolo-v2则是着眼于检测得更快更准&#xff0c;同时它利用WordTree创造性地将Imag…