flutter聊天界面-聊天气泡长按弹出复制、删除按钮菜单

news2024/10/6 14:38:24

flutter聊天界面-聊天气泡长按弹出复制、删除按钮菜单
在之前实现了flutter聊天界面的富文本展示内容,这里记录一下当长按聊天气泡的时候弹出复制、删除等菜单功能

一、查看效果

当长按聊天气泡的时候弹出复制、删除等菜单,可新增更多按钮

在这里插入图片描述

二、代码实现

实现箭头效果,这里实现自定义的CustomPainter。flutter提供一块2D画布Canvas,Canvas内部封装了一些基本绘制的API,开发者可以通过Canvas绘制各种自定义图形。在Flutter中,提供了一个CustomPaint 组件,它可以结合画笔CustomPainter来实现自定义图形绘制。

绘制箭头效果代码

class ChatBubbleMenuShape extends CustomPainter {
  final Color bgColor;
  final double arrowSize;

  ChatBubbleMenuShape(this.bgColor, this.arrowSize);

  
  void paint(Canvas canvas, Size size) {
    var paint = Paint()..color = bgColor;

    var path = Path();
    path.lineTo(-arrowSize, 0);
    path.lineTo(0, arrowSize);
    path.lineTo(arrowSize, 0);

    canvas.drawPath(path, paint);
  }

  
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

// 长按气泡菜单的容器,展示具体的菜单容器

// 长按气泡菜单的容器
class ChatBubbleMenuContainer extends StatefulWidget {
  const ChatBubbleMenuContainer({
    Key? key,
    required this.chatMessage,
    required this.bubbleOffset,
    required this.bubbleSize,
    required this.onBubbleMenuButtonPressed,
  }) : super(key: key);

  final CommonChatMessage chatMessage;
  final Offset bubbleOffset;
  final Size bubbleSize;
  final Function(int index) onBubbleMenuButtonPressed;

  
  State<ChatBubbleMenuContainer> createState() =>
      _ChatBubbleMenuContainerState();
}

class _ChatBubbleMenuContainerState extends State<ChatBubbleMenuContainer> {
  
  Widget build(BuildContext context) {
    double itemWidth = 60;
    double itemHeight = 40;

    double menuWidth = itemWidth * 2;
    double menuHeight = itemHeight * 2;

    double dx =
        widget.bubbleOffset.dx + (widget.bubbleSize.width - menuWidth) / 2.0;
    double dy = widget.bubbleOffset.dy;

    print("widget.bubbleOffset:${widget.bubbleOffset}");

    LoggerManager().debug("chatBubbleFrame offset:${widget.bubbleOffset},"
        "size:${widget.bubbleSize}");

    double arrowSize = 10.0;

    return Stack(
      children: [
        Positioned(
          left: dx - arrowSize / 2.0,
          top: dy - menuHeight / 2.0,
          child: buildMenu(
            context,
            Size(itemWidth, itemHeight),
          ),
        ),
        Positioned(
          left: dx + menuWidth / 2 + arrowSize / 2.0,
          top: dy - menuHeight / 2.0 + itemHeight + arrowSize - 2.0,
          child: CustomPaint(
            painter:
                ChatBubbleMenuShape(ColorUtil.hexColor(0x454545), arrowSize),
          ),
        ),
      ],
    );
  }

  Widget buildMenu(BuildContext context, Size itemSize) {
    return Container(
      padding: const EdgeInsets.all(5.0),
      decoration: BoxDecoration(
        color: ColorUtil.hexColor(0x454545),
        borderRadius: const BorderRadius.only(
          topRight: Radius.circular(3),
          topLeft: Radius.circular(3),
          bottomLeft: Radius.circular(3),
          bottomRight: Radius.circular(3),
        ),
      ),
      child: Wrap(
        spacing: 8.0, // 主轴(水平)方向间距
        runSpacing: 4.0, // 纵轴(垂直)方向间距
        alignment: WrapAlignment.center, //沿主轴方向居中
        children: [
          ChatBubbleMenuButton(
            width: itemSize.width,
            height: itemSize.height,
            icon: "file://ic_post_unlike.png",
            name: "复制",
            onBubbleMenuButtonPressed: () {
              widget.onBubbleMenuButtonPressed(0);
            },
          ),
          ChatBubbleMenuButton(
            width: itemSize.width,
            height: itemSize.height,
            icon: "file://ic_post_unlike.png",
            name: "删除",
            onBubbleMenuButtonPressed: () {
              widget.onBubbleMenuButtonPressed(1);
            },
          ),
        ],
      ),
    );
  }
}

// 显示气泡菜单
class ChatBubbleMenuButton extends StatelessWidget {
  const ChatBubbleMenuButton({
    Key? key,
    required this.icon,
    required this.name,
    required this.onBubbleMenuButtonPressed,
    required this.width,
    required this.height,
  }) : super(key: key);

