全新研发flutter3+dart3+photo_view跨多端仿微信App界面聊天Flutter3-Chat

flutter3-chat基于最新跨全平台技术flutter3+dart3+material-design+shared_preferences+easy_refresh构建的仿微信APP界面聊天实例项目。实现发送图文表情消息/gif大图、长按仿微信语音操作面板、图片预览、红包及朋友圈等功能。

技术架构

  • 编辑器:Vscode
  • 框架技术:Flutter3.16.5+Dart3.2.3
  • UI组件库:material-design3
  • 弹窗组件:showDialog/SimpleDialog/showModalBottomSheet/AlertDialog
  • 图片预览:photo_view^0.14.0
  • 本地缓存:shared_preferences^2.2.2
  • 下拉刷新:easy_refresh^3.3.4
  • toast提示:toast^0.3.0
  • 网址预览组件:url_launcher^6.2.4

Flutter3.x开发跨平台App项目,性能有了大幅度提升,官方支持编译到android/ios/macos/windows/linux/web等多平台,未来可期!

项目构建目录

通过 flutter create app_project 命令即可快速创建一个跨平台初始化项目。

通过命令创建项目后,项目结构就如上图所示。

需要注意的是,flutter项目是基于dart语音开发,需要首先配置Dart SDK和Flutter SDK开发环境,大家可以去官网查看详细配置文档。

https://flutter.dev/

https://flutter.cn/

https://pub.flutter-io.cn/

https://www.dartcn.com/

另外使用VScode编辑器开发项目,可自行安装Flutter / Dart扩展插件。

由于flutter3支持编译到windows,大家可以开发初期在windows上面调试,后期release apk到手机上。

通过如下命令即可运行到windows平台

flutter run -d windows

运行后默认窗口大小为1280x720,可以修改windows/runner/main.cpp文件里面的窗口尺寸。

同样,可以通过 flutter run -d chrome 命令运行到web上预览。

假如在没有真机的情况下,我们可以选择模拟器调试。目前市面上有很多类型模拟器,他们使用adb连接时都会有不同的默认端口,下面列出了一些常用的模拟器及端口号。通过adb connect连接上指定模拟器之后,执行flutter run命令即可运行项目到模拟器上面。

flutter3实现圆角文本框及渐变按钮

  1. Container(
  2. height: 40.0,
  3. margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 30.0),
  4. decoration: BoxDecoration(
  5. color: Colors.white,
  6. border: Border.all(color: const Color(0xffdddddd)),
  7. borderRadius: BorderRadius.circular(15.0),
  8. ),
  9. child: Row(
  10. children: [
  11. Expanded(
  12. child: TextField(
  13. keyboardType: TextInputType.phone,
  14. controller: fieldController,
  15. decoration: InputDecoration(
  16. hintText: '输入手机号',
  17. suffixIcon: Visibility(
  18. visible: authObj['tel'].isNotEmpty,
  19. child: InkWell(
  20. hoverColor: Colors.transparent,
  21. highlightColor: Colors.transparent,
  22. splashColor: Colors.transparent,
  23. onTap: handleClear,
  24. child: const Icon(Icons.clear, size: 16.0,),
  25. )
  26. ),
  27. contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12.0),
  28. border: const OutlineInputBorder(borderSide: BorderSide.none),
  29. ),
  30. onChanged: (value) {
  31. setState(() {
  32. authObj['tel'] = value;
  33. });
  34. },
  35. ),
  36. )
  37. ],
  38. ),
  39. ),

按钮渐变则是通过Container组件的decotaion里面的gradient属性设置渐变效果。

  1. Container(
  2. margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
  3. decoration: BoxDecoration(
  4. borderRadius: BorderRadius.circular(15.0),
  5. // 自定义按钮渐变色
  6. gradient: const LinearGradient(
  7. begin: Alignment.topLeft,
  8. end: Alignment.bottomRight,
  9. colors: [
  10. Color(0xFF0091EA), Color(0xFF07C160)
  11. ],
  12. )
  13. ),
  14. child: SizedBox(
  15. width: double.infinity,
  16. height: 45.0,
  17. child: FilledButton(
  18. style: ButtonStyle(
  19. backgroundColor: MaterialStateProperty.all(Colors.transparent),
  20. shadowColor: MaterialStateProperty.all(Colors.transparent),
  21. shape: MaterialStatePropertyAll(
  22. RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0))
  23. )
  24. ),
  25. onPressed: handleSubmit,
  26. child: const Text('登录', style: TextStyle(fontSize: 18.0),),
  27. ),
  28. )
  29. ),

