fix: wrap long badge displays

This commit is contained in:
zhx 2026-05-18 12:01:26 +08:00
parent d5653afa4f
commit c2b5ef9e16
3 changed files with 228 additions and 11 deletions

View File

@ -436,6 +436,29 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
);
}
Widget _buildHeaderLongBadgeStrip(SocialChatUserProfileManager ref) {
return SizedBox(
width: ScreenUtil().screenWidth - 40.w,
child: SCUserBadgeStrip(
userId: ref.userProfile?.id ?? widget.tageId,
fallbackBadges:
ref.userProfile?.wearBadge
?.where((item) => item.use ?? false)
.toList() ??
const <WearBadge>[],
displayScope: SCUserBadgeDisplayScope.long,
height: 45.w,
badgeHeight: 20.w,
longBadgeWidth: 62.w,
spacing: 4.w,
wrap: true,
maxWrapRows: 2,
scrollLastWrapRow: true,
reserveSpace: false,
),
);
}
Widget _buildHeaderIntroduction(SocialChatUserProfileManager ref) {
final intro = (ref.userProfile?.autograph ?? "").trim();
return Container(
@ -674,6 +697,8 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
SizedBox(height: 5.w),
_buildHeaderMeta(ref),
SizedBox(height: 4.w),
_buildHeaderLongBadgeStrip(ref),
SizedBox(height: 4.w),
_buildHeaderBadgeStrip(ref),
SizedBox(height: 6.w),
_buildHeaderIntroduction(ref),

View File

@ -20,6 +20,8 @@ class SCUserBadgeStrip extends StatefulWidget {
this.spacing,
this.maxBadges,
this.wrap = false,
this.maxWrapRows,
this.scrollLastWrapRow = false,
this.reserveSpace = false,
this.alignment = WrapAlignment.center,
});
@ -33,6 +35,8 @@ class SCUserBadgeStrip extends StatefulWidget {
final double? spacing;
final int? maxBadges;
final bool wrap;
final int? maxWrapRows;
final bool scrollLastWrapRow;
final bool reserveSpace;
final WrapAlignment alignment;
@ -164,6 +168,14 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
.toList();
if (widget.wrap) {
if (widget.maxWrapRows != null) {
return _buildLimitedWrapBadges(
badges,
stripHeight: stripHeight,
badgeHeight: badgeHeight,
spacing: spacing,
);
}
return SizedBox(
height: stripHeight,
child: Center(
@ -187,14 +199,137 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
);
}
Widget _buildLimitedWrapBadges(
List<WearBadge> badges, {
required double stripHeight,
required double badgeHeight,
required double spacing,
}) {
final maxRows = widget.maxWrapRows ?? 1;
if (maxRows <= 1) {
return _buildScrollableBadgeRow(
badges,
stripHeight: stripHeight,
badgeHeight: badgeHeight,
spacing: spacing,
);
}
return SizedBox(
height: stripHeight,
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth =
constraints.maxWidth.isFinite
? constraints.maxWidth
: _badgesTotalWidth(badges, badgeHeight, spacing);
final widestBadge = badges
.map((badge) => _badgeWidth(badge, badgeHeight))
.fold<double>(
0,
(current, width) => width > current ? width : current,
);
final perRow =
widestBadge <= 0
? badges.length
: ((maxWidth + spacing) / (widestBadge + spacing))
.floor()
.clamp(1, badges.length);
final firstRows = maxRows - 1;
final fixedCount = (perRow * firstRows).clamp(0, badges.length);
final fixedBadges = badges.take(fixedCount).toList();
final lastRowBadges = badges.skip(fixedCount).toList();
final rows = <Widget>[];
for (var start = 0; start < fixedBadges.length; start += perRow) {
final end =
start + perRow > fixedBadges.length
? fixedBadges.length
: start + perRow;
rows.add(
_buildBadgeRow(
fixedBadges.sublist(start, end),
badgeHeight: badgeHeight,
spacing: spacing,
),
);
}
if (lastRowBadges.isNotEmpty) {
rows.add(
widget.scrollLastWrapRow
? _buildScrollableBadgeRow(
lastRowBadges,
stripHeight: badgeHeight,
badgeHeight: badgeHeight,
spacing: spacing,
)
: _buildBadgeRow(
lastRowBadges.take(perRow).toList(),
badgeHeight: badgeHeight,
spacing: spacing,
),
);
}
if (rows.isEmpty) {
return const SizedBox.shrink();
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _withVerticalSpacing(rows, 5.w),
);
},
),
);
}
Widget _buildBadgeRow(
List<WearBadge> badges, {
required double badgeHeight,
required double spacing,
}) {
return SizedBox(
height: badgeHeight,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: _rowAlignment,
children: _withSpacing(
badges
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
.toList(),
spacing,
),
),
);
}
Widget _buildScrollableBadgeRow(
List<WearBadge> badges, {
required double stripHeight,
required double badgeHeight,
required double spacing,
}) {
return SizedBox(
height: stripHeight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: _withSpacing(
badges
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
.toList(),
spacing,
),
),
),
);
}
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
final type = badge.type?.trim().toUpperCase() ?? "";
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP";
final width =
isLongBadge
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
: badgeHeight;
final width = _badgeWidth(badge, badgeHeight);
return netImage(
url: _badgeUrl(badge),
width: width,
@ -206,6 +341,47 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
);
}
double _badgeWidth(WearBadge badge, double badgeHeight) {
final type = badge.type?.trim().toUpperCase() ?? "";
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP";
return isLongBadge
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
: badgeHeight;
}
double _badgesTotalWidth(
List<WearBadge> badges,
double badgeHeight,
double spacing,
) {
if (badges.isEmpty) {
return 0;
}
final badgeWidth = badges.fold<double>(
0,
(sum, badge) => sum + _badgeWidth(badge, badgeHeight),
);
return badgeWidth + spacing * (badges.length - 1);
}
MainAxisAlignment get _rowAlignment {
switch (widget.alignment) {
case WrapAlignment.start:
return MainAxisAlignment.start;
case WrapAlignment.end:
return MainAxisAlignment.end;
case WrapAlignment.spaceBetween:
return MainAxisAlignment.spaceBetween;
case WrapAlignment.spaceAround:
return MainAxisAlignment.spaceAround;
case WrapAlignment.spaceEvenly:
return MainAxisAlignment.spaceEvenly;
case WrapAlignment.center:
return MainAxisAlignment.center;
}
}
String _badgeUrl(WearBadge badge) {
return (badge.selectUrl ?? "").trim();
}
@ -223,6 +399,20 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
}
return spaced;
}
List<Widget> _withVerticalSpacing(List<Widget> children, double spacing) {
if (children.length < 2) {
return children;
}
final spaced = <Widget>[];
for (var i = 0; i < children.length; i++) {
if (i > 0) {
spaced.add(SizedBox(height: spacing));
}
spaced.add(children[i]);
}
return spaced;
}
}
class _BadgeFutureCacheEntry {

View File

@ -562,16 +562,18 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
SocialChatUserProfile? profile,
) {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 64.w),
constraints: BoxConstraints(maxWidth: 140.w),
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: _roomCardFallbackBadges(ref, profile),
displayScope: SCUserBadgeDisplayScope.long,
height: 22.w,
height: 45.w,
badgeHeight: 20.w,
longBadgeWidth: 62.w,
spacing: 0,
maxBadges: 1,
spacing: 4.w,
wrap: true,
maxWrapRows: 2,
scrollLastWrapRow: true,
reserveSpace: false,
),
);