flutter3-wchat一款基于flutter3+dart3+material-ui
技术构建的跨多端仿微信聊天项目。
flutter3已经支持全终端项目开发,可编译到android/ios/windows/macos/linux/web等多个平台。
使用flutter create flutter_chat
命令创建项目,生成的多端结构如下:
在开始开发之前,需要先配置好相应的开发环境。
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:toast/toast.dart';
// 引入公共样式
import 'styles/index.dart';
// 引入底部tabbar
import 'components/tabbar.dart';
// 引入路由管理
import 'router/index.dart';
// 错误模块
import '../views/error/index.dart';
void main() {
runApp(const MyApp());
}
DateTime? lastPopTime;
class MyApp extends StatelessWidget {
const MyApp({ super.key });
// 退出app提示
Future<bool> appOnPopInvoked(didPop) async {
if(lastPopTime == null || DateTime.now().difference(lastPopTime!) > const Duration(seconds: 2)) {
lastPopTime = DateTime.now();
Toast.show('再按一次退出应用');
return false;
}
SystemNavigator.pop();
return true;
}
@override
Widget build(BuildContext context){
ToastContext().init(context);
return MaterialApp(
title: 'Flutter Chat',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: FStyle.primaryColor,
useMaterial3: true,
// windows桌面端字体粗细不一样
fontFamily: Platform.isWindows ? 'Microsoft YaHei' : null,
),
// home: const FTabBar(),
home: PopScope(
// canPop: false,
onPopInvoked: appOnPopInvoked,
child: const FTabBar(),
),
// 初始路由
// initialRoute: '/',
// 自定义路由
onGenerateRoute: onGenerateRoute,
// 错误路由
onUnknownRoute: (settings) {
return MaterialPageRoute(builder: (context) => const Error());
},
);
}
}
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);
}
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组件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中Appbar组件background属性只能单纯设置颜色,不能设置渐变背景。可通过FlexibleSpace
属性设置背景渐变色/渐变背景图。
AppBar(
title: Text('Flutter3-Chat'),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0091EA), Color(0xFF07C160)
],
)
),
)
),
flutter3实现各种弹窗效果。
// 关于弹窗
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),)
),
],
);
}
);
}
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,
)));
}
},
);
}
// 语音
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,),
),
),
),
],
),
),
)
Okay,以上就是Flutter3/Dart3仿微信App聊天的一些知识分享。后续还会分享一些其它flutter实例项目。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。