首页 > 其他分享 >flutter3+dart3聊天室|Flutter3跨平台仿微信App语音聊天/朋友圈

flutter3+dart3聊天室|Flutter3跨平台仿微信App语音聊天/朋友圈

时间:2024-02-05 16:35:24浏览次数:35  
标签:const color App Colors width 跨平台 朋友圈 context child

全新研发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开发跨平台项目,性能有了大幅度提升,官方支持编译到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实现圆角文本框及渐变按钮

Container(
  height: 40.0,
  margin: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 30.0),
  decoration: BoxDecoration(
    color: Colors.white,
    border: Border.all(color: const Color(0xffdddddd)),
    borderRadius: BorderRadius.circular(15.0),
  ),
  child: Row(
    children: [
      Expanded(
        child: TextField(
          keyboardType: TextInputType.phone,
          controller: fieldController,
          decoration: InputDecoration(
            hintText: '输入手机号',
            suffixIcon: Visibility(
              visible: authObj['tel'].isNotEmpty,
              child: InkWell(
                hoverColor: Colors.transparent,
                highlightColor: Colors.transparent,
                splashColor: Colors.transparent,
                onTap: handleClear,
                child: const Icon(Icons.clear, size: 16.0,),
              )
            ),
            contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12.0),
            border: const OutlineInputBorder(borderSide: BorderSide.none),
          ),
          onChanged: (value) {
            setState(() {
              authObj['tel'] = value;
            });
          },
        ),
      )
    ],
  ),
),

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

Container(
  margin: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 30.0),
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(15.0),
    // 自定义按钮渐变色
    gradient: const LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: [
        Color(0xFF0091EA), Color(0xFF07C160)
      ],
    )
  ),
  child: SizedBox(
    width: double.infinity,
    height: 45.0,
    child: FilledButton(
      style: ButtonStyle(
        backgroundColor: MaterialStateProperty.all(Colors.transparent),
        shadowColor: MaterialStateProperty.all(Colors.transparent),
        shape: MaterialStatePropertyAll(
          RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0))
        )
      ),
      onPressed: handleSubmit,
      child: const Text('登录', style: TextStyle(fontSize: 18.0),),
    ),
  )
),

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

Timer? timer;
String vcodeText = '获取验证码';
bool disabled = false;
int time = 60;

// 60s倒计时
void handleVcode() {
  if(authObj['tel'] == '') {
    snackbar('手机号不能为空');
  }else if(!Utils.checkTel(authObj['tel'])) {
    snackbar('手机号格式不正确');
  }else {
    setState(() {
      disabled = true;
    });
    startTimer();
  }
}
startTimer() {
  timer = Timer.periodic(const Duration(seconds: 1), (timer) {
    setState(() {
      if(time > 0) {
        vcodeText = '获取验证码(${time--})';
      }else {
        vcodeText = '获取验证码';
        time = 60;
        disabled = false;
        timer.cancel();
      }
    });
  });
  snackbar('短信验证码已发送,请注意查收', color: Colors.green);
}

Flutter3沉浸式渐变状态导航栏

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

AppBar(
  title: Text('Flutter3-Chat'),
  flexibleSpace: Container(
    decoration: const BoxDecoration(
      gradient: LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [
          Color(0xFF0091EA), Color(0xFF07C160)
        ],
      )
    ),
  )
),

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中引入字体文件。

class FStyle {
  // 自定义iconfont图标
  static iconfont(int codePoint, {double size = 16.0, Color? color}) {
    return Icon(
      IconData(codePoint, fontFamily: 'iconfont', matchTextDirection: true),
      size: size,
      color: color,
    );
  }

