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);//获取sizeSize 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弹出。
学习记录,每天不停进步。