Flutter QQ聊天项目(1):登录界面实现
下面介绍 Flutter 最基本的通用项目框架搭建,顺便实现一个 QQ 聊天界面,这里只先实现了 QQ 登录界面。先看下效果图:
一、运用技术
- 编辑器:VS Code
- 框架技术:Flutter3.104 + Dart3.105
- 路由/状态管理:get^4.6.6
更具体的:
- 使用
ScreenUtilInit
自适应界面大小; - 使用
Stack
支持多个子界面在同一个全屏主界面上选择显示; - 使用 Get 插件实现界面之间的跳转和国际化翻译;
- 界面都通过
Transform
实现了鼠标移动界面; - 使用
Controller.dart
管理所有全局变量和界面控制器;
二、项目目录结构
asserts\images
:存放图片资源文件的目录;Translation.dart
:翻译文件;Controller.dart
:全局变量和对应控制器的定义;Utils.dart
:通用配置文件;LoginWidget.dart
:登录界面;main.dart
:主界面;
三、代码实现与分析
2.1 pubspec.yaml
pubspec.yaml
的内容如下:
dependencies:
flutter:
sdk: flutter
flutter_screenutil: ^5.9.3
get: ^4.6.5
flutter:
assets:
- assets/images/
因为用到了 Get 插件与 ScreenUtilInit,所以需要加上这两种的依赖;另外定义了图片资源文件的路径;
2.2 Translation.dart
实现了中文简体、中文繁体和英文的语言切换,翻译文件如下所示:
import 'package:get/get.dart';
// (2)自定义自己的国际化字符串
class Translation extends Translations {
@override
Map<String, Map<String, String>> get keys => {
// 1-配置中文简体
'zh_CN': {
'用户': '用户',
'密码': '密码',
'记住密码': '记住密码',
'自动登录': '自动登录',
'登录': '登录',
'注册账号': '注册账号',
'找回密码': '找回密码',
'用户名异常': '用户名异常',
'用户名为空': '用户名为空',
'密码异常': '密码异常',
'密码为空': '密码为空',
'用户名、密码正确': '用户名、密码正确',
'去登陆': '去登陆',
},
// 2-配置中文繁体
'zh_HK': {
'用户': '用戶',
'密码': '密碼',
'记住密码': '記住密碼',
'自动登录': '自動登錄',
'登录': '登錄',
'注册账号': '註冊賬號',
'找回密码': '找回密碼',
'用户名异常': '用戶名異常',
'用户名为空': '用戶名爲空',
'密码异常': '密碼異常',
'密码为空': '密碼爲空',
'用户名、密码正确': '用戶名、密碼正確',
'去登陆': '去登陸',
},
// 3-配置英文
'en_US': {
'用户': 'user',
'密码': 'password',
'记住密码': 'Remember pw',
'自动登录': 'Auto login',
'登录': 'Login',
'注册账号': 'Register',
'找回密码': 'Retrieve',
'用户名异常': 'Abnormal username',
'用户名为空': 'The username is empty',
'密码异常': 'Password exception',
'密码为空': 'Password is empty',
'用户名、密码正确': 'The username and password are correct',
'去登陆': 'Go log in',
}
};
}
这里使用了 Get 插件方式实现国际化翻译,具体可参考:Flutter插件Get(7):实现语言的国际化 - fengMisaka - 博客园
2.3 Controller.dart
全局变量和对应控制器的定义:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../Widget/LoginWidget.dart';
// state只专注数据,需要使用数据,直接通过state获取
// logic只专注于触发事件交互,操作或更新数据
// view只专注UI显示
// 按钮状态
enum ButtonState {
BTN_NORMAL,
BTN_PRESSED,
BTN_HOVER,
}
// 全局状态
class GlobalState {
final screenSize = const Size(1250,680).obs; // 屏幕尺寸
final screenSizeRight = const Size(1014,680).obs; // 右侧屏幕尺寸
var language = const Locale('zh', 'CN').obs; //语言参数
final curAcc = ''.obs; // 当前账号
final globalMousePressPos = Offset.zero.obs; // 全局点击坐标
late GlobalKey key = GlobalKey();
}
// 全局变量控制器
class GlobalController extends GetxController {
// 全局变量, 内部调用
final GlobalState _globalState = GlobalState();
// 获取屏幕尺寸与设置屏幕尺寸的函数
Size get screenSize => _globalState.screenSize.value;
set screenSize(Size value) => _globalState.screenSize.value = value;
// 获取右侧屏幕尺寸与设置右侧屏幕尺寸的函数
Size get screenSizeRight => _globalState.screenSizeRight.value;
set screenSizeRight(Size value) => _globalState.screenSizeRight.value = value;
// 获取当前语言与设置当前语言的函数
Locale get language => _globalState.language.value;
set language(Locale language) => () {
_globalState.language.value = language;
Get.updateLocale(language);
}();
// 获取当前账号与设置当前账号的函数
String get curAcc => _globalState.curAcc.value;
set curAcc(String acc) => _globalState.curAcc.value = acc;
Offset get globalMousePressPos => _globalState.globalMousePressPos.value;
set globalMousePressPos(Offset pos) => _globalState.globalMousePressPos.value = pos;
GlobalKey get key => _globalState.key;
set key(GlobalKey val) => _globalState.key = val;
}
// 定义全局变量控制器
final GlobalController globalCtrl = Get.put(GlobalController());
// 初始化通用配置
void initCommomCfg() {
Get.lazyPut<LoginController>(() => LoginController());
}
2.4 LoginWidget.dart
登录界面:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../Util/Controller.dart';
// 状态类
class LoginState {
final _isHidden = false.obs; // 是否隐藏
final _width = 400.0.obs; // 宽度
final _height = 328.0.obs; // 高度
final _offset = const Offset(0, 0).obs; // 位置
final _isLogined = false.obs; // 是否登陆完成
}
// 控制器类
class LoginController extends GetxController {
final LoginState state = LoginState();
double get width => state._width.value;
set width(double value) => state._width.value = value;
double get height => state._height.value;
set height(double value) => state._height.value = value;
Offset get offset => state._offset.value;
set offset(Offset value) => state._offset.value = value;
bool get isLogined => state._isLogined.value;
set isLogined(bool flag) => state._isLogined.value = flag;
LoginController() {
// 新加:初始化登录界面的偏移
state._offset.value = Offset(
(globalCtrl.screenSize.width - state._width.value) / 2,
(globalCtrl.screenSize.height - state._height.value) / 2);
}
// 是否隐藏
bool isHidden() {
return state._isHidden.value;
}
// 显示
void show() {
state._isHidden.value = false;
}
// 隐藏
void hide() {
state._isHidden.value = true;
}
// 设置窗口显示/隐藏状态
void setVisable(bool isVisable) {
state._isHidden.value = !isVisable;
}
// 移动
void move(double x, double y) {
state._offset.value = Offset(x, y);
}
// 登陆按钮点击事件
login(TextEditingController userNameController,
TextEditingController passWordController) {
var userName = userNameController.text;
var passWord = passWordController.text;
// 用户名判断
if (userName.isEmpty) {
Get.snackbar('用户名异常'.tr, '用户名为空'.tr, snackPosition: SnackPosition.BOTTOM);
return;
}
// 密码判断
if (passWord.isEmpty) {
Get.snackbar('密码异常'.tr, '密码为空'.tr, snackPosition: SnackPosition.BOTTOM);
return;
}
Get.snackbar('用户名、密码正确'.tr, '去登陆'.tr, snackPosition: SnackPosition.BOTTOM);
}
}
// 登陆界面
class LoginWidget extends StatelessWidget {
LoginWidget({super.key});
final LoginController loginCtrl = Get.find<LoginController>(); // 登录界面控制器
@override
Widget build(BuildContext context) {
return Positioned(
left: (globalCtrl.screenSize.width - loginCtrl.width) / 2,
top: (globalCtrl.screenSize.height - loginCtrl.height) / 2,
child: Obx(() => Transform.translate(
// 让部件在 x、y 轴上平移指定的距离
offset: loginCtrl.offset, // 平移距离
child: GestureDetector(
// 手势识别组件,以让鼠标移动到该组件上时光标为"选中样式"
behavior: HitTestBehavior.opaque,
child: Visibility(
// 是一个用于根据布尔值条件显示或隐藏小部件的控件
visible: !loginCtrl.isHidden(), // 控制是否显示
maintainState: true,
child: Container(
width: loginCtrl.width,
height: loginCtrl.height,
padding: const EdgeInsets.symmetric(horizontal: 0.0),
decoration: const BoxDecoration(
color: Color.fromARGB(255, 235, 242, 249),
),
child: Column(
mainAxisAlignment:
MainAxisAlignment.start, // 确保子部件从顶部开始排列
crossAxisAlignment:
CrossAxisAlignment.start, // 确保子部件从左侧开始排列
children: const [
// 登录界面标题栏
LoginTabBar(),
// 下方编辑界面
InputWidget(),
],
),
)),
// 按压拖动回调,以支持鼠标移动界面
onPanUpdate: (details) {
loginCtrl.offset += details.delta;
}))));
}
}
// 登录界面标题栏
class LoginTabBar extends StatefulWidget {
const LoginTabBar({super.key});
@override
// ignore: library_private_types_in_public_api
_LoginTabBarState createState() => _LoginTabBarState();
}
// 登录界面标题栏实现
class _LoginTabBarState extends State<LoginTabBar> {
// 控制器定义
final LoginController loginCtrl = Get.find<LoginController>();
// 当前图片路径
String currentImagePath = 'assets/Widget/blue70-2.gif';
// 图片路径列表
final Map<String, String> imagePaths = {
'蓝色': 'assets/Widget/blue70-2.gif',
'红色': 'assets/Widget/red69-2.gif',
'紫色': 'assets/Widget/purple0.4.gif',
};
@override
Widget build(BuildContext context) {
return Container(
height: 170,
color: Colors.transparent,
padding: EdgeInsets.zero, // 容器内部的间距为0(如果有的话)
child: Stack(
alignment: Alignment.center,
children: <Widget>[
// 动态gif动图
Image.asset(
currentImagePath, // 替换为你的GIF路径
width: loginCtrl.width,
height: 170,
fit: BoxFit.cover, // 调整图片的填充方式
),
// 语言切换按钮
Positioned(
right: 74,
top: 4,
child: SizedBox(
width: 30,
height: 30,
child: Obx(() => IconButton(
onPressed: () {
if (globalCtrl.language == const Locale('zh', 'CN')) {
globalCtrl.language = const Locale('zh', 'HK');
} else if (globalCtrl.language ==
const Locale('zh', 'HK')) {
globalCtrl.language = const Locale('en', 'US');
} else if (globalCtrl.language ==
const Locale('en', 'US')) {
globalCtrl.language = const Locale('zh', 'CN');
}
},
icon: () {
if (globalCtrl.language == const Locale('zh', 'CN')) {
return Image.asset(
"assets/Widget/btn_Chinese_jianti.png",
width: 30,
height: 30);
} else if (globalCtrl.language ==
const Locale('zh', 'HK')) {
return Image.asset(
"assets/Widget/btn_Chinese_fanti.png",
width: 30,
height: 30);
} else if (globalCtrl.language ==
const Locale('en', 'US')) {
return Image.asset("assets/Widget/btn_English.png",
width: 30, height: 30);
} else {
return const Icon(null);
}
}(),
padding: EdgeInsets.zero,
)),
),
),
// 动图设置菜单按钮
Positioned(
right: 38,
top: 0,
child: PopupMenuButton<String>(
icon: const Icon(
Icons.settings,
size: 24,
), // 使用当前图标作为按钮图标
onSelected: (String key) {
// 更新当前图片路径
setState(() {
currentImagePath = imagePaths[key]!;
});
},
itemBuilder: (BuildContext context) {
return imagePaths.keys.map((String key) {
return PopupMenuItem<String>(
value: key,
child: Text(key),
);
}).toList();
},
),
),
// 关闭按钮
Positioned(
right: 4,
top: 0,
child: IconButton(
onPressed: () {
loginCtrl.hide(); // 隐藏登录界面
},
icon: const Icon(
Icons.close,
size: 26,
),
padding: EdgeInsets.zero,
),
),
],
),
);
}
}
// 下方输入界面
class InputWidget extends StatefulWidget {
const InputWidget({super.key});
@override
// ignore: library_private_types_in_public_api
_InputWidgetState createState() => _InputWidgetState();
}
// 下方输入界面实现
class _InputWidgetState extends State<InputWidget> {
// 控制器定义
final LoginController loginCtrl = Get.find<LoginController>();
// 编辑框控制器
final userNameController = TextEditingController();
final passWordController = TextEditingController();
// 勾选框否选状态
bool isCheckedRem = false;
bool isCheckedAuto = false;
@override
Widget build(BuildContext context) {
return Container(
height: 158,
padding: EdgeInsets.zero, // 两侧边距
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 水平均匀分布
children: [
// QQ头像
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Image.asset(
'assets/Widget/QQ_3D2.png', // 替换为你的GIF路径
width: 84,
height: 96,
fit: BoxFit.cover, // 调整图片的填充方式
),
]),
// 编辑区域-垂直布局
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 距离上一个View距离
const SizedBox(height: 8),
// 用户编辑框
SizedBox(
width: 200,
height: 30,
child: TextField(
controller: userNameController,
decoration: InputDecoration(
hintText: '用户'.tr,
hintStyle: TextStyle(color: Color.fromARGB(255, 127, 127, 127), fontSize: 14.0),
filled: true, // 启用背景填充
fillColor: Colors.white, // 背景颜色
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none, // 去掉边框
),
),
)),
// 距离上一个View距离
const SizedBox(height: 6),
// 密码编辑框
SizedBox(
width: 200,
height: 30,
child: TextField(
controller: passWordController,
decoration: InputDecoration(
hintText: '密码'.tr,
hintStyle: TextStyle(color: Color.fromARGB(255, 127, 127, 127), fontSize: 14.0),
filled: true, // 启用背景填充
fillColor: Colors.white, // 背景颜色
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none, // 去掉边框
),
),
)),
//距离上一个View距离
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 水平均匀分布
children: [
// 记住密码勾选框
Transform.scale( // 使用Transform.scale来设置勾选框的大小
scale: 0.9, // 缩放比例,2.0 表示放大两倍
child: Checkbox(
value: isCheckedRem, // 当前复选框的值,表示是否选中
onChanged: (bool? newValue) { // 当复选框的值改变时调用
setState(() {
isCheckedRem = newValue ?? false;
});
},
activeColor: Colors.blue, // 选中时的颜色
checkColor: Colors.white, // 选中标记的颜色
),
),
// 文本
Text(
'记住密码'.tr,
style: TextStyle(
fontSize: 13.0,
color: Color.fromARGB(255, 44, 44, 44),
),
),
// 距离上一个View距离
const SizedBox(height: 12),
// 自动登录勾选框
Transform.scale( // 使用Transform.scale来设置勾选框的大小
scale: 0.9, // 缩放比例,2.0 表示放大两倍
child: Checkbox(
value: isCheckedAuto, // 当前复选框的值,表示是否选中
onChanged: (bool? newValue) { // 当复选框的值改变时调用
setState(() {
isCheckedAuto = newValue ?? false;
});
},
activeColor: Colors.blue, // 选中时的颜色
checkColor: Colors.white, // 选中标记的颜色
),
),
// 文本
Text(
'自动登录'.tr,
style: TextStyle(
fontSize: 13.0,
color: Color.fromARGB(255, 44, 44, 44),
),
),
]),
//距离上一个View距离
const SizedBox(height: 6),
// 登录按钮
SizedBox(
width: 200,
height: 32,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Color.fromARGB(255, 14, 150, 254)), // 按扭背景颜色
foregroundColor:
WidgetStateProperty.all(Colors.white), // 按钮文本颜色
shape: WidgetStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6))), // 圆角
),
child: Text('登录'.tr),
onPressed: () {
debugPrint("ElevatedButton Click");
// 登录验证
loginCtrl.login(userNameController, passWordController);
// 设置当前账号
globalCtrl.curAcc = userNameController.text;
// 打印当前账号
debugPrint("globalLoginName: ${globalCtrl.curAcc}");
// 登录成功后,隐藏登录界面(本界面)
//loginCtrl.hide();
},
)),
//距离上一个View距离
const SizedBox(height: 8),
],
),
// 注册账号、找回密码按钮区域-垂直布局
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 距离上一个View距离
const SizedBox(height: 8),
// 注册账号按钮
TextButton(
child: Text(
'注册账号'.tr,
style: TextStyle(
fontSize: 13.0,
color: Color.fromARGB(255, 9, 163, 220),
),
),
onPressed: () {},
),
// 距离上一个View距离
const SizedBox(height: 8),
// 找回密码按钮
TextButton(
child: Text(
'找回密码'.tr,
style: TextStyle(
fontSize: 13.0,
color: Color.fromARGB(255, 9, 163, 220),
),
),
onPressed: () {},
),
])
]),
);
}
}
2.5 main.dart
主界面:
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'Controller.dart';
import 'Translation.dart';
import 'LogoWidget.dart';
import 'LoginWidget.dart';
// 主函数
main(List<String> args) {
// 初始化通用配置
initCommomCfg();
runApp(MainApp());
}
// 主界面
class MainApp extends StatelessWidget {
MainApp({super.key});
// 各界面的实例
final LoginWidget loginWidget = LoginWidget();
final LogoWidget logoWidget = LogoWidget();
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: Size(1920, 1080),
builder: (context, child) {
return GetMaterialApp(
// 配置GetMaterialApp
translations: Translation(), // 你的翻译
locale: const Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
fallbackLocale: const Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
debugShowCheckedModeBanner: false,
theme: ThemeData(fontFamily: "Ali"),
home: Scaffold(
backgroundColor: Colors.white,
body: Stack( // 使用Stack以同时选择显示多个子界面在同一个主界面中
alignment: Alignment.center,
children: <Widget>[
logoWidget,
loginWidget,
],
),
),
);
},
);
}
}
四、程序下载
程序下载:Flutter_Demo/qq_chat-1 at main · confidentFeng/Flutter_Demo · GitHub
分类:
Flutter
, Flutter / Flutter项目
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
2021-03-04 OBJ格式模型详细介绍
2021-03-04 计算机图形学底层知识