  // 自定义Badge红点
  static badge(int count, {
    Color color = Colors.redAccent,
    bool isdot = false,
    double height = 18.0,
    double width = 18.0
  }) {
    final num = count > 99 ? '99+' : count;
    return Container(
      alignment: Alignment.center,
      height: isdot ? height / 2 : height,
      width: isdot ? width / 2 : width,
      decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(100.00)),
      child: isdot ? null : Text('$num', style: const TextStyle(color: Colors.white, fontSize: 12.0)),
    );
  }
}

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

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

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

PopupMenuButton(
  icon: FStyle.iconfont(0xe62d, size: 17.0),
  offset: const Offset(0, 50.0),
  tooltip: '',
  color: const Color(0xFF353535),
  itemBuilder: (BuildContext context) {
    return <PopupMenuItem>[
      popupMenuItem(0xe666, '发起群聊', 0),
      popupMenuItem(0xe75c, '添加朋友', 1),
      popupMenuItem(0xe603, '扫一扫', 2),
      popupMenuItem(0xe6ab, '收付款', 3),
    ];
  },
  onSelected: (value) {
    switch(value) {
      case 0:
        print('发起群聊');
        break;
      case 1:
        Navigator.pushNamed(context, '/addfriends');
        break;
      case 2:
        print('扫一扫');
        break;
      case 3:
        print('收付款');
        break;
    }
  },
)
// 下拉菜单项
static popupMenuItem(int codePoint, String title, value) {
  return PopupMenuItem(
    value: value,
    child: Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        const SizedBox(width: 10.0,),
        FStyle.iconfont(codePoint, size: 21.0, color: Colors.white),
        const SizedBox(width: 10.0,),
        Text(title, style: const TextStyle(fontSize: 16.0, color: Colors.white),),
      ],
    ),
  );
}

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

EasyRefresh(
  // 下拉加载提示
  header: const ClassicHeader(
    // showMessage: false,
  ),
  // 加载更多提示
  footer: ClassicFooter(),
  // 下拉刷新逻辑
  onRefresh: () async {
    // ...下拉逻辑
    await Future.delayed(const Duration(seconds: 2));
  },
  // 上拉加载逻辑
  onl oad: () async {
    // ...
  },
  child: ListView.builder(
    itemCount: chatList.length,
    itemBuilder: (context, index) {
      return Ink(
        // ...
      );
    },
  ),
)

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

// 关于弹窗
void aboutAlertDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return UnconstrainedBox(
        constrainedAxis: Axis.vertical,
        child: SizedBox(
          width: 320.0,
          child: AlertDialog(
            contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
            backgroundColor: Colors.white,
            surfaceTintColor: Colors.white,
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
            content: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Image.asset('assets/images/logo.png', width: 90.0, height: 90.0, fit: BoxFit.cover,),
                  const SizedBox(height: 10.0),
                  const Text('Flutter3-WChat', style: TextStyle(color: Color(0xFF0091EA), fontSize: 22.0),),
                  const SizedBox(height: 5.0),
                  const Text('基于flutter3+dart3开发跨平台仿微信App聊天实例。', style: TextStyle(color: Colors.black45),),
                  const SizedBox(height: 20.0),
                  Text('©2024/01 Andy   Q: 282310962', style: TextStyle(color: Colors.grey[400], fontSize: 12.0),),
                ],
              ),
            ),
          ),
        ),
      );
    }
  );
}

// 二维码名片弹窗
void qrcodeAlertDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return UnconstrainedBox(
        constrainedAxis: Axis.vertical,
        child: SizedBox(
          width: 320.0,
          child: AlertDialog(
            contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
            backgroundColor: const Color(0xFF07C160),
            surfaceTintColor: const Color(0xFF07C160),
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3.0)),
            content: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Image.asset('assets/images/qrcode.png', width: 250.0, fit: BoxFit.cover,),
                  const SizedBox(height: 15.0),
                  const Text('扫一扫,加我公众号', style: TextStyle(color: Colors.white60, fontSize: 14.0,),),
                ],
              ),
            ),
          ),
        ),
      );
    }
  );
}