flutter实现60s倒计时发送验证码功能。

  1. Timer? timer;
  2. String vcodeText = '获取验证码';
  3. bool disabled = false;
  4. int time = 60;
  5.  
  6. // 60s倒计时
  7. void handleVcode() {
  8. if(authObj['tel'] == '') {
  9. snackbar('手机号不能为空');
  10. }else if(!Utils.checkTel(authObj['tel'])) {
  11. snackbar('手机号格式不正确');
  12. }else {
  13. setState(() {
  14. disabled = true;
  15. });
  16. startTimer();
  17. }
  18. }
  19. startTimer() {
  20. timer = Timer.periodic(const Duration(seconds: 1), (timer) {
  21. setState(() {
  22. if(time > 0) {
  23. vcodeText = '获取验证码(${time--})';
  24. }else {
  25. vcodeText = '获取验证码';
  26. time = 60;
  27. disabled = false;
  28. timer.cancel();
  29. }
  30. });
  31. });
  32. snackbar('短信验证码已发送,请注意查收', color: Colors.green);
  33. }

Flutter3沉浸式渐变状态导航栏

要实现如上图渐变AppBar也非常简单,只需要配置AppBar提供的可伸缩灵活区域属性 flexibleSpace 配合gradient即可快速实现渐变导航栏。

  1. AppBar(
  2. title: Text('Flutter3-Chat'),
  3. flexibleSpace: Container(
  4. decoration: const BoxDecoration(
  5. gradient: LinearGradient(
  6. begin: Alignment.topLeft,
  7. end: Alignment.bottomRight,
  8. colors: [
  9. Color(0xFF0091EA), Color(0xFF07C160)
  10. ],
  11. )
  12. ),
  13. )
  14. ),

Flutter3字体图标/自定义badge

flutter内置了丰富的字体图标,通过图标组件 Icon(Icons.add) 引入即可使用。

https://api.flutter-io.cn/flutter/material/Icons-class.html

另外还支持通过自定义IconData方式自定义图标,如使用阿里iconfont图表库图标。

Icon(IconData(0xe666, fontFamily: 'iconfont'), size: 18.0)

把下载的字体文件放到assets目录,

pubspec.yaml中引入字体文件。

  1. class FStyle {
  2. // 自定义iconfont图标
  3. static iconfont(int codePoint, {double size = 16.0, Color? color}) {
  4. return Icon(
  5. IconData(codePoint, fontFamily: 'iconfont', matchTextDirection: true),
  6. size: size,
  7. color: color,
  8. );
  9. }
  10.  
  11. // 自定义Badge红点
  12. static badge(int count, {
  13. Color color = Colors.redAccent,
  14. bool isdot = false,
  15. double height = 18.0,
  16. double width = 18.0
  17. }) {
  18. final num = count > 99 ? '99+' : count;
  19. return Container(
  20. alignment: Alignment.center,
  21. height: isdot ? height / 2 : height,
  22. width: isdot ? width / 2 : width,
  23. decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(100.00)),
  24. child: isdot ? null : Text('$num', style: const TextStyle(color: Colors.white, fontSize: 12.0)),
  25. );
  26. }
  27. }

  1. FStyle.badge(23)
  2. FStyle.badge(2, color: Colors.pink, height: 10.0, width: 10.0)
  3. FStyle.badge(0, isdot: true)

Flutter仿微信PopupMenu下拉菜单/下拉刷新

