Browse Source

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

- 创建 UnifiedAvatar 组件统一处理所有头像显示逻辑
- 在 ProfileController 中添加 isAvatarReady 状态防止闪烁
- 修改多个视图使用新的统一头像组件
- 优化头像加载流程,确保本地和远程头像一致性
newdev_shunjiawei
lxm 1 year ago
parent
commit
0941e53f84
  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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:io';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../profile/controllers/profile_controller.dart'; import '../../profile/controllers/profile_controller.dart';
import '../../profile/views/unified_avatar.dart';
import '../controllers/meeting_mine_controller.dart'; import '../controllers/meeting_mine_controller.dart';
import 'bottomSheet/statistics_bottom_sheet.dart'; import 'bottomSheet/statistics_bottom_sheet.dart';
@ -104,32 +104,17 @@ class MeetingMineView extends GetView<MeetingMineController> {
onTap: () => Get.toNamed(Routes.profileMeeting), // 跳转到个人信息页面 onTap: () => Get.toNamed(Routes.profileMeeting), // 跳转到个人信息页面
child: Row( child: Row(
children: [ children: [
// 统一头像显示逻辑:本地头像 > 网络头像 > 默认图标 // 使用统一头像组件,避免闪烁问题
Obx(() => Container( Container(
width: 50.w, margin: EdgeInsets.only(right: 10.w),
height: 50.w, child: UnifiedAvatar(
margin: EdgeInsets.only(right: 10.w), radius: 25.w,
decoration: BoxDecoration( backgroundColor: Colors.grey[200],
borderRadius: BorderRadius.circular(50.w), iconColor: Colors.grey[600],
), iconSize: 25.r,
child: CircleAvatar( isDarkMode: false,
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,
),
)),
Expanded( Expanded(
child: Obx( child: Obx(
() => Text( () => Text(

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

@ -31,6 +31,7 @@ class ProfileController extends GetxController {
final RxBool hasEdits = false.obs; // 是否有编辑 final RxBool hasEdits = false.obs; // 是否有编辑
final RxBool isUpdating = false.obs; // 是否正在更新用户信息 final RxBool isUpdating = false.obs; // 是否正在更新用户信息
final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态)
final RxBool isAvatarReady = false.obs; // 头像数据是否已准备好,用于防止闪烁
//音乐播放管理 //音乐播放管理
final _userportraitManager = Get.find<UserportraitManager>(); final _userportraitManager = Get.find<UserportraitManager>();
// 用于乐观更新的原始状态备份 // 用于乐观更新的原始状态备份
@ -289,19 +290,23 @@ class ProfileController extends GetxController {
} }
} }
void loadUserInfo() { void loadUserInfo() async {
final userJson = _storage.read(userKey); final userJson = _storage.read(userKey);
if (userJson != null) { if (userJson != null) {
userInfo.value = User.instance; userInfo.value = User.instance;
Logger().d('从本地加载用户信息: $userJson'); Logger().d('从本地加载用户信息: $userJson');
// 加载本地头像路径并验证文件有效性 // 重置头像准备状态
isAvatarReady.value = false;
// 同步加载本地头像路径并验证文件有效性 - 修复闪烁问题
final savedLocalAvatar = _storage.read('local_avatar'); final savedLocalAvatar = _storage.read('local_avatar');
if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) { if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) {
final avatarPath = savedLocalAvatar.toString(); final avatarPath = savedLocalAvatar.toString();
// 验证文件是否存在 // 同步验证文件是否存在,避免异步导致的闪烁
final file = File(avatarPath); final file = File(avatarPath);
file.exists().then((exists) { try {
final exists = await file.exists();
if (exists) { if (exists) {
localAvatar.value = avatarPath; localAvatar.value = avatarPath;
Logger().d('加载本地头像路径: ${localAvatar.value}'); Logger().d('加载本地头像路径: ${localAvatar.value}');
@ -311,18 +316,26 @@ class ProfileController extends GetxController {
_storage.remove('local_avatar'); _storage.remove('local_avatar');
Logger().w('本地头像文件不存在,已清除路径: $avatarPath'); Logger().w('本地头像文件不存在,已清除路径: $avatarPath');
} }
}); } catch (e) {
// 文件访问异常,清除无效路径
localAvatar.value = '';
_storage.remove('local_avatar');
Logger().e('验证本地头像文件时发生异常: $e');
}
} else { } else {
// 确保本地头像路径为空 // 确保本地头像路径为空
localAvatar.value = ''; localAvatar.value = '';
Logger().d('没有本地头像路径'); Logger().d('没有本地头像路径');
} }
// 头像数据准备完成
isAvatarReady.value = true;
// 保存原始状态用于回滚 // 保存原始状态用于回滚
_saveOriginalState(); _saveOriginalState();
// 如果有远程头像地址,下载并保存到本地 // 如果有远程头像地址且本地头像不存在,才下载并保存到本地
if (userInfo.value.avatar.isNotEmpty) { if (userInfo.value.avatar.isNotEmpty && localAvatar.value.isEmpty) {
Logger().d('远程头像地址: ${userInfo.value.avatar}'); Logger().d('远程头像地址: ${userInfo.value.avatar}');
_downloadAndSaveAvatar(userInfo.value.avatar); _downloadAndSaveAvatar(userInfo.value.avatar);
} }
@ -333,11 +346,12 @@ class ProfileController extends GetxController {
} }
// 确保本地状态与服务器状态的一致性 // 确保本地状态与服务器状态的一致性
void _ensureStateConsistency() { void _ensureStateConsistency() async {
// 检查本地头像文件是否存在 // 同步检查本地头像文件是否存在 - 修复闪烁问题
if (localAvatar.value.isNotEmpty) { if (localAvatar.value.isNotEmpty) {
final file = File(localAvatar.value); final file = File(localAvatar.value);
file.exists().then((exists) { try {
final exists = await file.exists();
if (!exists) { if (!exists) {
Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}'); Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}');
localAvatar.value = ''; localAvatar.value = '';
@ -348,7 +362,11 @@ class ProfileController extends GetxController {
_downloadAndSaveAvatar(userInfo.value.avatar); _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:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/profile_controller.dart'; import '../controllers/profile_controller.dart';
import 'unified_avatar.dart';
class ProfileMeetingView extends GetView<ProfileController> { class ProfileMeetingView extends GetView<ProfileController> {
ProfileMeetingView({super.key}); ProfileMeetingView({super.key});
@ -100,15 +101,15 @@ class ProfileMeetingView extends GetView<ProfileController> {
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildNicknameItem(isDarkMode), _buildNicknameItem(isDarkMode),
Divider( Divider(
height: 1.h, height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildLoginMethodItem(isDarkMode), _buildLoginMethodItem(isDarkMode),
Divider( Divider(
height: 1.h, height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
_buildManageLoginItem(isDarkMode), _buildManageLoginItem(isDarkMode),
Divider( Divider(
height: 1.h, height: 0.5.h,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
SizedBox(height: 32.h), SizedBox(height: 32.h),
_buildLogoutButton(isDarkMode), _buildLogoutButton(isDarkMode),
@ -126,55 +127,43 @@ class ProfileMeetingView extends GetView<ProfileController> {
return Center( return Center(
child: Column( child: Column(
children: [ children: [
Obx(() => GestureDetector( // 使用统一头像组件,避免闪烁问题
onTap: () => controller.updateUserAvatar(), GestureDetector(
child: Stack( onTap: () => controller.updateUserAvatar(),
children: [ child: Stack(
CircleAvatar( children: [
radius: 40.r, UnifiedAvatar(
backgroundImage: controller.localAvatar.value.isNotEmpty radius: 40.r,
? FileImage(File(controller.localAvatar.value)) backgroundColor:
: (controller.userInfo.value.avatar.isNotEmpty isDarkMode ? Colors.grey[800] : Colors.grey[200],
? NetworkImage(controller.userInfo.value.avatar) iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600],
: null) as ImageProvider<Object>?, iconSize: 30.r,
backgroundColor: isDarkMode: isDarkMode,
isDarkMode ? Colors.grey[800] : Colors.grey[200], ),
child: (controller.localAvatar.value.isEmpty && Positioned(
controller.userInfo.value.avatar.isEmpty) bottom: 0,
? Icon( right: 0,
Icons.person, child: Container(
size: 30.r, width: 22.w,
color: isDarkMode height: 22.w,
? Colors.grey[400] decoration: BoxDecoration(
: Colors.grey[600], color: isDarkMode ? Colors.grey[600] : Colors.grey[350],
) shape: BoxShape.circle,
: null, border: Border.all(
), color: isDarkMode ? Colors.black : Colors.white,
Positioned( width: 0.w,
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,
),
), ),
), ),
], child: Icon(
Icons.camera_alt,
size: 14.w,
color: Colors.black,
),
),
), ),
)), ],
),
),
], ],
), ),
); );
@ -336,9 +325,8 @@ class ProfileMeetingView extends GetView<ProfileController> {
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _showLogoutDialog(isDarkMode), onPressed: () => _showLogoutDialog(isDarkMode),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50], backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200],
side: BorderSide( side: BorderSide.none,
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
), ),
@ -358,10 +346,11 @@ class ProfileMeetingView extends GetView<ProfileController> {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 48.h, height: 48.h,
child: ElevatedButton( child: OutlinedButton(
onPressed: () => _showDeleteAccountDialog(isDarkMode), onPressed: () => _showDeleteAccountDialog(isDarkMode),
style: ElevatedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: Colors.red, side: BorderSide.none,
backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200],
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
), ),
@ -370,7 +359,7 @@ class ProfileMeetingView extends GetView<ProfileController> {
'删除账号', '删除账号',
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Colors.white, color: Colors.red[600],
), ),
), ),
), ),
@ -378,11 +367,25 @@ class ProfileMeetingView extends GetView<ProfileController> {
} }
Widget _buildWarningText() { Widget _buildWarningText() {
return Text( return Text.rich(
'警告:您的账号信息和所有文件将被永久删除。', TextSpan(
style: TextStyle( children: [
fontSize: 12.sp, TextSpan(
color: Colors.red, 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, textAlign: TextAlign.center,
); );
@ -464,7 +467,7 @@ class ProfileMeetingView extends GetView<ProfileController> {
controller.deleteAccount(); controller.deleteAccount();
}, },
child: Text( child: Text(
'comfirm'.tr, 'confirm'.tr,
style: const TextStyle( style: const TextStyle(
color: Colors.red, 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:io';
import 'dart:ui'; import 'dart:ui';
import '../../common/webview_page.dart'; import '../../common/webview_page.dart';
import 'unified_avatar.dart';
// ProfileView: 用户设置页面 // ProfileView: 用户设置页面
// class ProfileView extends GetView<ProfileController> { // class ProfileView extends GetView<ProfileController> {
@ -326,20 +327,14 @@ class EditProfileView extends GetView<ProfileController> {
// --- 右侧内容保持不变 --- // --- 右侧内容保持不变 ---
Row( Row(
children: [ children: [
Obx(() => CircleAvatar( // 使用统一头像组件,避免闪烁问题
radius: 26.r, // 调整头像大小以适应行高 UnifiedAvatar(
backgroundImage: controller.localAvatar.value.isNotEmpty radius: 26.r,
? FileImage(File(controller.localAvatar.value)) backgroundColor: Colors.grey[200],
: (controller.userInfo.value.avatar.isNotEmpty iconColor: Colors.grey,
? NetworkImage(controller.userInfo.value.avatar) iconSize: 20.r,
: null) as ImageProvider<Object>?, isDarkMode: isDarkMode,
backgroundColor: Colors.grey[200], ),
child: (controller.localAvatar.value.isEmpty &&
controller.userInfo.value.avatar.isEmpty)
? Icon(Icons.person,
size: 20.r, color: Colors.grey) // 默认图标
: null, // 添加背景色防止透明图片问题
)),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Container( Container(
width: 32.w, 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:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui'; import 'dart:ui';
import 'dart:io';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../controllers/settings_controller.dart'; import '../controllers/settings_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart'; import '../../../core/widgets/common_bottom_nav.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
//import '../../../data/models/user_Info.dart'; //import '../../../data/models/user_Info.dart';
import 'dart:io';
import '../../common/webview_page.dart'; import '../../common/webview_page.dart';
import '../../../modules/profile/controllers/profile_controller.dart'; import '../../../modules/profile/controllers/profile_controller.dart';
import '../../profile/views/unified_avatar.dart';
class SettingsView extends GetView<SettingsController> { class SettingsView extends GetView<SettingsController> {
const SettingsView({Key? key}) : super(key: key); const SettingsView({Key? key}) : super(key: key);
@ -98,46 +99,24 @@ class SettingsView extends GetView<SettingsController> {
: ImageFilter.blur(sigmaX: 0, sigmaY: 0), : ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Row( child: Row(
children: [ children: [
Obx(() => GestureDetector( // 使用统一头像组件,避免闪烁问题
//onTap: ()=>controller.updateUserAvatar(), UnifiedAvatarWithContainer(
child: Container( width: 60.w,
//用户头像部分 height: 60.w,
width: 60.w, radius: 30.r,
height: 60.w, backgroundColor:
decoration: BoxDecoration( isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(30.r), iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600],
boxShadow: [ iconSize: 30.r,
BoxShadow( isDarkMode: isDarkMode,
color: Colors.black.withOpacity(0.1), boxShadow: [
blurRadius: 8, BoxShadow(
offset: const Offset(0, 2), 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,
),
),
),
)),
SizedBox(width: 16.w), SizedBox(width: 16.w),
Expanded( Expanded(
// 占据剩余空间 // 占据剩余空间

Loading…
Cancel
Save