// 退出登录弹窗
void logoutAlertDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        content: const Text('确定要退出登录吗?', style: TextStyle(fontSize: 16.0),),
        backgroundColor: Colors.white,
        surfaceTintColor: Colors.white,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
        elevation: 2.0,
        actionsPadding: const EdgeInsets.all(15.0),
        actions: [
          TextButton(
            onPressed: () {Navigator.of(context).pop();},
            child: const Text('取消', style: TextStyle(color: Colors.black54),)
          ),
          TextButton(
            onPressed: handleLogout,
            child: const Text('退出登录', style: TextStyle(color: Colors.red),)
          ),
        ],
      );
    }
  );
}

flutter实现微信朋友圈九宫格

GroupZone(images: item['images']),

GroupZone(
  images: uploadList,
  album: true,
  onChoose: () async {
    Toast.show('选择手机相册图片', duration: 2, gravity: 1);
  },
),

// 创建可点击预览图片
createImage(BuildContext context, String img, int key) {
  return GestureDetector(
    child: Hero(
      tag: img, // 放大缩小动画效果标识
      child: img == '+' ? 
      Container(color: Colors.transparent, child: const Icon(Icons.add, size: 30.0, color: Colors.black45),)
      :
      Image.asset(
        img,
        width: width,
        fit: BoxFit.contain,
      ),
    ),
    onTap: () {
      // 选择图片
      if(img == '+') {
        onChoose!();
      }else {
        Navigator.of(context).push(FadeRoute(route: ImageViewer(
          images: album ? imgList!.sublist(0, imgList!.length - 1) : imgList,
          index: key,
        )));
      }
    },
  );
}

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

import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';

class ImageViewer extends StatefulWidget {
  const ImageViewer({
    super.key,
    this.images,
    this.index = 0,
  });

  final List? images; // 预览图列表
  final int index; // 当前预览图索引

  @override
  State<ImageViewer> createState() => _ImageViewerState();
}

class _ImageViewerState extends State<ImageViewer> {
  int currentIndex = 0;

  @override
  void initState() {
    super.initState();
    currentIndex = widget.index;
  }

  @override
  Widget build(BuildContext context) {
    var imgCount = widget.images?.length;

    return Scaffold(
      body: Stack(
        children: [
          Positioned(
            top: 0,
            left: 0,
            bottom: 0,
            right: 0,
            child: GestureDetector(
              child: imgCount == 1 ? PhotoView(
                imageProvider: AssetImage(widget.images![0]),
                backgroundDecoration: const BoxDecoration(
                  color: Colors.black,
                ),
                minScale: PhotoViewComputedScale.contained,
                maxScale: PhotoViewComputedScale.covered * 2,
                heroAttributes: PhotoViewHeroAttributes(tag: widget.images![0]),
                enableRotation: true,
              )
              :
              PhotoViewGallery.builder(
                itemCount: widget.images?.length,
                builder: (context, index) {
                  return PhotoViewGalleryPageOptions(
                    imageProvider: AssetImage(widget.images![index]),
                    minScale: PhotoViewComputedScale.contained,
                    maxScale: PhotoViewComputedScale.covered * 2,
                    heroAttributes: PhotoViewHeroAttributes(tag: widget.images![index]),
                  );
                },
                scrollPhysics: const BouncingScrollPhysics(),
                backgroundDecoration: const BoxDecoration(
                  color: Colors.black,
                ),
                pageController: PageController(initialPage: widget.index),
                enableRotation: true,
                onPageChanged: (index) {
                  setState(() {
                    currentIndex = index;
                  });
                },
              ),
              onTap: () {
                Navigator.of(context).pop();
              },
            ),
          ),
          // 图片索引index
          Positioned(
            top: MediaQuery.of(context).padding.top + 15,
            width: MediaQuery.of(context).size.width,
            child: Center(
              child: Visibility(
                visible: imgCount! > 1 ? true : false,
                child: Text('${currentIndex+1} / ${widget.images?.length}', style: const TextStyle(color: Colors.white)),
              )
            ),
          ),
        ],
      ),
    );
  }
}

