455 lines
16 KiB
Dart
455 lines
16 KiB
Dart
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<CoreServicesImpl>();
|
||
final BorderRadius? borderRadius;
|
||
final V2TimUserStatus? onlineStatus;
|
||
final int? type; // 1 c2c 2 group
|
||
final bool isShowBigWhenClick;
|
||
final bool isCircle; // 是否显示为圆形头像
|
||
final TUISelfInfoViewModel selfInfoViewModel =
|
||
serviceLocator<TUISelfInfoViewModel>();
|
||
// 新增:群ID,用于组合头像
|
||
final String? groupID;
|
||
// 获取群服务
|
||
final GroupServices _groupServices = serviceLocator<GroupServices>();
|
||
// 组合头像缓存,避免频繁重建导致的闪烁
|
||
static final Map<String, List<String>> _groupCompositeCache = {};
|
||
static final Map<String, Future<List<String>>> _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<List<String>> _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<String> urls = members
|
||
.whereType<V2TimGroupMemberFullInfo>()
|
||
.map((m) => m.faceUrl ?? "")
|
||
.where((u) => u.isNotEmpty)
|
||
.take(maxCount)
|
||
.toList();
|
||
return urls;
|
||
}
|
||
|
||
// 组合头像布局:带背景和圆角(28),内部头像为圆形
|
||
Widget _buildCompositeGrid(List<String> u) {
|
||
final count = u.length;
|
||
const double spacing = 2.0; // 内部头像之间的间距
|
||
const double outerPadding = 6.0; // 内头像距离外层的间距
|
||
|
||
List<Widget> 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<List<String>>(
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|