Browse Source

feat(profile): 添加统一头像组件解决闪烁问题

- 创建 UnifiedAvatar 组件统一处理所有头像显示逻辑
- 在 ProfileController 中添加 isAvatarReady 状态防止闪烁
- 修改多个视图使用新的统一头像组件
- 优化头像加载流程,确保本地和远程头像一致性
weicu
lxm 1 year ago
parent
commit
5e9b74af65
  1. 39
      lib/modules/meeting/views/meeting_mine_view.dart
  2. 40
      lib/modules/profile/controllers/profile_controller.dart
  3. 127
      lib/modules/profile/views/profile_meeting_view.dart
  4. 23
      lib/modules/profile/views/profile_view.dart
  5. 137
      lib/modules/profile/views/unified_avatar.dart
  6. 61
      lib/modules/settings/views/settings_view.dart

39
lib/modules/meeting/views/meeting_mine_view.dart

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'dart:io';
import '../../../routes/app_routes.dart';
import '../../profile/controllers/profile_controller.dart';
import '../../profile/views/unified_avatar.dart';
import '../controllers/meeting_mine_controller.dart';
import 'bottomSheet/statistics_bottom_sheet.dart';
@ -104,32 +104,17 @@ class MeetingMineView extends GetView<MeetingMineController> {
onTap: () => Get.toNamed(Routes.profileMeeting), // 跳转到个人信息页面
child: Row(
children: [
// 统一头像显示逻辑:本地头像 > 网络头像 > 默认图标
Obx(() => Container(
width: 50.w,
height: 50.w,
margin: EdgeInsets.only(right: 10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.w),
),
child: CircleAvatar(
radius: 25.w,
backgroundImage: profileCtrl.localAvatar.value.isNotEmpty
? FileImage(File(profileCtrl.localAvatar.value))
: (profileCtrl.userInfo.value.avatar.isNotEmpty
? NetworkImage(profileCtrl.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
backgroundColor: Colors.grey[200],
child: (profileCtrl.localAvatar.value.isEmpty &&
profileCtrl.userInfo.value.avatar.isEmpty)
? Icon(
Icons.person,
size: 25.r,
color: Colors.grey[600],
)
: null,
),
)),
// 使用统一头像组件,避免闪烁问题
Container(
margin: EdgeInsets.only(right: 10.w),
child: UnifiedAvatar(
radius: 25.w,
backgroundColor: Colors.grey[200],
iconColor: Colors.grey[600],
iconSize: 25.r,
isDarkMode: false,
),
),
Expanded(
child: Obx(
() => Text(

40
lib/modules/profile/controllers/profile_controller.dart

@ -31,6 +31,7 @@ class ProfileController extends GetxController {
final RxBool hasEdits = false.obs; // 是否有编辑
final RxBool isUpdating = false.obs; // 是否正在更新用户信息
final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态)
final RxBool isAvatarReady = false.obs; // 头像数据是否已准备好,用于防止闪烁
//音乐播放管理
final _userportraitManager = Get.find<UserportraitManager>();
// 用于乐观更新的原始状态备份
@ -289,19 +290,23 @@ class ProfileController extends GetxController {
}
}
void loadUserInfo() {
void loadUserInfo() async {
final userJson = _storage.read(userKey);
if (userJson != null) {
userInfo.value = User.instance;
Logger().d('从本地加载用户信息: $userJson');
// 加载本地头像路径并验证文件有效性
// 重置头像准备状态
isAvatarReady.value = false;
// 同步加载本地头像路径并验证文件有效性 - 修复闪烁问题
final savedLocalAvatar = _storage.read('local_avatar');
if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) {
final avatarPath = savedLocalAvatar.toString();
// 验证文件是否存在
// 同步验证文件是否存在,避免异步导致的闪烁
final file = File(avatarPath);
file.exists().then((exists) {
try {
final exists = await file.exists();
if (exists) {
localAvatar.value = avatarPath;
Logger().d('加载本地头像路径: ${localAvatar.value}');
@ -311,18 +316,26 @@ class ProfileController extends GetxController {
_storage.remove('local_avatar');
Logger().w('本地头像文件不存在,已清除路径: $avatarPath');
}
});
} catch (e) {
// 文件访问异常,清除无效路径
localAvatar.value = '';
_storage.remove('local_avatar');
Logger().e('验证本地头像文件时发生异常: $e');
}
} else {
// 确保本地头像路径为空
localAvatar.value = '';
Logger().d('没有本地头像路径');
}
// 头像数据准备完成
isAvatarReady.value = true;
// 保存原始状态用于回滚
_saveOriginalState();
// 如果有远程头像地址,下载并保存到本地
if (userInfo.value.avatar.isNotEmpty) {
// 如果有远程头像地址且本地头像不存在,才下载并保存到本地
if (userInfo.value.avatar.isNotEmpty && localAvatar.value.isEmpty) {
Logger().d('远程头像地址: ${userInfo.value.avatar}');
_downloadAndSaveAvatar(userInfo.value.avatar);
}
@ -333,11 +346,12 @@ class ProfileController extends GetxController {
}
// 确保本地状态与服务器状态的一致性
void _ensureStateConsistency() {
// 检查本地头像文件是否存在
void _ensureStateConsistency() async {
// 同步检查本地头像文件是否存在 - 修复闪烁问题
if (localAvatar.value.isNotEmpty) {
final file = File(localAvatar.value);
file.exists().then((exists) {
try {
final exists = await file.exists();
if (!exists) {
Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}');
localAvatar.value = '';
@ -348,7 +362,11 @@ class ProfileController extends GetxController {
_downloadAndSaveAvatar(userInfo.value.avatar);
}
}
});
} catch (e) {
Logger().e('检查本地头像文件一致性时发生异常: $e');
localAvatar.value = '';
_storage.remove('local_avatar');
}
}
}

127
lib/modules/profile/views/profile_meeting_view.dart

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
import 'unified_avatar.dart';
class ProfileMeetingView extends GetView<ProfileController> {
ProfileMeetingView({super.key});
@ -100,15 +101,15 @@ class ProfileMeetingView extends GetView<ProfileController> {
SizedBox(height: 16.h),
_buildNicknameItem(isDarkMode),
Divider(
height: 1.h,
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildLoginMethodItem(isDarkMode),
Divider(
height: 1.h,
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildManageLoginItem(isDarkMode),
Divider(
height: 1.h,
height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
SizedBox(height: 32.h),
_buildLogoutButton(isDarkMode),
@ -126,55 +127,43 @@ class ProfileMeetingView extends GetView<ProfileController> {
return Center(
child: Column(
children: [
Obx(() => GestureDetector(
onTap: () => controller.updateUserAvatar(),
child: Stack(
children: [
CircleAvatar(
radius: 40.r,
backgroundImage: controller.localAvatar.value.isNotEmpty
? FileImage(File(controller.localAvatar.value))
: (controller.userInfo.value.avatar.isNotEmpty
? NetworkImage(controller.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[200],
child: (controller.localAvatar.value.isEmpty &&
controller.userInfo.value.avatar.isEmpty)
? Icon(
Icons.person,
size: 30.r,
color: isDarkMode
? Colors.grey[400]
: Colors.grey[600],
)
: null,
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 22.w,
height: 22.w,
decoration: BoxDecoration(
color:
isDarkMode ? Colors.grey[600] : Colors.grey[350],
shape: BoxShape.circle,
border: Border.all(
color: isDarkMode ? Colors.black : Colors.white,
width: 0.w,
),
),
child: Icon(
Icons.camera_alt,
size: 14.w,
color: Colors.black,
),
// 使用统一头像组件,避免闪烁问题
GestureDetector(
onTap: () => controller.updateUserAvatar(),
child: Stack(
children: [
UnifiedAvatar(
radius: 40.r,
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[200],
iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600],
iconSize: 30.r,
isDarkMode: isDarkMode,
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 22.w,
height: 22.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[600] : Colors.grey[350],
shape: BoxShape.circle,
border: Border.all(
color: isDarkMode ? Colors.black : Colors.white,
width: 0.w,
),
),
],
child: Icon(
Icons.camera_alt,
size: 14.w,
color: Colors.black,
),
),
),
)),
],
),
),
],
),
);
@ -336,9 +325,8 @@ class ProfileMeetingView extends GetView<ProfileController> {
child: OutlinedButton(
onPressed: () => _showLogoutDialog(isDarkMode),
style: OutlinedButton.styleFrom(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50],
side: BorderSide(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!),
backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200],
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
@ -358,10 +346,11 @@ class ProfileMeetingView extends GetView<ProfileController> {
return SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
child: OutlinedButton(
onPressed: () => _showDeleteAccountDialog(isDarkMode),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
style: OutlinedButton.styleFrom(
side: BorderSide.none,
backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
@ -370,7 +359,7 @@ class ProfileMeetingView extends GetView<ProfileController> {
'删除账号',
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
color: Colors.red[600],
),
),
),
@ -378,11 +367,25 @@ class ProfileMeetingView extends GetView<ProfileController> {
}
Widget _buildWarningText() {
return Text(
'警告:您的账号信息和所有文件将被永久删除。',
style: TextStyle(
fontSize: 12.sp,
color: Colors.red,
return Text.rich(
TextSpan(
children: [
TextSpan(
text: '警告:',
style: TextStyle(
fontSize: 12.sp, // 稍微大一点的字号
color: Colors.red[800], // 更深的红色
//fontStyle: FontStyle.italic, // 斜体
),
),
TextSpan(
text: '您的账号信息和所有文件将被永久删除。',
style: TextStyle(
fontSize: 11.sp,
color: Colors.grey[600],
),
),
],
),
textAlign: TextAlign.center,
);
@ -464,7 +467,7 @@ class ProfileMeetingView extends GetView<ProfileController> {
controller.deleteAccount();
},
child: Text(
'comfirm'.tr,
'confirm'.tr,
style: const TextStyle(
color: Colors.red,
),

23
lib/modules/profile/views/profile_view.dart

@ -7,6 +7,7 @@ import '../../../routes/app_routes.dart';
import 'dart:io';
import 'dart:ui';
import '../../common/webview_page.dart';
import 'unified_avatar.dart';
// ProfileView: 用户设置页面
// class ProfileView extends GetView<ProfileController> {
@ -326,20 +327,14 @@ class EditProfileView extends GetView<ProfileController> {
// --- 右侧内容保持不变 ---
Row(
children: [
Obx(() => CircleAvatar(
radius: 26.r, // 调整头像大小以适应行高
backgroundImage: controller.localAvatar.value.isNotEmpty
? FileImage(File(controller.localAvatar.value))
: (controller.userInfo.value.avatar.isNotEmpty
? NetworkImage(controller.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
backgroundColor: Colors.grey[200],
child: (controller.localAvatar.value.isEmpty &&
controller.userInfo.value.avatar.isEmpty)
? Icon(Icons.person,
size: 20.r, color: Colors.grey) // 默认图标
: null, // 添加背景色防止透明图片问题
)),
// 使用统一头像组件,避免闪烁问题
UnifiedAvatar(
radius: 26.r,
backgroundColor: Colors.grey[200],
iconColor: Colors.grey,
iconSize: 20.r,
isDarkMode: isDarkMode,
),
SizedBox(width: 8.w),
Container(
width: 32.w,

137
lib/modules/profile/views/unified_avatar.dart

@ -0,0 +1,137 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
/// 统一头像显示组件
/// 确保所有页面的头像显示逻辑一致,避免闪烁问题
class UnifiedAvatar extends StatelessWidget {
final double radius;
final Color? backgroundColor;
final Color? iconColor;
final double? iconSize;
final bool isDarkMode;
final VoidCallback? onTap;
const UnifiedAvatar({
super.key,
required this.radius,
this.backgroundColor,
this.iconColor,
this.iconSize,
this.isDarkMode = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final ProfileController profileCtrl = Get.find<ProfileController>();
return GestureDetector(
onTap: onTap,
child: Obx(() {
// 等待头像数据准备完成,避免闪烁
if (!profileCtrl.isAvatarReady.value) {
return _buildLoadingAvatar();
}
return _buildAvatar(profileCtrl);
}),
);
}
/// 构建加载中的头像占位符
Widget _buildLoadingAvatar() {
return CircleAvatar(
radius: radius,
backgroundColor:
backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
child: Icon(
Icons.person,
size: iconSize ?? (radius * 0.6),
color: iconColor ?? (isDarkMode ? Colors.grey[400] : Colors.grey[600]),
),
);
}
/// 构建实际头像
Widget _buildAvatar(ProfileController profileCtrl) {
ImageProvider<Object>? imageProvider;
// 统一头像显示优先级:本地头像 > 网络头像 > 默认图标
if (profileCtrl.localAvatar.value.isNotEmpty) {
imageProvider = FileImage(File(profileCtrl.localAvatar.value));
} else if (profileCtrl.userInfo.value.avatar.isNotEmpty) {
imageProvider = NetworkImage(profileCtrl.userInfo.value.avatar);
}
return CircleAvatar(
radius: radius,
backgroundImage: imageProvider,
backgroundColor:
backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
child: (profileCtrl.localAvatar.value.isEmpty &&
profileCtrl.userInfo.value.avatar.isEmpty)
? Icon(
Icons.person,
size: iconSize ?? (radius * 0.6),
color: iconColor ??
(isDarkMode ? Colors.grey[400] : Colors.grey[600]),
)
: null,
);
}
}
/// 带容器装饰的统一头像组件
class UnifiedAvatarWithContainer extends StatelessWidget {
final double width;
final double height;
final double radius;
final Color? backgroundColor;
final Color? iconColor;
final double? iconSize;
final bool isDarkMode;
final VoidCallback? onTap;
final EdgeInsetsGeometry? margin;
final List<BoxShadow>? boxShadow;
const UnifiedAvatarWithContainer({
super.key,
required this.width,
required this.height,
required this.radius,
this.backgroundColor,
this.iconColor,
this.iconSize,
this.isDarkMode = false,
this.onTap,
this.margin,
this.boxShadow,
});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
margin: margin,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
boxShadow: boxShadow,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: UnifiedAvatar(
radius: radius / 2,
backgroundColor: backgroundColor,
iconColor: iconColor,
iconSize: iconSize,
isDarkMode: isDarkMode,
onTap: onTap,
),
),
);
}
}

61
lib/modules/settings/views/settings_view.dart

@ -3,14 +3,15 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import 'dart:io';
import '../../../core/theme/app_colors.dart';
import '../controllers/settings_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../routes/app_routes.dart';
//import '../../../data/models/user_Info.dart';
import 'dart:io';
import '../../common/webview_page.dart';
import '../../../modules/profile/controllers/profile_controller.dart';
import '../../profile/views/unified_avatar.dart';
class SettingsView extends GetView<SettingsController> {
const SettingsView({Key? key}) : super(key: key);
@ -98,46 +99,24 @@ class SettingsView extends GetView<SettingsController> {
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Row(
children: [
Obx(() => GestureDetector(
//onTap: ()=>controller.updateUserAvatar(),
child: Container(
//用户头像部分
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
// 裁剪圆形头像
borderRadius: BorderRadius.circular(30.r),
child: CircleAvatar(
radius: 30.r,
// 统一头像显示优先级:本地头像 > 网络头像 > 默认图标
backgroundImage: profileCtrl.localAvatar.value.isNotEmpty
? FileImage(File(profileCtrl.localAvatar.value))
: (profileCtrl.userInfo.value.avatar.isNotEmpty
? NetworkImage(profileCtrl.userInfo.value.avatar)
: null) as ImageProvider<Object>?,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200],
child: (profileCtrl.localAvatar.value.isEmpty &&
profileCtrl.userInfo.value.avatar.isEmpty)
? Icon(
Icons.person,
size: 30.r,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
)
: null,
),
),
),
)),
// 使用统一头像组件,避免闪烁问题
UnifiedAvatarWithContainer(
width: 60.w,
height: 60.w,
radius: 30.r,
backgroundColor:
isDarkMode ? Colors.grey[800] : Colors.grey[200],
iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600],
iconSize: 30.r,
isDarkMode: isDarkMode,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
SizedBox(width: 16.w),
Expanded(
// 占据剩余空间

Loading…
Cancel
Save