flutter3聊天模块

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

// 输入框
Offstage(
  offstage: voiceBtnEnable,
  child: TextField(
    decoration: const InputDecoration(
      isDense: true,
      hoverColor: Colors.transparent,
      contentPadding: EdgeInsets.all(8.0),
      border: OutlineInputBorder(borderSide: BorderSide.none),
    ),
    style: const TextStyle(fontSize: 16.0,),
    maxLines: null,
    controller: editorController,
    focusNode: editorFocusNode,
    cursorColor: const Color(0xFF07C160),
    onChanged: (value) {},
  ),
),

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

// 语音
Offstage(
  offstage: !voiceBtnEnable,
  child: GestureDetector(
    child: Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(5),
      ),
      alignment: Alignment.center,
      height: 40.0,
      width: double.infinity,
      child: Text(voiceTypeMap[voiceType], style: const TextStyle(fontSize: 15.0),),
    ),
    onPanStart: (details) {
      setState(() {
        voiceType = 1;
        voicePanelEnable = true;
      });
    },
    onPanUpdate: (details) {
      Offset pos = details.globalPosition;
      double swipeY = MediaQuery.of(context).size.height - 120;
      double swipeX = MediaQuery.of(context).size.width / 2 + 50;
      setState(() {
        if(pos.dy >= swipeY) {
          voiceType = 1; // 松开发送
        }else if (pos.dy < swipeY && pos.dx < swipeX) {
          voiceType = 2; // 左滑松开取消
        }else if (pos.dy < swipeY && pos.dx >= swipeX) {
          voiceType = 3; // 右滑语音转文字
        }
      });
    },
    onPanEnd: (details) {
      // print('停止录音');
      setState(() {
        switch(voiceType) {
          case 1:
            Toast.show('发送录音文件', duration: 1, gravity: 1);
            voicePanelEnable = false;
            break;
          case 2:
            Toast.show('取消发送', duration: 1, gravity: 1);
            voicePanelEnable = false;
            break;
          case 3:
            Toast.show('语音转文字', duration: 1, gravity: 1);
            voicePanelEnable = true;
            voiceToTransfer = true;
            break;
        }
        voiceType = 0;
      });
    },
  ),
),

按住录音显示面板

