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弹出。
学习记录,每天不停进步。