  final String icon;
  final String name;
  final Function onBubbleMenuButtonPressed;
  final double width;
  final double height;

  
  Widget build(BuildContext context) {
    return ButtonWidget(
      width: width,
      height: height,
      borderRadius: 6.0,
      onPressed: () {
        onBubbleMenuButtonPressed();
      },
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          buildButtonIcon(context),
          SizedBox(
            height: 2.0,
          ),
          Text(
            "${name}",
            textAlign: TextAlign.left,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: TextStyle(
              fontSize: 11,
              fontWeight: FontWeight.w500,
              fontStyle: FontStyle.normal,
              color: ColorUtil.hexColor(0xffffff),
              decoration: TextDecoration.none,
            ),
          ),
        ],
      ),
    );
  }

  Widget buildButtonIcon(BuildContext context) {
    // 本地图片
    String imageUrl = "${icon ?? ""}";
    String start = "file://";
    if (imageUrl.startsWith(start)) {
      String imageAssetFile = imageUrl.substring(start.length);

      return ImageHelper.wrapAssetAtImages(
        "icons/${imageAssetFile}",
        width: 18.0,
        height: 18.0,
      );
    }

    // 网络图片
    return ImageHelper.imageNetwork(
      imageUrl: imageUrl,
      width: 18.0,
      height: 18.0,
      errorHolder: Container(),
    );
  }
}

我们需要在聊天气泡上使用Gesture实现长按获取到获取气泡的位置及大小

GestureDetector(
        onTap: () {
          if (widget.onBubbleTapPressed != null) {

          }
        },
        onDoubleTap: () {
          if (widget.onBubbleDoubleTapPressed != null) {

          }
        },
        onLongPressStart: (LongPressStartDetails details) {
// 获取到获取气泡的位置及大小
        },
        child: Container(),
      );

获取大小代码

if (bubbleKey.currentContext == null) return null;
    // 获取输入框的位置
    final renderObject =
        bubbleKey.currentContext!.findRenderObject() as RenderBox;
    if (renderObject == null) return null;

    // offset.dx , offset.dy 就是控件的左上角坐标
    Offset offset = renderObject.localToGlobal(Offset.zero);
    //获取size
    Size size = renderObject.size;

三、实现弹窗功能

showGeneralDialog:用于自定义提示框