// 录音主体(按住说话/松开取消/语音转文本)
Visibility(
  visible: voicePanelEnable,
  child: Material(
    color: const Color(0xDD1B1B1B),
    child: Stack(
      children: [
        // 取消发送+语音转文字
        Positioned(
          bottom: 120,
          left: 30,
          right: 30,
          child: Visibility(
            visible: !voiceToTransfer,
            child: Column(
              children: [
                // 语音动画层
                Stack(
                  children: [
                    Container(
                      height: 70.0,
                      margin: const EdgeInsets.symmetric(horizontal: 50.0),
                      decoration: BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.circular(15.0),
                      ),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Image.asset('assets/images/voice_record.gif', height: 30.0,)
                        ],
                      ),
                    ),
                    Positioned(
                      right: (MediaQuery.of(context).size.width - 60) / 2,
                      bottom: 1,
                      child: RotatedBox(
                        quarterTurns: 0,
                        child: CustomPaint(painter: ArrowShape(arrowColor: Colors.white, arrowSize: 10.0)),
                      )
                    ),
                  ],
                ),
                const SizedBox(height: 50.0,),
                // 操作项
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    // 取消发送
                    Container(
                      height: 60.0,
                      width: 60.0,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(50.0),
                        color: voiceType == 2 ? Colors.red : Colors.black38,
                      ),
                      child: const Icon(Icons.close, color: Colors.white54,),
                    ),
                    // 语音转文字
                    Container(
                      height: 60.0,
                      width: 60.0,
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(50.0),
                        color: voiceType == 3 ? Colors.green : Colors.black38,
                      ),
                      child: const Icon(Icons.translate, color: Colors.white54,),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
        // 语音转文字(识别结果状态)
        Positioned(
          bottom: 120,
          left: 30,
          right: 30,
          child: Visibility(
            visible: voiceToTransfer,
            child: Column(
              children: [
                // 提示结果
                Stack(
                  children: [
                    Container(
                      height: 100.0,
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(15.0),
                      ),
                      child: const Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Icon(Icons.info, color: Colors.white,),
                          Text('未识别到文字。', style: TextStyle(color: Colors.white),),
                        ],
                      ),
                    ),
                    Positioned(
                      right: 35.0,
                      bottom: 1,
                      child: RotatedBox(
                        quarterTurns: 0,
                        child: CustomPaint(painter: ArrowShape(arrowColor: Colors.red, arrowSize: 10.0)),
                      )
                    ),
                  ],
                ),
                const SizedBox(height: 50.0,),
                // 操作项
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    GestureDetector(
                      child: Container(
                        height: 60.0,
                        width: 60.0,
                        decoration: const BoxDecoration(
                          color: Colors.transparent,
                        ),
                        child: const Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.undo, color: Colors.white54,),
                            Text('取消', style: TextStyle(color: Colors.white70),)
                          ],
                        ),
                      ),
                      onTap: () {
                        setState(() {
                          voicePanelEnable = false;
                          voiceToTransfer = false;
                        });
                      },
                    ),
                    GestureDetector(
                      child: Container(
                        height: 60.0,
                        width: 100.0,
                        decoration: const BoxDecoration(
                          color: Colors.transparent,
                        ),
                        child: const Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.graphic_eq_rounded, color: Colors.white54,),
                            Text('发送原语音', style: TextStyle(color: Colors.white70),)
                          ],
                        ),
                      ),
                      onTap: () {},
                    ),
                    GestureDetector(
                      child: Container(
                        height: 60.0,
                        width: 60.0,
                        decoration: BoxDecoration(
                          borderRadius: BorderRadius.circular(50.0),
                          color: Colors.white12,
                        ),
                        child: const Icon(Icons.check, color: Colors.white12,),
                      ),
                      onTap: () {},
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
        // 提示文字(操作状态)
        Positioned(
          bottom: 120,
          left: 0,
          width: MediaQuery.of(context).size.width,
          child: Visibility(
            visible: !voiceToTransfer,
            child: Align(
              child: Text(voiceTypeMap[voiceType], style: const TextStyle(color: Colors.white70),),
            ),
          ),
        ),
        // 背景
        Align(
          alignment: Alignment.bottomCenter,
          child: Visibility(
            visible: !voiceToTransfer,
            child: Image.asset('assets/images/voice_record_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill),
          ),
        ),
        // 背景图标
        Positioned(
          bottom: 25,
          left: 0,
          width: MediaQuery.of(context).size.width,
          child: Visibility(
            visible: !voiceToTransfer,
            child: const Align(
              child: Icon(Icons.graphic_eq_rounded, color: Colors.black54,),
            ),
          ),
        ),
      ],
    ),
  ),
)

flutter3绘制箭头

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

// 绘制气泡箭头
class ArrowShape extends CustomPainter {
  ArrowShape({
    required this.arrowColor,
    this.arrowSize = 7,
  });

  final Color arrowColor; // 箭头颜色
  final double arrowSize; // 箭头大小

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

    var path = Path();
    path.lineTo(-arrowSize, 0);
    path.lineTo(0, arrowSize);
    path.lineTo(arrowSize, 0);
    canvas.drawPath(path, paint);
  }

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

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

标签:const,color,App,Colors,width,跨平台,朋友圈,context,child
From: https://www.cnblogs.com/xiaoyan2017/p/18008370

