import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'dart:math' as math; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart' if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_user_status.dart'; import 'package:tencent_cloud_chat_uikit/business_logic/view_models/tui_self_info_view_model.dart'; import 'package:tencent_cloud_chat_uikit/ui/utils/common_utils.dart'; import 'package:tencent_cloud_chat_uikit/ui/widgets/image_screen.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_statelesswidget.dart'; import 'package:tencent_cloud_chat_uikit/data_services/core/core_services_implements.dart'; import 'package:tencent_cloud_chat_uikit/data_services/services_locatar.dart'; import 'package:tencent_cloud_chat_uikit/base_widgets/tim_ui_kit_base.dart'; import 'package:tencent_cloud_chat_uikit/theme/tui_theme.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart' if (dart.library.html) 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_group_member_full_info.dart'; import 'package:tencent_cloud_chat_uikit/data_services/group/group_services.dart'; class Avatar extends TIMUIKitStatelessWidget { final String faceUrl; final String showName; final bool isFromLocalAsset; final CoreServicesImpl coreService = serviceLocator(); final BorderRadius? borderRadius; final V2TimUserStatus? onlineStatus; final int? type; // 1 c2c 2 group final bool isShowBigWhenClick; final bool isCircle; // 是否显示为圆形头像 final TUISelfInfoViewModel selfInfoViewModel = serviceLocator(); // 新增:群ID,用于组合头像 final String? groupID; // 获取群服务 final GroupServices _groupServices = serviceLocator(); // 组合头像缓存,避免频繁重建导致的闪烁 static final Map> _groupCompositeCache = {}; static final Map>> _groupCompositeFutureCache = {}; // 对外暴露:清理指定群的组合头像缓存 static void clearGroupCompositeCache(String groupID) { _groupCompositeCache.remove(groupID); _groupCompositeFutureCache.remove(groupID); } // 对外暴露:清理所有组合头像缓存 static void clearAllGroupCompositeCache() { _groupCompositeCache.clear(); _groupCompositeFutureCache.clear(); } Avatar( {Key? key, required this.faceUrl, this.onlineStatus, required this.showName, this.isShowBigWhenClick = false, this.isFromLocalAsset = false, this.borderRadius, this.isCircle = true, this.type = 1, this.groupID}) : super(key: key); Future> _fetchGroupAvatarUrls( String groupID, int maxCount) async { final res = await _groupServices.getGroupMemberList( groupID: groupID, filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL, nextSeq: "0", count: maxCount, offset: 0); if (res.code != 0 || res.data == null) { return []; } final members = res.data!.memberInfoList ?? []; final List urls = members .whereType() .map((m) => m.faceUrl ?? "") .where((u) => u.isNotEmpty) .take(maxCount) .toList(); return urls; } // 组合头像布局:带背景和圆角(28),内部头像为圆形 Widget _buildCompositeGrid(List u) { final count = u.length; const double spacing = 2.0; // 内部头像之间的间距 const double outerPadding = 6.0; // 内头像距离外层的间距 List tiles = []; for (var i = 0; i < count; i++) { tiles.add(ClipOval( child: CachedNetworkImage( imageUrl: u[i], fadeInDuration: const Duration(milliseconds: 0), fit: BoxFit.cover, errorWidget: (context, _, __) => Image.asset( TencentUtils.checkString( selfInfoViewModel.globalConfig?.defaultAvatarAssetPath) ?? 'images/default_c2c_head.png', package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null : 'tencent_cloud_chat_uikit', fit: BoxFit.cover, ), ), )); } Widget gridChild; if (count <= 1) { gridChild = tiles.isNotEmpty ? tiles[0] : const SizedBox.shrink(); } else if (count == 2) { gridChild = Row(children: [ Expanded(child: AspectRatio(aspectRatio: 1, child: tiles[0])), const SizedBox(width: spacing), Expanded(child: AspectRatio(aspectRatio: 1, child: tiles[1])), ]); } else if (count == 3) { // 三宫格(上2下1),固定方块尺寸,整体垂直水平居中,并轻微下移修正视觉偏上 gridChild = LayoutBuilder( builder: (context, constraints) { // 以最小边为准,保证在非正方形约束下也能居中 final double side = math.min(constraints.maxWidth, constraints.maxHeight); final double tileSize = (side - spacing) / 2; // 轻微向下偏移,修正视觉上略靠上的问题 // const double vBias = spacing; // + 1.0; return Center( child: SizedBox( width: tileSize * 2 + spacing, height: tileSize * 2 + spacing, child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: tileSize, child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: tileSize, height: tileSize, child: tiles[0]), const SizedBox(width: spacing), SizedBox( width: tileSize, height: tileSize, child: tiles[1]), ], ), ), const SizedBox(height: spacing), SizedBox( height: tileSize, child: Center( child: SizedBox( width: tileSize, height: tileSize, child: tiles[2], ), ), ), ], ), ), ); }, ); } else { // 4个或以上按2x2 gridChild = Column(children: [ Expanded( child: Row(children: [ Expanded(child: AspectRatio(aspectRatio: 1, child: tiles[0])), const SizedBox(width: spacing), Expanded(child: AspectRatio(aspectRatio: 1, child: tiles[1])), ])), const SizedBox(height: spacing), Expanded( child: Row(children: [ Expanded(child: AspectRatio(aspectRatio: 1, child: tiles[2])), const SizedBox(width: spacing), Expanded(child: AspectRatio(aspectRatio: 1, child: tiles[3])), ])), ]); } return Container( decoration: BoxDecoration( color: const Color(0xFFEFEFEF), borderRadius: BorderRadius.circular(14), ), child: Padding( padding: const EdgeInsets.all(outerPadding), child: gridChild, ), ); } // 组合头像占位:减少切换的闪动 Widget _compositePlaceholder() { return Container( decoration: BoxDecoration( color: const Color(0xFFEFEFEF), borderRadius: BorderRadius.circular(28), ), ); } // 组合头像子项 Widget _compositeAvatar(BuildContext context, TUITheme theme) { final config = selfInfoViewModel.globalConfig; final enableComposite = config?.enableGroupCompositeAvatar ?? true; final maxCount = config?.maxCompositeAvatarsCount ?? 4; // 若非群聊或未开启组合头像,则回退到单一头像 if ((type ?? 1) != 2 || !enableComposite || groupID == null || groupID!.isEmpty) { return _singleAvatarWidget(context, theme); } // 命中缓存,直接同步渲染,避免闪动 final cached = _groupCompositeCache[groupID!]; if (cached != null && cached.isNotEmpty) { return _buildCompositeGrid(cached); } final future = _groupCompositeFutureCache[groupID!] ??= _fetchGroupAvatarUrls(groupID!, maxCount); return FutureBuilder>( future: future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { // 等待时使用占位背景,避免先显示单头像再切换 return _compositePlaceholder(); } if (snapshot.hasError || !snapshot.hasData) { return _singleAvatarWidget(context, theme); } final urls = snapshot.data ?? []; if (urls.isEmpty) { return _singleAvatarWidget(context, theme); } // 写入缓存以便后续快速渲染 _groupCompositeCache[groupID!] = urls; return _buildCompositeGrid(urls); }, ); } // 提取原有单头像逻辑,避免在组合头像回退时发生递归 Widget _singleAvatarWidget(BuildContext context, TUITheme theme) { Widget defaultAvatar() { if (type == 1) { return Image.asset( TencentUtils.checkString( selfInfoViewModel.globalConfig?.defaultAvatarAssetPath) ?? 'images/default_c2c_head.png', fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null : 'tencent_cloud_chat_uikit'); } else { return Image.asset( TencentUtils.checkString( selfInfoViewModel.globalConfig?.defaultAvatarAssetPath) ?? 'images/default_group_head.png', fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null : 'tencent_cloud_chat_uikit'); } } if (faceUrl != "" && faceUrl.isNotEmpty) { if (isFromLocalAsset) { return Image.asset( faceUrl, fit: BoxFit.cover, ); } return CachedNetworkImage( imageUrl: faceUrl, fadeInDuration: const Duration(milliseconds: 0), fit: BoxFit.cover, errorWidget: (BuildContext context, String c, dynamic s) { return CachedNetworkImage( imageUrl: 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/avatar.png', fadeInDuration: const Duration(milliseconds: 0), fit: BoxFit.cover, errorWidget: (BuildContext context, String c, dynamic s) { return defaultAvatar(); }, ); }, ); } else { // return defaultAvatar(); return CachedNetworkImage( imageUrl: 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/avatar.png', fadeInDuration: const Duration(milliseconds: 0), fit: BoxFit.cover, errorWidget: (BuildContext context, String c, dynamic s) { return defaultAvatar(); }, ); } } Widget getImageWidget(BuildContext context, TUITheme theme) { // 若满足组合头像条件则优先组合 final config = selfInfoViewModel.globalConfig; final enableComposite = config?.enableGroupCompositeAvatar ?? true; if ((type ?? 1) == 2 && enableComposite && groupID != null && groupID!.isNotEmpty) { return _compositeAvatar(context, theme); } // 原有单头像逻辑 return _singleAvatarWidget(context, theme); } ImageProvider getImageProvider() { ImageProvider defaultAvatar() { if (type == 1) { return Image.asset( TencentUtils.checkString(selfInfoViewModel .globalConfig?.defaultAvatarAssetPath) ?? 'images/default_c2c_head.png', fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null : 'tencent_cloud_chat_uikit') .image; } else { return Image.asset( TencentUtils.checkString(selfInfoViewModel .globalConfig?.defaultAvatarAssetPath) ?? 'images/default_group_head.png', fit: BoxFit.cover, package: selfInfoViewModel.globalConfig?.defaultAvatarAssetPath != null ? null : 'tencent_cloud_chat_uikit') .image; } } if (faceUrl != "" && faceUrl.isNotEmpty) { if (isFromLocalAsset) { return Image.asset(faceUrl).image; } return CachedNetworkImageProvider( faceUrl, ); } else { return CachedNetworkImageProvider( 'https://quanxue-oa.oss-cn-chengdu.aliyuncs.com/avatar.png', ); } } @override Widget tuiBuild(BuildContext context, TUIKitBuildValue value) { final TUITheme theme = value.theme; // 根据isCircle参数决定borderRadius;群组合头像强制4圆角 BorderRadius getAvatarBorderRadius() { final config = selfInfoViewModel.globalConfig; // 默认启用组合头像(当 globalConfig 为空时也生效) final enableComposite = config?.enableGroupCompositeAvatar ?? true; final bool isCompositeCase = ((type ?? 1) == 2) && enableComposite && groupID != null && groupID!.isNotEmpty; if (isCompositeCase) { return BorderRadius.circular(14); } if (isCircle) { return BorderRadius.circular(200); // 使用大圆角值来实现圆形效果 } return borderRadius ?? selfInfoViewModel.globalConfig?.defaultAvatarBorderRadius ?? BorderRadius.circular(4.8); } return Stack( fit: StackFit.expand, clipBehavior: Clip.none, children: [ if (isShowBigWhenClick) GestureDetector( onTap: () { Navigator.of(context).push( PageRouteBuilder( opaque: false, // set to false pageBuilder: (_, __, ___) => ImageScreen( imageProvider: getImageProvider(), heroTag: faceUrl), ), ); }, child: Hero( tag: faceUrl, child: ClipRRect( borderRadius: getAvatarBorderRadius(), child: getImageWidget(context, theme), ), ), ), if (!isShowBigWhenClick) ClipRRect( borderRadius: getAvatarBorderRadius(), child: getImageWidget(context, theme), ), if (onlineStatus?.statusType != null && onlineStatus?.statusType != 0) Positioned( bottom: -4, right: -4, child: Container( width: 12, height: 12, alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.white, width: 2.0, ), color: onlineStatus?.statusType == 1 ? theme.conversationItemOnlineStatusBgColor : theme.conversationItemOfflineStatusBgColor, ), child: null, ), ), ], ); } }