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

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弹出。

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

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

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

相关文章

2023/7/8总结

Tomcat 启动&#xff1a;双击bin目录下的startup.bat文件停止&#xff1a;双击bin目录下的shutdown.bat 文件访问 &#xff1a;http://localhost:8080&#xff08;默认是8080&#xff0c;可以修改&#xff09; git的使用 打开git bash git config --global user.name "名…

OpenCV读取一张深度图像并显示

#include <iostream> #include <opencv2/imgcodecs.hpp> #include <opencv2/opencv.hpp> #include

服务端研发提测模板

test环境分支自测通过 提测邮件标注test环境分支 【xxxxxx需求】服务端研发提测了&#xff0c;快去测试吧!

第十二章 elk

1、ELK可以帮助我们解决哪些问题 日志分布在多台不同的服务器上,业务一旦出现故障,需要一台台查看日志 单个日志文件巨大,无法使用常用的文本工具分析,检索困难; 2、架构设计分析 Filebeat和Logstash ELK架构中使用Logstash收集、解析日志,但是Logstash对内存、cpu、i…

简述JMeter实现分布式并发及操作

为什么要分布式并发&#xff1f; JMeter性能实践过程中&#xff0c;一旦进行高并发操作时就会出现以下尴尬场景&#xff0c;JMeter客户端卡死、请求错误或是超时等&#xff0c;导致很难得出准确的性能测试结论。 目前知道的有两个方法可以解决JMeter支撑高并发&#xff1a; …

【ELK企业级日志分析系统】部署Filebeat+ELK详解

部署FilebeatELK详解 1. 部署Filebeat节点&#xff08;CentOS 7-4&#xff09;1.1 部署Apache服务1.2 部署Filebeat服务 2. filter插件2.1 grok正则捕获插件2.1.1 内置正则表达式调用2.1.2 自定义表达式调用2.1.3 设置正则表达式过滤条件 2.2 mutate数据修改插件2.2.1 Mutate过…

抖音seo矩阵系统源码|需求文档编译说明(二)

目录 1.抖音seo矩阵系统文档开发流程 2.各平台源码编译方式说明 3.底层技术功能表达式 1.抖音seo矩阵系统文档开发流程 ①产品原型 ②需求文档 ③产品流程图 ④部署方式说明 ⑤完整源码 ⑥源码编译方式说明 ⑦三方框架和SDK使用情况说明和代码位置 ⑧平台操作文档 ⑨程序架…

Ubuntu 20.04 L2TP VPN 自动重连脚本,cron定时任务设置

1、连接VNP脚本 reconnect_l2tp_vpn.sh #!/bin/sh ppp0_flagifconfig | grep ppp0 echo $ppp0_flag if [ -z "$ppp0_flag" ];thenecho "connet to vpn ..."# connet vpn# echo PASSWORD &#xff5c; sudo -S 这样可以不用手动输入密码!echo abc123| su…

IDEA集成Maven

目录 配置Maven环境 创建Maven项目 Maven坐标 导入Maven项目 Maven依赖管理&#xff08;核心&#xff09; 配置Maven环境 两种方法 每没创建一个maven项目都需要在项目中配置一遍在所有设置中进行全局设置&#xff0c;适用于所有的maven项目 步骤 在idea的初始界面中找到所…

【雕爷学编程】Arduino动手做(158)---VL53L0X激光测距模块

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

【Django学习】(十二)GenericAPIView_过滤_排序_分页

上篇文章初步接触了GenericAPIView&#xff0c;这次来更加深入的学习它&#xff0c;了解里面的一些使用和方法 get_object&#xff1a;源码中&#xff1a;处理查询集&#xff0c;并含有所需要得pk值,lookup_fieldget_queryset&#xff1a;源码中&#xff1a;先判断queryset是否…

可使用Linux 测试IP和端口是否能访问,查看返回状态码

一、 使用wget判断 wget是linux下的下载工具&#xff0c;需要先安装. 用法: wget ip:port wget ip:port连接存在的端口 二、使用telnet判断 telnet是windows标准服务&#xff0c;可以直接用&#xff1b;如果是linux机器&#xff0c;需要安装telnet. 用法: telnet ip port…

uniapp电子签名以及竖屏签名后内容旋转90度变为横屏图片

用该插件挺不错的 电子签名插件地址 如果你一个页面要用多个该插件&#xff0c;就改成不同的cavas-id&#xff0c;修改插件源码 效果图 竖屏写 旋转成横屏图片 插件内 在拿到签名临时地址后的页面 <!-- 旋转图片canvas --> <canvas canvas-id"camCacnvs&quo…

MySQL-SQL存储函数以及触发器详解

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

15 Java 使用for进行死循环

括号里直接写两个分号即可。for(;;) package demo;public class Demo8 {public static void main(String[] args) {for (;;){System.out.println("你是最棒的&#xff01;");}} }

springboot高校党务系统

开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9

神经网络解决预测问题(以共享单车预测为例)

背景:大约从2015年起,共享单车席卷了大部分城市。共享单车在给人们带来便利的同时,也存在一些问题:单车的分布很不均匀。比如在早高峰的时候,一些地铁口往往聚集着大量的单车,而到了晚高峰却很难找到一辆单车了。那么如何解决共享单车分布不均匀的问题呢?目前,共享单车…

Docker镜像

是什么 是一种轻量级、可执行的独立软件包&#xff0c;它包含运行某个软件所需的所有内容&#xff0c;我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等)&#xff0c;这个打包好的运行环境就是image镜像文件。 只有通…

离散化模板(附 区间和 解决方法)

目录 用于解决的问题类型&#xff1a; 作用&#xff1a; 使用到的函数&#xff1a; 常用模板&#xff1a; 例题引入&#xff1a; 题目&#xff1a; 解题思路&#xff1a; 代码详解&#xff1a; 用于解决的问题类型&#xff1a; 对于值域比较大&#xff0c;但个数比较少…

Window10 系统 RabbitMQ的安装和简单使用

1、下载 & 安装 Erlang 因为RabbitMQ的服务端是基于 Erlang编写的&#xff0c;所以&#xff0c;首先需要安装Erlang。 1&#xff09;下载 下载地址如下&#xff1a; https://www.erlang.org/downloads此处下载比较慢&#xff0c;可以参考如下百度网盘&#xff1a; 链接…