通过flutter提供的PopupMenuButton组件实现下拉菜单功能。

  1. PopupMenuButton(
  2. icon: FStyle.iconfont(0xe62d, size: 17.0),
  3. offset: const Offset(0, 50.0),
  4. tooltip: '',
  5. color: const Color(0xFF353535),
  6. itemBuilder: (BuildContext context) {
  7. return <PopupMenuItem>[
  8. popupMenuItem(0xe666, '发起群聊', 0),
  9. popupMenuItem(0xe75c, '添加朋友', 1),
  10. popupMenuItem(0xe603, '扫一扫', 2),
  11. popupMenuItem(0xe6ab, '收付款', 3),
  12. ];
  13. },
  14. onSelected: (value) {
  15. switch(value) {
  16. case 0:
  17. print('发起群聊');
  18. break;
  19. case 1:
  20. Navigator.pushNamed(context, '/addfriends');
  21. break;
  22. case 2:
  23. print('扫一扫');
  24. break;
  25. case 3:
  26. print('收付款');
  27. break;
  28. }
  29. },
  30. )
  1. // 下拉菜单项
  2. static popupMenuItem(int codePoint, String title, value) {
  3. return PopupMenuItem(
  4. value: value,
  5. child: Row(
  6. mainAxisAlignment: MainAxisAlignment.start,
  7. children: [
  8. const SizedBox(width: 10.0,),
  9. FStyle.iconfont(codePoint, size: 21.0, color: Colors.white),
  10. const SizedBox(width: 10.0,),
  11. Text(title, style: const TextStyle(fontSize: 16.0, color: Colors.white),),
  12. ],
  13. ),
  14. );
  15. }

如上图:下拉刷新、上拉加载更多是通过 easy_refresh 组件实现功能。

  1. EasyRefresh(
  2. // 下拉加载提示
  3. header: const ClassicHeader(
  4. // showMessage: false,
  5. ),
  6. // 加载更多提示
  7. footer: ClassicFooter(),
  8. // 下拉刷新逻辑
  9. onRefresh: () async {
  10. // ...下拉逻辑
  11. await Future.delayed(const Duration(seconds: 2));
  12. },
  13. // 上拉加载逻辑
  14. onLoad: () async {
  15. // ...
  16. },
  17. child: ListView.builder(
  18. itemCount: chatList.length,
  19. itemBuilder: (context, index) {
  20. return Ink(
  21. // ...
  22. );
  23. },
  24. ),
  25. )

如上图:弹窗功能均是自定义AlertDialog实现效果。通过无限制容器UnconstrainedBox配合SizedBox组件实现自定义窗口大小。

  1. // 关于弹窗
  2. void aboutAlertDialog(BuildContext context) {
  3. showDialog(
  4. context: context,
  5. builder: (context) {
  6. return UnconstrainedBox(
  7. constrainedAxis: Axis.vertical,
  8. child: SizedBox(
  9. width: 320.0,
  10. child: AlertDialog(
  11. contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
  12. backgroundColor: Colors.white,
  13. surfaceTintColor: Colors.white,
  14. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
  15. content: Padding(
  16. padding: const EdgeInsets.symmetric(horizontal: 10.0),
  17. child: Column(
  18. mainAxisSize: MainAxisSize.min,
  19. children: [
  20. Image.asset('assets/images/logo.png', width: 90.0, height: 90.0, fit: BoxFit.cover,),
  21. const SizedBox(height: 10.0),
  22. const Text('Flutter3-WChat', style: TextStyle(color: Color(0xFF0091EA), fontSize: 22.0),),
  23. const SizedBox(height: 5.0),
  24. const Text('基于flutter3+dart3开发跨平台仿微信App聊天实例。', style: TextStyle(color: Colors.black45),),
  25. const SizedBox(height: 20.0),
  26. Text('2024/01 Andy Q: 282310962', style: TextStyle(color: Colors.grey[400], fontSize: 12.0),),
  27. ],
  28. ),
  29. ),
  30. ),
  31. ),
  32. );
  33. }
  34. );
  35. }
  36.  
  37. // 二维码名片弹窗
  38. void qrcodeAlertDialog(BuildContext context) {
  39. showDialog(
  40. context: context,
  41. builder: (context) {
  42. return UnconstrainedBox(
  43. constrainedAxis: Axis.vertical,
  44. child: SizedBox(
  45. width: 320.0,
  46. child: AlertDialog(
  47. contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
  48. backgroundColor: const Color(0xFF07C160),
  49. surfaceTintColor: const Color(0xFF07C160),
  50. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3.0)),
  51. content: Padding(
  52. padding: const EdgeInsets.symmetric(horizontal: 10.0),
  53. child: Column(
  54. mainAxisSize: MainAxisSize.min,
  55. children: [
  56. Image.asset('assets/images/qrcode.png', width: 250.0, fit: BoxFit.cover,),
  57. const SizedBox(height: 15.0),
  58. const Text('扫一扫,加我公众号', style: TextStyle(color: Colors.white60, fontSize: 14.0,),),
  59. ],
  60. ),
  61. ),
  62. ),
  63. ),
  64. );
  65. }
  66. );
  67. }
  68.  
  69. // 退出登录弹窗
  70. void logoutAlertDialog(BuildContext context) {
  71. showDialog(
  72. context: context,
  73. builder: (context) {
  74. return AlertDialog(
  75. content: const Text('确定要退出登录吗?', style: TextStyle(fontSize: 16.0),),
  76. backgroundColor: Colors.white,
  77. surfaceTintColor: Colors.white,
  78. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
  79. elevation: 2.0,
  80. actionsPadding: const EdgeInsets.all(15.0),
  81. actions: [
  82. TextButton(
  83. onPressed: () {Navigator.of(context).pop();},
  84. child: const Text('取消', style: TextStyle(color: Colors.black54),)
  85. ),
  86. TextButton(
  87. onPressed: handleLogout,
  88. child: const Text('退出登录', style: TextStyle(color: Colors.red),)
  89. ),
  90. ],
  91. );
  92. }
  93. );
  94. }