相关文章

  • app自动化测试环境搭建
    一,安装AppiumAppium和node.js一起安装(据说两者有依赖关系)。1,node.js傻瓜式安装官网地址:https://nodejs.org/en/安装完成后,运行cmd,输入node–v,如果安装成功,提示版本号2,安装Appium官网下载对应的版本并安装,官网地址:https://bitbucket.org/appium/appium.app/downloads/......
  • APP在申请敏感权限时,应同步说明权限申请的使用目的,包括但不限于申请权限的名称、服务
    一、概述华为APP应用市场反馈问题:(目前是华为审核特有的问题)APP在申请敏感权限时,应同步说明权限申请的使用目的,包括但不限于申请权限的名称、服务的具体功能、用途;告知方式不限于弹窗、蒙层、浮窗、或者自定义操作系统权限弹框等验证了小米、vivo、oppo、应用宝,只......
  • 实战经验分享:开发直播带货商城APP的技术心得
    近期,小编有幸参与了一项直播带货商城APP的开发项目,积累了丰富的技术经验。在这篇文章中,我将分享一些在开发过程中学到的技术心得,希望对同行和对这一领域感兴趣的开发者有所启发。一、项目概述直播带货商城APP的开发并非易事,需要综合考虑直播功能、商品展示、支付系统、用户体验等多......
  • uniapp 选择城市 根据城市首字母分类排序
     selectCity.vue<template><viewclass="select-city"><viewclass="search"><van-fieldclass="select-input"input-align="left":value="searchVal"placeholder=&......
  • 微信支付普通商户与AppID账号关联管理-授权
     微信支付普通商户与AppID账号关联管理二、名词解释名词释义微信支付普通商户公司企业、政府机关、事业单位、社会组织、个体工商户、个人卖家、小微商户。(微信支付商户接入指引)AppID已通过微信认证的服务号,订阅号*[1],小程序,企业微信*[2],移动应用*[3]......
  • CSAPP 第二章 信息的表示与处理(2) 整数运算
    加减法运算所有的加法运算在内存中的运算都遵循二进制的计算法则,只不过因为相同二进制表示在不同整数类型下表示的数不同,运算法则也有所不同。无符号加法计算规则可以将无符号数的加法视作是一种模运算,在二进制表示中丢弃掉溢出的位的操作就......
  • 在Windows 10中,C:\Users\Administrator\AppData目录下存储了当前用户(Administrator
    在Windows10中,C:\Users\Administrator\AppData目录下存储了当前用户(Administrator)的应用程序数据。该目录包含了许多子文件夹,每个子文件夹有不同的功能和用途。以下是一些常见的子文件夹:Local:用于存储应用程序的本地数据,如缓存文件、临时文件等。Roaming:用于存储应用程序的配......
  • PowerShell是一种跨平台的任务自动化解决方案,包括命令行shell、脚本语言和配置管理框
    PowerShell是一种跨平台的任务自动化解决方案,包括命令行shell、脚本语言和配置管理框架。PowerShell运行在Windows、Linux和macOS上。创建一个思维导图来概述PowerShell命令可能包括以下几个主要部分:基础命令操作Get-Help:获取命令帮助Get-Command:查找命令Get-Member:查看对......
  • openharmony4.0 实现网络 app, 鸿蒙
    问题:拿到了开发者手机,但是再测试网络部分的时候出现问题。记录一下。 解决:需要再应用中对网络进行配置,便可以直接复制官方文档中的例子。 1官方文档的路径。https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis/js-apis-http.md......
  • 5-Docker实例-nginx application
    1.编写Dockerfile文件,添加如下内容并保存命令:vimDockerfile[root@centos79~]#catDockerfileFROMcentos:centos7MAINTAINERztjENVPATH/usr/local/nginx/sbin:$PATHADDnginx-1.8.0.tar.gz/usr/local/ADDepel-release-latest-7.noarch.rpm/usr/local/RUNrpm-......