// 气泡长按操作
  static void elemBubbleLongPress(
      BuildContext context, CommonChatMessage chatMessage,
      {Map<String, dynamic>? additionalArguments,
      required LongPressStartDetails details,
      ChatBubbleFrame? chatBubbleFrame}) {
    if (ChatBubbleFrame == null) {
      // 没有气泡大小的时候
      return;
    }

    Offset bubbleOffset = chatBubbleFrame!.offset;
    Size bubbleSize = chatBubbleFrame!.size;

    LoggerManager().debug("chatBubbleFrame offset:${chatBubbleFrame.offset},"
        "size:${chatBubbleFrame.size}");

    // 气泡长按弹出菜单
    showGeneralDialog(
      context: context,
      barrierLabel: '',
      barrierColor: Colors.black.withOpacity(0.0),
      transitionDuration: const Duration(milliseconds: 200),
      barrierDismissible: true,
      pageBuilder: (BuildContext dialogContext, Animation animation,
          Animation secondaryAnimation) {
        return GestureDetector(
          child: ChatBubbleMenuContainer(
            chatMessage: chatMessage,
            bubbleOffset: bubbleOffset,
            bubbleSize: bubbleSize,
            onBubbleMenuButtonPressed: (int index) {
              Navigator.of(dialogContext).pop();
            },
          ),
          onTapDown: (TapDownDetails details) {
            Navigator.of(dialogContext).pop();
          },
        );
      },
      transitionBuilder: (_, anim, __, child) {
        return FadeTransition(
          opacity: anim,
          child: child,
        );
      },
    );

四、小结

flutter聊天界面-聊天气泡长按弹出复制、删除按钮菜单,主要实现Canvas结合画笔CustomPainter绘制,根据GestureDetector获取位置,通过findRenderObject、localToGlobal获取当前气泡的大小及位置,最后使用showGeneralDialog弹出。

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【网络安全带你练爬虫-100练】第8练:json数据的最小项提取

目录 一、目标1&#xff1a;爬取指定json中数据 二、目标2&#xff1a;循环取json中数据 三、目标3&#xff1a;提取每个数据中的某一项 四、网络安全小圈子 一、目标1&#xff1a;爬取指定json中数据 爬取data里数据 核心代码&#xff1a; dirt1 json.loads(res.text)pr…

奉劝各位项目经理,最好别参加8月PMP考试了……

早上好&#xff0c;我是老原。 上周我看到一篇介绍考证的文章&#xff0c;结果在热评看到了PMP证书。 不得不承认&#xff0c;PMP证书现在的火热程度已经超过了我们的想象。前几年我选择考PMP&#xff0c;也只是为了在求职的时候多个优势。 但是现在&#xff0c;几乎我周围所…

ThreadLocal使用场景介绍以及关于内存泄漏的探讨

目录 1. 使用场景一&#xff1a;线程隔离 2. 使用场景二&#xff1a;使用ThreadLocal进行跨函数数据传递 3. ThreadLocal导致的内存泄漏问题 4. ThreadLocal在Spring框架中的应用 5. 扩展&#xff1a;InheritableThreadLocal 转载&#xff1a;【Java】ThreadLocal使用场景…

字节、数据位、ascii码、RS232串口、modbus协议

RS232通信协议详解 - 百度文库 ASCII_百度百科 ascii码表 modebus协议 - 百度文库https://wenku.baidu.com/view/58aae6134431b90d6c85c7b6.html?_wkts_1688713246949&bdQueryascii%E7%A0%81modelbus%E7%A0%81 Modbus 的RTU、ASCII、TCP傻傻搞不清楚&#xff1f;一文全…

归并排序的应用—逆序对的个数

一、&#xff08;题目&#xff09; 给定一个长度为 n 的整数数列&#xff0c;请你计算数列中的逆序对的数量。 逆序对的定义如下&#xff1a;对于数列的第 i个和第 j 个元素&#xff0c;如果满足 i<j且 a[i]>a[j]&#xff0c;则其为一个逆序对&#xff1b;否则不是。 输…

吐血整合!风控与反欺诈相关数据来源(上)

本文会分上、下两篇&#xff0c;介绍各种风控与反欺诈相关的数据来源&#xff0c;包括其主要的数据维度和产品服务形态。 目录&#xff1a; 人行二代征信 持牌征信机构数据 司法大数据 航旅大数据 铁路大数据 税务大数据 交通大数据 电力大数据 保险大数据 人社大数据 一、人行…

vs2019打包发布c#编写的exe应用

由于该应用还要引用其它的exe应用&#xff0c;所以 .NET Framework 4.6.2 脱机安装程序Windows - Microsoft 支持 参考下面的配置即可 VS2019如何打包程序_vs2019 打包_咻咻咻...的博客-CSDN博客https://blog.csdn.net/qq_37043193/article/details/119530253?ops_request_…

JVM源码剖析之Java对象创建过程

关于 "Java的对象创建" 这个话题分布在各种论坛、各种帖子&#xff0c;文章的水平参差不齐。并且大部分仅仅是总结 "面试宝典" 的流程&#xff0c;小部分就是copy其他帖子&#xff0c;极少能看到拿源码作为论证。所以特意写下这篇文章。 版本信息如下&…

搭建帮助中心5大注意事项

在现代互联网时代&#xff0c;为企业网站建立一个优雅实用的网站帮助中心变得尤为重要。一个好的网站帮助中心可以帮助企业解决客户的难点、痛点&#xff0c;提高客户满意度、期待值&#xff0c;从而更好地留住客户&#xff0c;增加收入。 如果没有帮助中心或者是帮助中心创建…

深入理解链表:一种动态的线性数据结构

文章目录 前言1. 概述2. 单向链表3. 单向链表&#xff08;带哨兵&#xff09;4. 双向链表&#xff08;带哨兵&#xff09;5. 环形链表&#xff08;带哨兵&#xff09;6. 结语 前言 链表是我们在日常编程中经常使用的一种数据结构&#xff0c;它相比于数组具有更好的动态性能。…

Spark(17):RDD、DataFrame、DataSet三者的关系

目录 0. 相关文章链接 1. 三者的产生 2. 三者的共性 3. 三者的区别 3.1. RDD 3.2. DataFrame 3.3. DataSet 4. 三者的互相转换 4.1. 互相转换图 4.2. DataFrame 和 DataSet 转换 0. 相关文章链接 Spark文章汇总 1. 三者的产生 在 SparkSQL 中 Spark 为我们提供了两…

Flutter TextField 输入框 简单使用

创建方式一&#xff1a; ///用于文本输入框 TextEditingController controller new TextEditingController();/// 设置TextField中显示的内容void setEditeInputTextFunction(String flagText) {controller .text flagText;}/// 清除TextField中显示的内容void clearEditeIn…

Web服务器群集:podman与docker技术集群

目录 一、理论 1.虚拟化 2.容器 3.podman 4.docker 5.podman与docker区别 二、实验 1.部署podman 2.部署docker 三、总结 一、理论 1.虚拟化 &#xff08;1&#xff09;概念 虚拟化&#xff1a;将应用程序和系统内核资源进行解耦&#xff0c;以操作系统级别进行隔离…

利用langchain-ChatGLM、langchain-TigerBot实现基于本地知识库的问答应用

目录 1 原理 2 langchain-ChatGLM的开发部署 2.1 安装环境 2.2 加载本地模型 3 langchain-TigerBot的开发部署 刷B站的时候&#xff0c;无意中看到吴恩达的一个langchain的教程&#xff0c;然后去github上搜了下&#xff0c;发现别人利用langchain和chatGLM做的基于本地知…

【C++11】lambda表达式 包装器

文章目录 1 lambda表达式1.1 引例1.2 lambda表达式的基本语法1.3 lambda表达式的底层原理 2 包装器3 bind 1 lambda表达式 1.1 引例 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法&#xff1a; #include <algorithm> …

docker运行 mycli

1. 制作镜像 1.1 Dockerfile: FROM python:3.8 ENV MYSQL_HOST192.168.1.108 ENV MYSQL_PWDroot RUN apt-get update && apt-get install -y less RUN pip3 install mycli ENTRYPOINT ["mycli"] 注意 python:3.8 pip3 mycli 具有版本限制的,如果改了版…

【计算机网络】1.5——计算机网络的体系结构(网络分层模型)

计算机网络的体系结构 概述 计算机网络的体系结构是计算机网络及其构建所应完成功能的精确定义 考题 不属于网络体系结构所描述的内容的是 A、网络的层次 B、每层使用的协议 C、协议的内部实现细节 D、每层必须完成的功能 这些功能的「实现细节」&#xff0c;是遵守这种体系…

Web 前端 Day 1

课程大纲&#xff1a; html 结构 css 表现 Js 行为 jquery库 &#xff08;地位下降趋势 仍旧在用&#xff09; bootstrap 前端高端只是&#xff1a;angular angularjs html 超文本标记语言 相关解释 描述网页的语言 不仅有文字&#xff0c;还有图片、音频、视频等等 超…

某网站JS加密、OB混淆与CSS反爬实战分析

1. 写在前面 最近一段时间接触了一些小说网站的业务。发现很多的小说网站&#xff0c;甚至一些小站它们的安全防护措施做的都很到位&#xff01;例如上次说到的的五秒盾也是存在于一个小说小站。今天要讲的这个网站它集JS加密、ob混淆、CSS反爬于一体 目标站点&#xff1a; aH…

用真人模型制作3D虚拟人物,岂不是更真实?

3D虚拟人物是指利用计算机技术和图形学技术创建的一种能够模拟真实人体形态、行为和语言的虚拟实体。与传统的平面图像或视频不同&#xff0c;3D虚拟人物具有立体感和真实感&#xff0c;能够在虚拟环境中实现人机交互和情感交流&#xff0c;给用户带来全新的沉浸式体验。 随着…