flutter实现微信朋友圈九宫格

  1. GroupZone(images: item['images']),
  2.  
  3. GroupZone(
  4. images: uploadList,
  5. album: true,
  6. onChoose: () async {
  7. Toast.show('选择手机相册图片', duration: 2, gravity: 1);
  8. },
  9. ),

  1. // 创建可点击预览图片
  2. createImage(BuildContext context, String img, int key) {
  3. return GestureDetector(
  4. child: Hero(
  5. tag: img, // 放大缩小动画效果标识
  6. child: img == '+' ?
  7. Container(color: Colors.transparent, child: const Icon(Icons.add, size: 30.0, color: Colors.black45),)
  8. :
  9. Image.asset(
  10. img,
  11. width: width,
  12. fit: BoxFit.contain,
  13. ),
  14. ),
  15. onTap: () {
  16. // 选择图片
  17. if(img == '+') {
  18. onChoose!();
  19. }else {
  20. Navigator.of(context).push(FadeRoute(route: ImageViewer(
  21. images: album ? imgList!.sublist(0, imgList!.length - 1) : imgList,
  22. index: key,
  23. )));
  24. }
  25. },
  26. );
  27. }

使用photo_view插件实现预览大图功能,支持预览单张及多张大图。

  1. import 'package:flutter/material.dart';
  2. import 'package:photo_view/photo_view.dart';
  3. import 'package:photo_view/photo_view_gallery.dart';
  4.  
  5. class ImageViewer extends StatefulWidget {
  6. const ImageViewer({
  7. super.key,
  8. this.images,
  9. this.index = 0,
  10. });
  11.  
  12. final List? images; // 预览图列表
  13. final int index; // 当前预览图索引
  14.  
  15. @override
  16. State<ImageViewer> createState() => _ImageViewerState();
  17. }
  18.  
  19. class _ImageViewerState extends State<ImageViewer> {
  20. int currentIndex = 0;
  21.  
  22. @override
  23. void initState() {
  24. super.initState();
  25. currentIndex = widget.index;
  26. }
  27.  
  28. @override
  29. Widget build(BuildContext context) {
  30. var imgCount = widget.images?.length;
  31.  
  32. return Scaffold(
  33. body: Stack(
  34. children: [
  35. Positioned(
  36. top: 0,
  37. left: 0,
  38. bottom: 0,
  39. right: 0,
  40. child: GestureDetector(
  41. child: imgCount == 1 ? PhotoView(
  42. imageProvider: AssetImage(widget.images![0]),
  43. backgroundDecoration: const BoxDecoration(
  44. color: Colors.black,
  45. ),
  46. minScale: PhotoViewComputedScale.contained,
  47. maxScale: PhotoViewComputedScale.covered * 2,
  48. heroAttributes: PhotoViewHeroAttributes(tag: widget.images![0]),
  49. enableRotation: true,
  50. )
  51. :
  52. PhotoViewGallery.builder(
  53. itemCount: widget.images?.length,
  54. builder: (context, index) {
  55. return PhotoViewGalleryPageOptions(
  56. imageProvider: AssetImage(widget.images![index]),
  57. minScale: PhotoViewComputedScale.contained,
  58. maxScale: PhotoViewComputedScale.covered * 2,
  59. heroAttributes: PhotoViewHeroAttributes(tag: widget.images![index]),
  60. );
  61. },
  62. scrollPhysics: const BouncingScrollPhysics(),
  63. backgroundDecoration: const BoxDecoration(
  64. color: Colors.black,
  65. ),
  66. pageController: PageController(initialPage: widget.index),
  67. enableRotation: true,
  68. onPageChanged: (index) {
  69. setState(() {
  70. currentIndex = index;
  71. });
  72. },
  73. ),
  74. onTap: () {
  75. Navigator.of(context).pop();
  76. },
  77. ),
  78. ),
  79. // 图片索引index
  80. Positioned(
  81. top: MediaQuery.of(context).padding.top + 15,
  82. width: MediaQuery.of(context).size.width,
  83. child: Center(
  84. child: Visibility(
  85. visible: imgCount! > 1 ? true : false,
  86. child: Text('${currentIndex+1} / ${widget.images?.length}', style: const TextStyle(color: Colors.white)),
  87. )
  88. ),
  89. ),
  90. ],
  91. ),
  92. );
  93. }
  94. }

