tencent_cloud_chat_uikit_fl.../lib/ui/widgets/avatar.dart

455 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
),
),
],
);
}
}