flutter3聊天模块

文本框TextField设置maxLines: null即可实现多行文本输入,支持图文emoj混排,网址连接识别等功能。

  1. // 输入框
  2. Offstage(
  3. offstage: voiceBtnEnable,
  4. child: TextField(
  5. decoration: const InputDecoration(
  6. isDense: true,
  7. hoverColor: Colors.transparent,
  8. contentPadding: EdgeInsets.all(8.0),
  9. border: OutlineInputBorder(borderSide: BorderSide.none),
  10. ),
  11. style: const TextStyle(fontSize: 16.0,),
  12. maxLines: null,
  13. controller: editorController,
  14. focusNode: editorFocusNode,
  15. cursorColor: const Color(0xFF07C160),
  16. onChanged: (value) {},
  17. ),
  18. ),

支持仿微信语音按住说话,左滑取消发送、右滑转换语音功能。

  1. // 语音
  2. Offstage(
  3. offstage: !voiceBtnEnable,
  4. child: GestureDetector(
  5. child: Container(
  6. decoration: BoxDecoration(
  7. color: Colors.white,
  8. borderRadius: BorderRadius.circular(5),
  9. ),
  10. alignment: Alignment.center,
  11. height: 40.0,
  12. width: double.infinity,
  13. child: Text(voiceTypeMap[voiceType], style: const TextStyle(fontSize: 15.0),),
  14. ),
  15. onPanStart: (details) {
  16. setState(() {
  17. voiceType = 1;
  18. voicePanelEnable = true;
  19. });
  20. },
  21. onPanUpdate: (details) {
  22. Offset pos = details.globalPosition;
  23. double swipeY = MediaQuery.of(context).size.height - 120;
  24. double swipeX = MediaQuery.of(context).size.width / 2 + 50;
  25. setState(() {
  26. if(pos.dy >= swipeY) {
  27. voiceType = 1; // 松开发送
  28. }else if (pos.dy < swipeY && pos.dx < swipeX) {
  29. voiceType = 2; // 左滑松开取消
  30. }else if (pos.dy < swipeY && pos.dx >= swipeX) {
  31. voiceType = 3; // 右滑语音转文字
  32. }
  33. });
  34. },
  35. onPanEnd: (details) {
  36. // print('停止录音');
  37. setState(() {
  38. switch(voiceType) {
  39. case 1:
  40. Toast.show('发送录音文件', duration: 1, gravity: 1);
  41. voicePanelEnable = false;
  42. break;
  43. case 2:
  44. Toast.show('取消发送', duration: 1, gravity: 1);
  45. voicePanelEnable = false;
  46. break;
  47. case 3:
  48. Toast.show('语音转文字', duration: 1, gravity: 1);
  49. voicePanelEnable = true;
  50. voiceToTransfer = true;
  51. break;
  52. }
  53. voiceType = 0;
  54. });
  55. },
  56. ),
  57. ),

按住录音显示面板

  1. // 录音主体(按住说话/松开取消/语音转文本)
  2. Visibility(
  3. visible: voicePanelEnable,
  4. child: Material(
  5. color: const Color(0xDD1B1B1B),
  6. child: Stack(
  7. children: [
  8. // 取消发送+语音转文字
  9. Positioned(
  10. bottom: 120,
  11. left: 30,
  12. right: 30,
  13. child: Visibility(
  14. visible: !voiceToTransfer,
  15. child: Column(
  16. children: [
  17. // 语音动画层
  18. Stack(
  19. children: [
  20. Container(
  21. height: 70.0,
  22. margin: const EdgeInsets.symmetric(horizontal: 50.0),
  23. decoration: BoxDecoration(
  24. color: Colors.white,
  25. borderRadius: BorderRadius.circular(15.0),
  26. ),
  27. child: Row(
  28. mainAxisAlignment: MainAxisAlignment.center,
  29. children: [
  30. Image.asset('assets/images/voice_record.gif', height: 30.0,)
  31. ],
  32. ),
  33. ),
  34. Positioned(
  35. right: (MediaQuery.of(context).size.width - 60) / 2,
  36. bottom: 1,
  37. child: RotatedBox(
  38. quarterTurns: 0,
  39. child: CustomPaint(painter: ArrowShape(arrowColor: Colors.white, arrowSize: 10.0)),
  40. )
  41. ),
  42. ],
  43. ),
  44. const SizedBox(height: 50.0,),
  45. // 操作项
  46. Row(
  47. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  48. children: [
  49. // 取消发送
  50. Container(
  51. height: 60.0,
  52. width: 60.0,
  53. decoration: BoxDecoration(
  54. borderRadius: BorderRadius.circular(50.0),
  55. color: voiceType == 2 ? Colors.red : Colors.black38,
  56. ),
  57. child: const Icon(Icons.close, color: Colors.white54,),
  58. ),
  59. // 语音转文字
  60. Container(
  61. height: 60.0,
  62. width: 60.0,
  63. decoration: BoxDecoration(
  64. borderRadius: BorderRadius.circular(50.0),
  65. color: voiceType == 3 ? Colors.green : Colors.black38,
  66. ),
  67. child: const Icon(Icons.translate, color: Colors.white54,),
  68. ),
  69. ],
  70. ),
  71. ],
  72. ),
  73. ),
  74. ),
  75. // 语音转文字(识别结果状态)
  76. Positioned(
  77. bottom: 120,
  78. left: 30,
  79. right: 30,
  80. child: Visibility(
  81. visible: voiceToTransfer,
  82. child: Column(
  83. children: [
  84. // 提示结果
  85. Stack(
  86. children: [
  87. Container(
  88. height: 100.0,
  89. decoration: BoxDecoration(
  90. color: Colors.red,
  91. borderRadius: BorderRadius.circular(15.0),
  92. ),
  93. child: const Row(
  94. mainAxisAlignment: MainAxisAlignment.center,
  95. children: [
  96. Icon(Icons.info, color: Colors.white,),
  97. Text('未识别到文字。', style: TextStyle(color: Colors.white),),
  98. ],
  99. ),
  100. ),
  101. Positioned(
  102. right: 35.0,
  103. bottom: 1,
  104. child: RotatedBox(
  105. quarterTurns: 0,
  106. child: CustomPaint(painter: ArrowShape(arrowColor: Colors.red, arrowSize: 10.0)),
  107. )
  108. ),
  109. ],
  110. ),
  111. const SizedBox(height: 50.0,),
  112. // 操作项
  113. Row(
  114. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  115. children: [
  116. GestureDetector(
  117. child: Container(
  118. height: 60.0,
  119. width: 60.0,
  120. decoration: const BoxDecoration(
  121. color: Colors.transparent,
  122. ),
  123. child: const Column(
  124. mainAxisAlignment: MainAxisAlignment.center,
  125. children: [
  126. Icon(Icons.undo, color: Colors.white54,),
  127. Text('取消', style: TextStyle(color: Colors.white70),)
  128. ],
  129. ),
  130. ),
  131. onTap: () {
  132. setState(() {
  133. voicePanelEnable = false;
  134. voiceToTransfer = false;
  135. });
  136. },
  137. ),
  138. GestureDetector(
  139. child: Container(
  140. height: 60.0,
  141. width: 100.0,
  142. decoration: const BoxDecoration(
  143. color: Colors.transparent,
  144. ),
  145. child: const Column(
  146. mainAxisAlignment: MainAxisAlignment.center,
  147. children: [
  148. Icon(Icons.graphic_eq_rounded, color: Colors.white54,),
  149. Text('发送原语音', style: TextStyle(color: Colors.white70),)
  150. ],
  151. ),
  152. ),
  153. onTap: () {},
  154. ),
  155. GestureDetector(
  156. child: Container(
  157. height: 60.0,
  158. width: 60.0,
  159. decoration: BoxDecoration(
  160. borderRadius: BorderRadius.circular(50.0),
  161. color: Colors.white12,
  162. ),
  163. child: const Icon(Icons.check, color: Colors.white12,),
  164. ),
  165. onTap: () {},
  166. ),
  167. ],
  168. ),
  169. ],
  170. ),
  171. ),
  172. ),
  173. // 提示文字(操作状态)
  174. Positioned(
  175. bottom: 120,
  176. left: 0,
  177. width: MediaQuery.of(context).size.width,
  178. child: Visibility(
  179. visible: !voiceToTransfer,
  180. child: Align(
  181. child: Text(voiceTypeMap[voiceType], style: const TextStyle(color: Colors.white70),),
  182. ),
  183. ),
  184. ),
  185. // 背景
  186. Align(
  187. alignment: Alignment.bottomCenter,
  188. child: Visibility(
  189. visible: !voiceToTransfer,
  190. child: Image.asset('assets/images/voice_record_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill),
  191. ),
  192. ),
  193. // 背景图标
  194. Positioned(
  195. bottom: 25,
  196. left: 0,
  197. width: MediaQuery.of(context).size.width,
  198. child: Visibility(
  199. visible: !voiceToTransfer,
  200. child: const Align(
  201. child: Icon(Icons.graphic_eq_rounded, color: Colors.black54,),
  202. ),
  203. ),
  204. ),
  205. ],
  206. ),
  207. ),
  208. )

flutter3绘制箭头

聊天模块消息及各种箭头展示,通过flutter提供的画板功能绘制箭头。

  1. // 绘制气泡箭头
  2. class ArrowShape extends CustomPainter {
  3. ArrowShape({
  4. required this.arrowColor,
  5. this.arrowSize = 7,
  6. });
  7.  
  8. final Color arrowColor; // 箭头颜色
  9. final double arrowSize; // 箭头大小
  10.  
  11. @override
  12. void paint(Canvas canvas, Size size) {
  13. var paint = Paint()..color = arrowColor;
  14.  
  15. var path = Path();
  16. path.lineTo(-arrowSize, 0);
  17. path.lineTo(0, arrowSize);
  18. path.lineTo(arrowSize, 0);
  19. canvas.drawPath(path, paint);
  20. }
  21.  
  22. @override
  23. bool shouldRepaint(CustomPainter oldDelegate) {
  24. return false;
  25. }
  26. }

Okay,以上就是Flutter3+Dart3开发全平台聊天App实例的一些知识分享,希望对大家有所帮助哈~~

最后附上两个最新实战项目

uni-app+vue3+pinia2仿抖音直播商城:https://www.cnblogs.com/xiaoyan2017/p/17938517

electron27+react18 hooks仿macos桌面系统:https://www.cnblogs.com/xiaoyan2017/p/17850653.html

flexibleSpace

flutter3+dart3聊天室|Flutter3跨平台仿微信App语音聊天/朋友圈的更多相关文章

  1. Taro聊天室|react+taro仿微信聊天App界面|taro聊天实例

    一.项目简述 taro-chatroom是基于Taro多端实例聊天项目,运用Taro+react+react-redux+taroPop+react-native等技术开发的仿微信App界面聊天室,实 ...

  2. Svelte3聊天室|svelte+svelteKit仿微信聊天实例|svelte.js开发App

    基于svelte3.x+svelteKit构建仿微信App聊天应用svelte-chatroom. svelte-chatroom 基于svelte.js+svelteKit+mescroll.js+ ...

  3. Svelte3.x网页聊天实例|svelte.js仿微信PC版聊天svelte-webchat

    基于Svelte3+SvelteKit+Sass仿微信Mac界面聊天实战项目SvelteWebChat. 基于svelte3+svelteKit+sass+mescroll.js+svelte-lay ...

  4. vue聊天室|h5+vue仿微信聊天界面|vue仿微信

    一.项目简介 基于Vue2.0+Vuex+vue-router+webpack2.0+es6+vuePhotoPreview+wcPop等技术架构开发的仿微信界面聊天室——vueChatRoom,实现 ...

  5. uni-app聊天室|vue+uniapp仿微信聊天实例|uniapp仿微信App界面

    一.介绍 运用UniApp+Vue+Vuex+swiper+uniPop等技术开发的仿微信原生App聊天室|仿微信聊天界面实例项目uniapp-chatroom,实现了发送图文消息.表情(gif图), ...

  6. react聊天室|react+redux仿微信聊天IM实例|react仿微信界面

    一.项目概况 基于react+react-dom+react-router-dom+redux+react-redux+webpack2.0+react-photoswipe+swiper等技术混合开 ...

  7. electron聊天室|vue+electron-vue仿微信客户端|electron桌面聊天

    一.项目概况 基于Electron+vue+electron-vue+vuex+Nodejs+vueVideoPlayer+electron-builder等技术仿制微信电脑端界面聊天室实例,实现消息 ...

  8. Vue3.0聊天室|vue3+vant3仿微信聊天实例|vue3.x仿微信app界面

    一.项目简介 基于Vue3.0+Vant3.x+Vuex4.x+Vue-router4+V3Popup等技术开发实现的仿微信手机App聊天实例项目Vue3-Chatroom.实现了发送图文表情消息/g ...

  9. Vue3.0+Electron聊天室|electron跨平台仿QQ客户端|vue3.x聊天应用

    基于vue3+electron11跨端仿制QQ桌面应用实战Vue3ElectronQchat. 使用vue3+electron+vuex4+ant-design-vue+v3scroll+v3laye ...

  10. Tauri-Vue3桌面端聊天室|tauri+vite3仿微信|tauri聊天程序EXE

    基于tauri+vue3.js+vite3跨桌面端仿微信聊天实例TauriVue3Chat. tauri-chat 运用最新tauri+vue3+vite3+element-plus+v3layer等 ...

随机推荐

  1. CSS Sticky Footer 几种实现方式

    项目里,有个需求,登录页,信息,需要使用到sticky footer布局,刚好,巩固下这个技术: 核心代码: 播客: https://www.jb51.net/css/676798.html 视频:h ...

  2. nextTick使用

  3. python之logging日志

    一.logging介绍: 使用 logging.debug(text)来打印信息,info等的使用方法与debug一致,都只有一个位置参数 默认日志界别为:会输出warning以上的信息,代码示例: ...

  4. wxpython窗体之间传递参数

    如何界面存在frame1与frame2,通过frame1打开页面frame2,并将frame2的值传递给frame1 可以使用回调函数传值参考具体代码如下: # -*- coding: utf-8 - ...

  5. 【scikit-learn基础】--『回归模型评估』之误差分析

    模型评估在统计学和机器学习中具有至关重要,它帮助我们主要目标是量化模型预测新数据的能力. 在这个数据充斥的时代,没有评估的模型就如同盲人摸象,可能带来误导和误判.模型评估不仅是一种方法,更是一种保障, ...

  6. 14-TTL与非门的输入特性和输出特性

    TTL与非门的电压传输特性 传输特性 输入电压连续发生变化,输出电压发生什么变化?需要研究输出电压与输入电压之间的关系 输入小的时候,输出大的信号:输入大时候输出小信号 中间有截止和导通,需要过渡过程 ...

  7. 如何查找SpringBoot应用中的请求路径(不使用idea)

    背景 昨天有个同事向我咨询某个接口的物理表是哪个,由于公司业务较多.这块业务的确不是我负责的,也没有使用idea不能全局搜索(eclipse搜不到jar内的字符串),也就回复了不清楚. 除了自己写代码 ...

  8. [转帖]AF_UNIX和AF_INET

    https://www.cnblogs.com/shangerzhong/p/9153737.html family参数代表地址家族,比较常用的为AF_INET或AF_UNIX.AF_UNIX用于同一 ...

  9. [转帖]TiDB 环境与系统配置检查

    https://docs-archive.pingcap.com/zh/tidb/v6.0/check-before-deployment 本文介绍部署 TiDB 前的环境检查操作,以下各项操作按优先 ...

  10. [转帖]InnoDB表聚集索引层高什么时候发生变化

    导读 本文略长,主要解决以下几个疑问 1.聚集索引里都存储了什么宝贝 2.什么时候索引层高会发生变化 3.预留的1/16空闲空间做什么用的 4.记录被删除后的空间能回收重复利用吗 1.背景信息 1.1 ...