Files
boss/android/app/src/main/java/com/hyzq/boss/BossUi.java

1811 lines
73 KiB
Java
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.

package com.hyzq.boss;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.widget.ImageButton;
import android.text.Layout;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.widget.ImageViewCompat;
import android.content.res.ColorStateList;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public final class BossUi {
private static final int[] AVATAR_BG_COLORS = {
Color.parseColor("#1EC76F"),
Color.parseColor("#DFF5EC"),
Color.parseColor("#DCEEF6"),
Color.parseColor("#FCE5BF")
};
private static final int[] ACTIVITY_ICON_COLORS = {
Color.parseColor("#C4CCC8"),
Color.parseColor("#B5C0BA"),
Color.parseColor("#A7B3AD"),
Color.parseColor("#98A6A0")
};
private static final int DEVICE_STATUS_ONLINE = Color.parseColor("#18B85A");
private static final int DEVICE_STATUS_ABNORMAL = Color.parseColor("#FF5A5A");
private static final int DEVICE_STATUS_OFFLINE = Color.parseColor("#A7AFB7");
private static final int PINNED_ROW_BG = Color.parseColor("#FFF7F7F7");
private static final int CONTEXT_RING_TRACK = Color.parseColor("#FFD0D0D0");
private static final int CONTEXT_RING_BG = Color.parseColor("#FFFFFFFF");
private BossUi() {}
public enum TopActionButtonStyle {
PRIMARY_TEXT,
SECONDARY_TEXT,
COMPACT_ICON
}
public enum TopActionIcon {
BACK,
CLOSE,
MORE,
ADD,
SEARCH,
REFRESH,
EDIT,
SAVE,
INFO
}
public static String formatRoleLabel(@Nullable String rawRole) {
if (TextUtils.isEmpty(rawRole)) {
return "";
}
switch (rawRole) {
case "highest_admin":
return "最高管理员";
case "admin":
return "管理员";
case "member":
return "成员";
case "primary":
return "主 GPT";
case "backup":
return "备用 GPT";
case "api_fallback":
return "API 容灾";
default:
return rawRole;
}
}
public static void applyTopActionButtonStyle(Context context, Button button, TopActionButtonStyle style) {
if (style == TopActionButtonStyle.COMPACT_ICON) {
button.setBackgroundResource(R.drawable.bg_secondary_button);
button.setTextColor(context.getColor(R.color.boss_text_primary));
button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
button.setMinWidth(dp(context, 38));
button.setMinimumWidth(dp(context, 38));
button.setMinHeight(dp(context, 36));
button.setMinimumHeight(dp(context, 36));
button.setPadding(dp(context, 12), dp(context, 6), dp(context, 12), dp(context, 6));
return;
}
if (style == TopActionButtonStyle.PRIMARY_TEXT) {
button.setBackgroundResource(R.drawable.bg_primary_button);
button.setTextColor(context.getColor(R.color.boss_surface));
} else {
button.setBackgroundResource(R.drawable.bg_secondary_button);
button.setTextColor(context.getColor(R.color.boss_text_primary));
}
button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinHeight(0);
button.setMinimumHeight(0);
button.setPadding(dp(context, 12), dp(context, 8), dp(context, 12), dp(context, 8));
}
public static void applyTopIconButtonStyle(Context context, ImageButton button) {
button.setBackgroundResource(R.drawable.bg_top_icon_button);
button.setMinimumWidth(dp(context, 40));
button.setMinimumHeight(dp(context, 40));
button.setPadding(dp(context, 8), dp(context, 8), dp(context, 8), dp(context, 8));
ImageViewCompat.setImageTintList(button, ColorStateList.valueOf(context.getColor(R.color.boss_text_primary)));
}
public static void bindTopIconButton(Context context, ImageButton button, TopActionIcon icon, String contentDescription) {
applyTopIconButtonStyle(context, button);
button.setImageResource(resolveTopActionIconRes(icon));
button.setContentDescription(normalizeTopActionContentDescription(contentDescription, icon));
}
public static String normalizeTopActionContentDescription(@Nullable String raw, TopActionIcon icon) {
if (!TextUtils.isEmpty(raw)) {
if ("...".contentEquals(raw)) {
return "更多";
}
return raw;
}
switch (icon) {
case BACK:
return "返回";
case CLOSE:
return "关闭";
case ADD:
return "新增";
case SEARCH:
return "搜索";
case REFRESH:
return "刷新";
case EDIT:
return "编辑";
case SAVE:
return "保存";
case INFO:
return "信息";
case MORE:
default:
return "更多";
}
}
public static TopActionIcon topActionIconForLabel(@Nullable String label) {
if (TextUtils.isEmpty(label)) {
return TopActionIcon.MORE;
}
if ("...".contentEquals(label)) {
return TopActionIcon.MORE;
}
if ("保存".contentEquals(label)) {
return TopActionIcon.SAVE;
}
if ("刷新".contentEquals(label)) {
return TopActionIcon.REFRESH;
}
if ("新增".contentEquals(label)) {
return TopActionIcon.ADD;
}
if ("编辑".contentEquals(label) || "编辑目标".contentEquals(label)) {
return TopActionIcon.EDIT;
}
if ("只读".contentEquals(label)) {
return TopActionIcon.INFO;
}
return TopActionIcon.MORE;
}
private static int resolveTopActionIconRes(TopActionIcon icon) {
switch (icon) {
case BACK:
return R.drawable.ic_boss_back;
case CLOSE:
return R.drawable.ic_boss_close;
case ADD:
return R.drawable.ic_boss_add;
case SEARCH:
return R.drawable.ic_boss_search;
case REFRESH:
return R.drawable.ic_boss_refresh;
case EDIT:
return R.drawable.ic_boss_edit;
case SAVE:
return R.drawable.ic_boss_check;
case INFO:
return R.drawable.ic_boss_info;
case MORE:
default:
return R.drawable.ic_boss_more;
}
}
public static LinearLayout buildListRow(
Context context,
String title,
@Nullable String subtitle,
@Nullable String meta,
@Nullable String badge,
@Nullable View.OnClickListener listener
) {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = dp(context, 1);
row.setLayoutParams(params);
row.setPadding(dp(context, 16), dp(context, 14), dp(context, 16), dp(context, 14));
row.setBackgroundResource(R.drawable.bg_list_row);
if (listener != null) {
row.setClickable(true);
row.setFocusable(true);
row.setOnClickListener(listener);
}
LinearLayout textWrap = new LinearLayout(context);
textWrap.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textWrap.setLayoutParams(textParams);
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
textWrap.addView(titleView);
if (!TextUtils.isEmpty(subtitle)) {
TextView subtitleView = new TextView(context);
subtitleView.setText(subtitle);
subtitleView.setTextSize(14);
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
subtitleView.setPadding(0, dp(context, 4), 0, 0);
textWrap.addView(subtitleView);
}
if (!TextUtils.isEmpty(meta)) {
TextView metaView = new TextView(context);
metaView.setText(meta);
metaView.setTextSize(12);
metaView.setTextColor(context.getColor(R.color.boss_text_soft));
metaView.setPadding(0, dp(context, 6), 0, 0);
textWrap.addView(metaView);
}
row.addView(textWrap);
LinearLayout accessoryWrap = new LinearLayout(context);
accessoryWrap.setOrientation(LinearLayout.VERTICAL);
accessoryWrap.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
if (!TextUtils.isEmpty(badge)) {
TextView badgeView = new TextView(context);
badgeView.setText(badge);
badgeView.setTextSize(12);
badgeView.setTextColor(context.getColor(R.color.boss_green));
badgeView.setBackgroundResource(R.drawable.bg_tab_active);
badgeView.setPadding(dp(context, 8), dp(context, 2), dp(context, 8), dp(context, 2));
accessoryWrap.addView(badgeView);
}
if (listener != null) {
TextView arrowView = new TextView(context);
arrowView.setText("");
arrowView.setTextSize(18);
arrowView.setTextColor(context.getColor(R.color.boss_text_soft));
arrowView.setPadding(dp(context, 10), dp(context, 4), 0, 0);
accessoryWrap.addView(arrowView);
}
row.addView(accessoryWrap);
return row;
}
public static LinearLayout buildCard(Context context, String title, String body, String meta) {
return buildCard(context, title, body, meta, null);
}
public static LinearLayout buildCard(
Context context,
String title,
String body,
String meta,
@Nullable View.OnClickListener listener
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = dp(context, 12);
card.setLayoutParams(params);
card.setPadding(dp(context, 18), dp(context, 18), dp(context, 18), dp(context, 18));
card.setBackgroundResource(R.drawable.bg_card);
if (listener != null) {
card.setClickable(true);
card.setFocusable(true);
card.setOnClickListener(listener);
}
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(18);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
TextView bodyView = new TextView(context);
bodyView.setText(body);
bodyView.setTextSize(14);
bodyView.setTextColor(context.getColor(R.color.boss_text_primary));
bodyView.setPadding(0, dp(context, 8), 0, 0);
TextView metaView = new TextView(context);
metaView.setText(meta);
metaView.setTextSize(12);
metaView.setTextColor(context.getColor(R.color.boss_text_muted));
metaView.setPadding(0, dp(context, 10), 0, 0);
card.addView(titleView);
card.addView(bodyView);
card.addView(metaView);
return card;
}
public static LinearLayout buildMenuRow(
Context context,
String title,
String description,
@Nullable String badge,
View.OnClickListener listener
) {
return buildListRow(context, title, description, null, badge, listener);
}
public static LinearLayout buildWechatMenuRow(
Context context,
String title,
@Nullable String subtitle,
@Nullable String meta,
@Nullable String badge,
@Nullable View.OnClickListener listener
) {
LinearLayout row = buildListRow(context, title, subtitle, meta, badge, listener);
row.setBackgroundColor(Color.WHITE);
row.setElevation(0f);
row.setPadding(dp(context, 18), dp(context, 15), dp(context, 18), dp(context, 15));
return row;
}
public static LinearLayout buildSimpleProfileHeader(
Context context,
String name,
String subtitle,
@Nullable String detail
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.HORIZONTAL);
card.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = dp(context, 10);
card.setLayoutParams(params);
card.setPadding(dp(context, 20), dp(context, 18), dp(context, 20), dp(context, 18));
card.setBackgroundColor(Color.WHITE);
card.setElevation(0f);
TextView avatar = new TextView(context);
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(dp(context, 70), dp(context, 70));
avatar.setLayoutParams(avatarParams);
avatar.setGravity(Gravity.CENTER);
avatar.setText(firstLetter(name));
avatar.setTextSize(30);
avatar.setTypeface(Typeface.DEFAULT_BOLD);
avatar.setTextColor(context.getColor(R.color.boss_green));
avatar.setBackground(createRoundedBackground(Color.parseColor("#DFF3E8"), dp(context, 35)));
card.addView(avatar);
LinearLayout textWrap = new LinearLayout(context);
textWrap.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textParams.leftMargin = dp(context, 14);
textWrap.setLayoutParams(textParams);
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(name) ? "我的" : name);
titleView.setTextSize(22);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
textWrap.addView(titleView);
TextView subtitleView = new TextView(context);
subtitleView.setText(subtitle);
subtitleView.setTextSize(14);
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
subtitleView.setPadding(0, dp(context, 5), 0, 0);
textWrap.addView(subtitleView);
if (!TextUtils.isEmpty(detail)) {
TextView detailView = new TextView(context);
detailView.setText(detail);
detailView.setTextSize(12);
detailView.setTextColor(context.getColor(R.color.boss_text_soft));
detailView.setPadding(0, dp(context, 5), 0, 0);
textWrap.addView(detailView);
}
card.addView(textWrap);
return card;
}
public static LinearLayout buildFormCell(
Context context,
String label,
@Nullable String helper,
View field
) {
LinearLayout cell = new LinearLayout(context);
cell.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 12);
params.rightMargin = dp(context, 12);
params.bottomMargin = dp(context, 10);
cell.setLayoutParams(params);
cell.setPadding(dp(context, 16), dp(context, 14), dp(context, 16), dp(context, 14));
cell.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
TextView labelView = new TextView(context);
labelView.setText(label);
labelView.setTextSize(14);
labelView.setTypeface(Typeface.DEFAULT_BOLD);
labelView.setTextColor(context.getColor(R.color.boss_text_primary));
cell.addView(labelView);
if (!TextUtils.isEmpty(helper)) {
TextView helperView = new TextView(context);
helperView.setText(helper);
helperView.setTextSize(12);
helperView.setTextColor(context.getColor(R.color.boss_text_soft));
helperView.setPadding(0, dp(context, 4), 0, dp(context, 10));
cell.addView(helperView);
}
field.setPadding(field.getPaddingLeft(), field.getPaddingTop(), field.getPaddingRight(), field.getPaddingBottom());
cell.addView(field);
return cell;
}
public static LinearLayout buildDeviceCard(
Context context,
WechatSurfaceMapper.DeviceRow row,
@Nullable View.OnClickListener listener,
@Nullable View.OnLongClickListener longClickListener
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.HORIZONTAL);
card.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 12);
params.rightMargin = dp(context, 12);
params.bottomMargin = dp(context, 12);
card.setLayoutParams(params);
card.setPadding(dp(context, 14), dp(context, 14), dp(context, 14), dp(context, 14));
card.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
card.setElevation(dp(context, 1));
if (listener != null) {
card.setClickable(true);
card.setFocusable(true);
card.setOnClickListener(listener);
}
if (longClickListener != null) {
card.setOnLongClickListener(longClickListener);
}
FrameLayout avatarWrap = new FrameLayout(context);
LinearLayout.LayoutParams avatarWrapParams = new LinearLayout.LayoutParams(dp(context, 56), dp(context, 56));
avatarWrap.setLayoutParams(avatarWrapParams);
TextView avatar = new TextView(context);
FrameLayout.LayoutParams avatarParams = new FrameLayout.LayoutParams(dp(context, 52), dp(context, 52));
avatar.setLayoutParams(avatarParams);
avatar.setGravity(Gravity.CENTER);
avatar.setText(firstLetter(firstNonEmpty(row.avatarLabel, row.title, "")));
avatar.setTextSize(22);
avatar.setTypeface(Typeface.DEFAULT_BOLD);
avatar.setTextColor(resolveDeviceAccentColor(row.statusKey));
avatar.setBackground(createRoundedBackground(resolveDeviceAvatarBackground(row.statusKey), dp(context, 18)));
avatarWrap.addView(avatar);
View statusDot = new View(context);
FrameLayout.LayoutParams dotParams = new FrameLayout.LayoutParams(dp(context, 10), dp(context, 10), Gravity.END | Gravity.CENTER_VERTICAL);
dotParams.rightMargin = dp(context, 1);
statusDot.setLayoutParams(dotParams);
statusDot.setBackground(createRoundedBackground(resolveDeviceAccentColor(row.statusKey), dp(context, 5)));
avatarWrap.addView(statusDot);
card.addView(avatarWrap);
LinearLayout textWrap = new LinearLayout(context);
textWrap.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textParams.leftMargin = dp(context, 12);
textWrap.setLayoutParams(textParams);
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(row.title) ? "设备" : row.title);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
titleView.setMaxLines(1);
titleView.setEllipsize(TextUtils.TruncateAt.END);
textWrap.addView(titleView);
TextView subtitleView = new TextView(context);
subtitleView.setText(row.subtitle);
subtitleView.setTextSize(13);
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
subtitleView.setPadding(0, dp(context, 5), 0, 0);
subtitleView.setMaxLines(2);
subtitleView.setEllipsize(TextUtils.TruncateAt.END);
textWrap.addView(subtitleView);
TextView metaView = new TextView(context);
metaView.setText(row.meta);
metaView.setTextSize(12);
metaView.setTextColor(resolveDeviceMetaColor(context, row.statusKey));
metaView.setPadding(0, dp(context, 6), 0, 0);
metaView.setMaxLines(2);
metaView.setEllipsize(TextUtils.TruncateAt.END);
textWrap.addView(metaView);
card.addView(textWrap);
if (listener != null) {
TextView arrowView = new TextView(context);
arrowView.setText("");
arrowView.setTextSize(18);
arrowView.setTextColor(context.getColor(R.color.boss_text_soft));
arrowView.setPadding(dp(context, 8), 0, 0, 0);
card.addView(arrowView);
}
return card;
}
public static LinearLayout buildSoftPanel(
Context context,
String title,
String body,
@Nullable String meta
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 12);
params.rightMargin = dp(context, 12);
params.bottomMargin = dp(context, 12);
card.setLayoutParams(params);
card.setPadding(dp(context, 16), dp(context, 16), dp(context, 16), dp(context, 16));
card.setBackground(createRoundedBackground(Color.parseColor("#E7F5ED"), dp(context, 18)));
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(16);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
card.addView(titleView);
TextView bodyView = new TextView(context);
bodyView.setText(body);
bodyView.setTextSize(14);
bodyView.setLineSpacing(0f, 1.2f);
bodyView.setTextColor(context.getColor(R.color.boss_text_primary));
bodyView.setPadding(0, dp(context, 8), 0, 0);
card.addView(bodyView);
if (!TextUtils.isEmpty(meta)) {
TextView metaView = new TextView(context);
metaView.setText(meta);
metaView.setTextSize(12);
metaView.setTextColor(context.getColor(R.color.boss_text_muted));
metaView.setPadding(0, dp(context, 10), 0, 0);
card.addView(metaView);
}
return card;
}
public static LinearLayout buildInlineActionRow(Context context, Button... buttons) {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
rowParams.leftMargin = dp(context, 12);
rowParams.rightMargin = dp(context, 12);
rowParams.bottomMargin = dp(context, 12);
row.setLayoutParams(rowParams);
for (int i = 0; i < buttons.length; i++) {
Button button = buttons[i];
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
if (i > 0) {
buttonParams.leftMargin = dp(context, 8);
}
button.setLayoutParams(buttonParams);
row.addView(button);
}
return row;
}
public static Button buildMiniActionButton(Context context, String label, boolean primary) {
Button button = new Button(context);
button.setText(label);
button.setAllCaps(false);
button.setTextSize(13);
button.setPadding(dp(context, 10), dp(context, 10), dp(context, 10), dp(context, 10));
button.setTextColor(context.getColor(primary ? R.color.boss_surface : R.color.boss_green));
button.setBackgroundResource(primary ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
return button;
}
public static LinearLayout buildConversationRow(
Context context,
WechatSurfaceMapper.ConversationRow row,
@Nullable View.OnClickListener listener
) {
return buildConversationRow(context, row, false, false, listener);
}
public static LinearLayout buildConversationRow(
Context context,
WechatSurfaceMapper.ConversationRow row,
boolean selectionMode,
boolean selected,
@Nullable View.OnClickListener listener
) {
return buildConversationRow(context, row, selectionMode, selected, false, listener);
}
public static LinearLayout buildConversationRow(
Context context,
WechatSurfaceMapper.ConversationRow row,
boolean selectionMode,
boolean selected,
boolean highlighted,
@Nullable View.OnClickListener listener
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.HORIZONTAL);
card.setGravity(Gravity.TOP);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = dp(context, 1);
card.setLayoutParams(params);
card.setPadding(dp(context, 16), dp(context, 12), dp(context, 16), dp(context, 12));
card.setBackgroundColor(highlighted ? Color.parseColor("#EAF8F0") : row.pinnedConversation ? PINNED_ROW_BG : Color.WHITE);
card.setElevation(0f);
if (listener != null) {
card.setClickable(true);
card.setFocusable(true);
card.setOnClickListener(listener);
}
if (selectionMode) {
card.setContentDescription(selected ? "已选中会话" : "未选中会话");
}
card.addView(buildConversationAvatar(context, row));
LinearLayout centerColumn = new LinearLayout(context);
centerColumn.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams centerParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
centerParams.leftMargin = dp(context, 12);
centerParams.rightMargin = dp(context, 10);
centerColumn.setLayoutParams(centerParams);
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(row.threadTitle) ? "未命名会话" : row.threadTitle);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
titleView.setMaxLines(1);
titleView.setEllipsize(TextUtils.TruncateAt.END);
centerColumn.addView(titleView);
if (!TextUtils.isEmpty(row.folderLabel)) {
TextView folderView = new TextView(context);
folderView.setText(row.folderLabel);
folderView.setTextSize(13);
folderView.setTextColor(context.getColor(R.color.boss_text_muted));
folderView.setPadding(0, dp(context, 4), 0, 0);
folderView.setMaxLines(1);
folderView.setEllipsize(TextUtils.TruncateAt.END);
centerColumn.addView(folderView);
}
TextView previewView = new TextView(context);
previewView.setText(TextUtils.isEmpty(row.lastMessagePreview) ? "暂无消息" : row.lastMessagePreview);
previewView.setTextSize(13);
previewView.setTextColor(context.getColor(R.color.boss_text_soft));
previewView.setPadding(0, dp(context, 4), 0, 0);
previewView.setMaxLines(1);
previewView.setEllipsize(TextUtils.TruncateAt.END);
centerColumn.addView(previewView);
card.addView(centerColumn);
LinearLayout trailingColumn = new LinearLayout(context);
trailingColumn.setOrientation(LinearLayout.VERTICAL);
trailingColumn.setGravity(Gravity.END);
trailingColumn.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
if (!row.pinnedConversation && !TextUtils.isEmpty(row.topPinnedLabel)) {
TextView pinnedView = new TextView(context);
pinnedView.setText(row.topPinnedLabel);
pinnedView.setTextSize(11);
pinnedView.setTypeface(Typeface.DEFAULT_BOLD);
pinnedView.setTextColor(context.getColor(R.color.boss_green));
pinnedView.setBackground(createRoundedBackground(Color.parseColor("#E7F7EE"), dp(context, 9)));
pinnedView.setPadding(dp(context, 7), dp(context, 3), dp(context, 7), dp(context, 3));
pinnedView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
trailingColumn.addView(pinnedView);
}
TextView timeView = new TextView(context);
timeView.setText(TextUtils.isEmpty(row.timeLabel) ? "--:--" : row.timeLabel);
timeView.setTextSize(12);
timeView.setTextColor(context.getColor(R.color.boss_text_muted));
timeView.setPadding(0, dp(context, TextUtils.isEmpty(row.topPinnedLabel) || row.pinnedConversation ? 2 : 8), 0, 0);
timeView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
trailingColumn.addView(timeView);
if (selectionMode) {
View selector = buildConversationSelectionIndicator(context, selected);
LinearLayout.LayoutParams selectorParams = new LinearLayout.LayoutParams(dp(context, 20), dp(context, 20));
selectorParams.topMargin = dp(context, 8);
selector.setLayoutParams(selectorParams);
trailingColumn.addView(selector);
}
if (highlighted) {
TextView targetView = new TextView(context);
targetView.setText("目标线程");
targetView.setTextSize(11);
targetView.setTypeface(Typeface.DEFAULT_BOLD);
targetView.setTextColor(context.getColor(R.color.boss_green));
targetView.setBackground(createRoundedBackground(Color.parseColor("#E7F7EE"), dp(context, 9)));
targetView.setPadding(dp(context, 7), dp(context, 3), dp(context, 7), dp(context, 3));
LinearLayout.LayoutParams targetParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
targetParams.topMargin = dp(context, 8);
targetView.setLayoutParams(targetParams);
trailingColumn.addView(targetView);
}
if (row.unreadCount > 0) {
TextView unreadView = new TextView(context);
unreadView.setText(row.unreadCount > 99 ? "99+" : String.valueOf(row.unreadCount));
unreadView.setTextSize(11);
unreadView.setTypeface(Typeface.DEFAULT_BOLD);
unreadView.setTextColor(Color.WHITE);
unreadView.setGravity(Gravity.CENTER);
unreadView.setMinWidth(dp(context, 20));
unreadView.setBackground(createRoundedBackground(Color.parseColor("#FF5A5A"), dp(context, 10)));
unreadView.setPadding(dp(context, 6), dp(context, 2), dp(context, 6), dp(context, 2));
LinearLayout.LayoutParams unreadParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
unreadParams.topMargin = dp(context, 8);
unreadView.setLayoutParams(unreadParams);
trailingColumn.addView(unreadView);
}
int activityCount = Math.max(0, Math.min(row.activityIconCount, WechatSurfaceMapper.maxConversationActivityIcons()));
if (activityCount > 0) {
LinearLayout activityWrap = new LinearLayout(context);
activityWrap.setOrientation(LinearLayout.HORIZONTAL);
activityWrap.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams activityParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
activityParams.topMargin = dp(context, 8);
activityWrap.setLayoutParams(activityParams);
for (int i = 0; i < activityCount; i++) {
View dot = buildAnimatedActivityDot(context, i);
if (i > 0) {
LinearLayout.LayoutParams dotParams = (LinearLayout.LayoutParams) dot.getLayoutParams();
dotParams.leftMargin = dp(context, 4);
dot.setLayoutParams(dotParams);
}
activityWrap.addView(dot);
}
trailingColumn.addView(activityWrap);
}
if (row.contextIndicatorVisible) {
FrameLayout ringWrap = new FrameLayout(context);
LinearLayout.LayoutParams ringWrapParams = new LinearLayout.LayoutParams(dp(context, 24), dp(context, 24));
ringWrapParams.topMargin = dp(context, 6);
ringWrap.setLayoutParams(ringWrapParams);
ringWrap.setBackground(createRoundedBackground(CONTEXT_RING_BG, dp(context, 12)));
ringWrap.setContentDescription(TextUtils.isEmpty(row.contextStatusLabel)
? "上下文使用量"
: "上下文使用量:" + row.contextStatusLabel);
View ring = buildContextUsageRing(
context,
row.contextUsagePercent,
row.contextStatusLevel,
row.contextMustFinish
);
FrameLayout.LayoutParams ringParams = new FrameLayout.LayoutParams(dp(context, 16), dp(context, 16), Gravity.CENTER);
ring.setLayoutParams(ringParams);
ringWrap.addView(ring);
trailingColumn.addView(ringWrap);
}
card.addView(trailingColumn);
return card;
}
private static View buildConversationSelectionIndicator(Context context, boolean selected) {
View indicator = new View(context);
GradientDrawable drawable = new GradientDrawable();
drawable.setShape(GradientDrawable.OVAL);
drawable.setColor(selected ? context.getColor(R.color.boss_green) : Color.WHITE);
drawable.setStroke(dp(context, 1.5f), selected ? context.getColor(R.color.boss_green) : Color.parseColor("#FFC8C8C8"));
indicator.setBackground(drawable);
indicator.setContentDescription(selected ? "已选中会话" : "未选中会话");
return indicator;
}
public static LinearLayout buildConversationSectionHeader(
Context context,
String title,
String actionLabel,
@Nullable View.OnClickListener listener
) {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 16);
params.rightMargin = dp(context, 16);
params.bottomMargin = dp(context, 6);
row.setLayoutParams(params);
row.setPadding(0, dp(context, 4), 0, dp(context, 2));
if (listener != null) {
row.setClickable(true);
row.setFocusable(true);
row.setOnClickListener(listener);
}
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(13);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_muted));
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
titleView.setLayoutParams(titleParams);
row.addView(titleView);
TextView actionView = new TextView(context);
actionView.setText(actionLabel);
actionView.setTextSize(12);
actionView.setTextColor(context.getColor(R.color.boss_text_soft));
row.addView(actionView);
return row;
}
public static LinearLayout buildEmptyCard(Context context, String text) {
return buildCard(context, "暂无内容", text, "下拉或点击顶部刷新按钮重试。");
}
public static TextView buildHintPill(Context context, String text) {
TextView pill = new TextView(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 12);
params.rightMargin = dp(context, 12);
params.bottomMargin = dp(context, 12);
pill.setLayoutParams(params);
pill.setText(text);
pill.setTextSize(12);
pill.setTextColor(context.getColor(R.color.boss_text_muted));
pill.setPadding(dp(context, 12), dp(context, 7), dp(context, 12), dp(context, 7));
pill.setBackground(createRoundedBackground(Color.parseColor("#F4F5F2"), dp(context, 14)));
return pill;
}
public static LinearLayout buildMessageBubble(
Context context,
String senderLabel,
String body,
@Nullable String meta,
boolean outgoing,
@Nullable String kindLabel
) {
LinearLayout wrapper = buildMessageWrapper(context, senderLabel, meta, outgoing);
LinearLayout bubble = new LinearLayout(context);
bubble.setOrientation(LinearLayout.VERTICAL);
bubble.setBackgroundResource(outgoing ? R.drawable.bg_message_outgoing : R.drawable.bg_message_incoming);
bubble.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
int maxBubbleWidth = Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.72f);
bubble.setMinimumWidth(dp(context, 84));
if (!TextUtils.isEmpty(kindLabel)) {
TextView kindView = new TextView(context);
kindView.setText(kindLabel);
kindView.setTextSize(11);
kindView.setTypeface(Typeface.DEFAULT_BOLD);
kindView.setTextColor(context.getColor(outgoing ? R.color.boss_surface : R.color.boss_text_muted));
kindView.setPadding(0, 0, 0, dp(context, 6));
bubble.addView(kindView);
}
TextView bodyView = new TextView(context);
bodyView.setText(BossMarkdown.render(context, body, outgoing));
bodyView.setTextSize(15);
bodyView.setLineSpacing(0f, 1.34f);
bodyView.setTextColor(context.getColor(outgoing ? R.color.boss_surface : R.color.boss_text_primary));
bodyView.setMaxWidth(maxBubbleWidth);
bodyView.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
bodyView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
bubble.addView(bodyView);
wrapper.addView(bubble);
return wrapper;
}
public static LinearLayout buildAttachmentMessageCard(
Context context,
String senderLabel,
String sourceType,
String fileName,
@Nullable String detail,
@Nullable String status,
@Nullable String summary,
@Nullable String actionLabel,
@Nullable View.OnClickListener actionListener,
@Nullable String meta,
boolean outgoing,
@Nullable View.OnClickListener cardClickListener
) {
LinearLayout wrapper = buildMessageWrapper(context, senderLabel, meta, outgoing);
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
card.setMinimumWidth(dp(context, 180));
card.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
card.setBackground(createRoundedBackground(
outgoing ? Color.parseColor("#CFF0D8") : Color.WHITE,
dp(context, 18)
));
card.setElevation(dp(context, 1));
if (cardClickListener != null) {
card.setClickable(true);
card.setFocusable(true);
card.setOnClickListener(cardClickListener);
}
if ("image".equals(sourceType) || "video".equals(sourceType)) {
TextView preview = new TextView(context);
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.56f),
dp(context, 118)
);
preview.setLayoutParams(previewParams);
preview.setGravity(Gravity.CENTER);
preview.setText("image".equals(sourceType) ? "图片预览" : "视频封面");
preview.setTextSize(13);
preview.setTypeface(Typeface.DEFAULT_BOLD);
preview.setTextColor(context.getColor(R.color.boss_text_muted));
preview.setBackground(createRoundedBackground(Color.parseColor("#EEF2EE"), dp(context, 14)));
card.addView(preview);
TextView nameView = buildAttachmentPrimaryText(context, fileName);
nameView.setPadding(0, dp(context, 10), 0, 0);
card.addView(nameView);
TextView statusView = buildAttachmentSecondaryText(
context,
TextUtils.isEmpty(status) ? "已发送" : status
);
statusView.setPadding(0, dp(context, 6), 0, 0);
card.addView(statusView);
} else {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL);
TextView icon = new TextView(context);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dp(context, 42), dp(context, 42));
icon.setLayoutParams(iconParams);
icon.setGravity(Gravity.CENTER);
icon.setText("");
icon.setTextSize(15);
icon.setTypeface(Typeface.DEFAULT_BOLD);
icon.setTextColor(context.getColor(R.color.boss_green));
icon.setBackground(createRoundedBackground(Color.parseColor("#E8F6ED"), dp(context, 12)));
row.addView(icon);
LinearLayout texts = new LinearLayout(context);
texts.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textParams.leftMargin = dp(context, 12);
texts.setLayoutParams(textParams);
texts.addView(buildAttachmentPrimaryText(context, fileName));
texts.addView(buildAttachmentSecondaryText(
context,
joinAttachmentDetail(detail, status)
));
row.addView(texts);
card.addView(row);
}
if (!TextUtils.isEmpty(summary)) {
TextView summaryView = buildAttachmentSecondaryText(context, summary);
summaryView.setPadding(0, dp(context, 10), 0, 0);
summaryView.setTextColor(context.getColor(R.color.boss_text_primary));
card.addView(summaryView);
}
if (!TextUtils.isEmpty(actionLabel) && actionListener != null) {
Button actionButton = new Button(context);
LinearLayout.LayoutParams actionParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
dp(context, 34)
);
actionParams.topMargin = dp(context, 12);
actionButton.setLayoutParams(actionParams);
actionButton.setMinWidth(dp(context, 88));
actionButton.setText(actionLabel);
actionButton.setAllCaps(false);
actionButton.setTextSize(13);
actionButton.setPadding(dp(context, 14), 0, dp(context, 14), 0);
actionButton.setBackgroundResource(R.drawable.bg_secondary_button);
actionButton.setTextColor(context.getColor(R.color.boss_text_primary));
actionButton.setOnClickListener(actionListener);
card.addView(actionButton);
}
wrapper.addView(card);
return wrapper;
}
public static LinearLayout buildForwardSingleBubble(
Context context,
String senderLabel,
String body,
@Nullable String meta,
@Nullable String sourceLabel,
boolean outgoing
) {
LinearLayout wrapper = buildMessageBubble(
context,
senderLabel,
body,
meta,
outgoing,
ProjectChatUiState.labelForForwardKind("forward_single")
);
if (wrapper.getChildCount() < 2 || !(wrapper.getChildAt(1) instanceof LinearLayout)) {
return wrapper;
}
LinearLayout bubble = (LinearLayout) wrapper.getChildAt(1);
if (!TextUtils.isEmpty(sourceLabel)) {
TextView sourceView = new TextView(context);
sourceView.setText("来自 " + sourceLabel);
sourceView.setTextSize(12);
sourceView.setTextColor(context.getColor(outgoing ? R.color.boss_surface : R.color.boss_text_muted));
sourceView.setAlpha(outgoing ? 0.85f : 1f);
sourceView.setPadding(0, 0, 0, dp(context, 8));
bubble.addView(sourceView, Math.min(1, bubble.getChildCount()));
}
return wrapper;
}
public static LinearLayout buildForwardBundleCard(
Context context,
String senderLabel,
String cardTitle,
String summary,
@Nullable String meta,
boolean outgoing
) {
LinearLayout wrapper = new LinearLayout(context);
wrapper.setOrientation(LinearLayout.VERTICAL);
wrapper.setGravity(outgoing ? Gravity.END : Gravity.START);
LinearLayout.LayoutParams wrapperParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
wrapperParams.bottomMargin = dp(context, 12);
wrapper.setLayoutParams(wrapperParams);
TextView metaView = new TextView(context);
String metaText = buildMessageMetaText(senderLabel, meta, outgoing);
metaView.setText(metaText);
metaView.setTextSize(11);
metaView.setTextColor(context.getColor(R.color.boss_text_soft));
metaView.setPadding(dp(context, 6), 0, dp(context, 6), dp(context, 4));
wrapper.addView(metaView);
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
card.setMinimumWidth(dp(context, 180));
card.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
card.setBackground(createRoundedBackground(
outgoing ? Color.parseColor("#CFF0D8") : Color.WHITE,
dp(context, 18)
));
card.setElevation(dp(context, 1));
TextView kindView = new TextView(context);
kindView.setText(ProjectChatUiState.labelForForwardKind("forward_bundle"));
kindView.setTextSize(11);
kindView.setTypeface(Typeface.DEFAULT_BOLD);
kindView.setTextColor(context.getColor(R.color.boss_text_muted));
card.addView(kindView);
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(cardTitle) ? "聊天记录" : cardTitle);
titleView.setTextSize(15);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
titleView.setPadding(0, dp(context, 6), 0, 0);
titleView.setMaxLines(2);
titleView.setEllipsize(TextUtils.TruncateAt.END);
card.addView(titleView);
TextView summaryView = new TextView(context);
summaryView.setText(TextUtils.isEmpty(summary) ? "转发的聊天记录" : summary);
summaryView.setTextSize(13);
summaryView.setLineSpacing(0f, 1.2f);
summaryView.setTextColor(context.getColor(R.color.boss_text_muted));
summaryView.setPadding(0, dp(context, 8), 0, 0);
summaryView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.72f));
card.addView(summaryView);
wrapper.addView(card);
return wrapper;
}
public static LinearLayout buildPendingOutgoingMessageBubble(
Context context,
String senderLabel,
String body
) {
String effectiveSender = TextUtils.isEmpty(senderLabel) ? "" : senderLabel;
return buildMessageBubble(context, effectiveSender, body, "发送中", true, null);
}
public static LinearLayout buildExecutionWarningCard(
Context context,
String title,
@Nullable String summary,
boolean outgoing
) {
LinearLayout card = new LinearLayout(context);
card.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 8);
card.setLayoutParams(params);
card.setPadding(dp(context, 12), dp(context, 10), dp(context, 12), dp(context, 10));
card.setBackground(createRoundedBackground(
outgoing ? Color.parseColor("#FFF1D7") : Color.parseColor("#FFF7E7"),
dp(context, 14)
));
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(title) ? "执行提醒" : title);
titleView.setTextSize(12);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(Color.parseColor("#B36B00"));
card.addView(titleView);
if (!TextUtils.isEmpty(summary)) {
TextView summaryView = new TextView(context);
summaryView.setText(summary);
summaryView.setTextSize(13);
summaryView.setLineSpacing(0f, 1.2f);
summaryView.setTextColor(context.getColor(R.color.boss_text_primary));
summaryView.setPadding(0, dp(context, 4), 0, 0);
summaryView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.62f));
summaryView.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
summaryView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
card.addView(summaryView);
}
return card;
}
public static LinearLayout buildMessageStatusRow(
Context context,
JSONObject message,
@Nullable JSONObject conversationTask,
List<JSONObject> warnings,
boolean outgoing
) {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
rowParams.topMargin = dp(context, 8);
row.setLayoutParams(rowParams);
boolean hasTask = conversationTask != null;
boolean hasWarnings = warnings != null && !warnings.isEmpty();
String detailText = buildStatusDetailText(message, conversationTask, warnings);
boolean hasDetail = !TextUtils.isEmpty(detailText);
if (!hasTask && !hasWarnings && !hasDetail) {
row.setVisibility(View.GONE);
return row;
}
if (hasTask || hasWarnings) {
LinearLayout pillRow = new LinearLayout(context);
pillRow.setOrientation(LinearLayout.HORIZONTAL);
pillRow.setGravity(Gravity.CENTER_VERTICAL);
row.addView(pillRow);
if (hasTask) {
pillRow.addView(buildStatusPill(
context,
taskStatusLabel(conversationTask.optString("status", "")),
outgoing ? Color.parseColor("#D7F3DF") : Color.parseColor("#EDF7F0"),
Color.parseColor("#215B39")
));
}
if (hasWarnings) {
if (pillRow.getChildCount() > 0) {
View spacer = new View(context);
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(dp(context, 6), 1);
spacer.setLayoutParams(spacerParams);
pillRow.addView(spacer);
}
pillRow.addView(buildStatusPill(
context,
warningBadgeLabel(warnings),
outgoing ? Color.parseColor("#FFF1D7") : Color.parseColor("#FFF7E7"),
Color.parseColor("#B36B00")
));
}
}
if (hasDetail) {
TextView detailView = new TextView(context);
detailView.setText(detailText);
detailView.setTextSize(12);
detailView.setTextColor(context.getColor(R.color.boss_text_muted));
detailView.setPadding(dp(context, 2), dp(context, 4), dp(context, 2), 0);
detailView.setMaxLines(2);
detailView.setEllipsize(TextUtils.TruncateAt.END);
detailView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.62f));
row.addView(detailView);
}
return row;
}
private static TextView buildStatusPill(
Context context,
String text,
int backgroundColor,
int textColor
) {
TextView pill = new TextView(context);
pill.setText(text);
pill.setTextSize(11);
pill.setTypeface(Typeface.DEFAULT_BOLD);
pill.setTextColor(textColor);
pill.setPadding(dp(context, 10), dp(context, 5), dp(context, 10), dp(context, 5));
pill.setBackground(createRoundedBackground(backgroundColor, dp(context, 12)));
return pill;
}
private static String taskStatusLabel(String status) {
switch (status) {
case "queued":
return "排队中";
case "running":
return "执行中";
case "completed":
return "已完成";
case "failed":
return "已失败";
default:
return TextUtils.isEmpty(status) ? "处理中" : status;
}
}
private static String warningBadgeLabel(List<JSONObject> warnings) {
int totalWarnings = 0;
for (JSONObject warning : warnings) {
if (warning == null) {
continue;
}
totalWarnings += Math.max(1, warning.optInt("count", 1));
}
return totalWarnings > 1 ? "提醒 " + totalWarnings : "执行提醒";
}
private static String buildStatusDetailText(
JSONObject message,
@Nullable JSONObject conversationTask,
List<JSONObject> warnings
) {
ArrayList<String> parts = new ArrayList<>();
if (conversationTask != null) {
String sessionId = conversationTask.optString("sessionId", "");
String taskId = conversationTask.optString("taskId", "");
if (!TextUtils.isEmpty(sessionId)) {
parts.add("Session " + sessionId);
} else if (!TextUtils.isEmpty(taskId)) {
parts.add("Task " + taskId);
}
}
if (warnings != null && !warnings.isEmpty()) {
JSONObject firstWarning = warnings.get(0);
if (firstWarning != null) {
String summary = firstWarning.optString("summary", "");
String title = firstWarning.optString("title", "执行提醒");
int count = Math.max(1, firstWarning.optInt("count", 1));
String warningText = !TextUtils.isEmpty(summary) ? summary : title;
if (count > 1) {
warningText = warningText + "" + count + "";
} else if (warnings.size() > 1) {
warningText = warningText + "" + warnings.size() + "";
}
parts.add(warningText);
}
} else if ("system_notice".equals(message.optString("kind", ""))) {
parts.add("系统同步提醒");
}
return TextUtils.join(" · ", parts);
}
public static void applyMessageSelectionState(Context context, View messageView, boolean selected) {
if (messageView == null) {
return;
}
if (selected) {
messageView.setBackground(createRoundedBackground(Color.parseColor("#EAF7F0"), dp(context, 20)));
messageView.setAlpha(1f);
} else {
messageView.setBackgroundColor(Color.TRANSPARENT);
messageView.setAlpha(1f);
}
}
public static TextView buildMessagePlaceholder(Context context, String text) {
TextView placeholder = new TextView(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 24);
placeholder.setLayoutParams(params);
placeholder.setGravity(Gravity.CENTER);
placeholder.setPadding(dp(context, 24), dp(context, 12), dp(context, 24), dp(context, 12));
placeholder.setText(text);
placeholder.setTextSize(13);
placeholder.setTextColor(context.getColor(R.color.boss_text_soft));
return placeholder;
}
public static Button buildPrimaryButton(Context context, String label) {
Button button = new Button(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 12);
button.setLayoutParams(params);
button.setText(label);
button.setTextColor(context.getColor(R.color.boss_surface));
button.setBackgroundResource(R.drawable.bg_primary_button);
button.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
button.setAllCaps(false);
return button;
}
public static Button buildSecondaryButton(Context context, String label) {
Button button = new Button(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.topMargin = dp(context, 10);
button.setLayoutParams(params);
button.setText(label);
button.setTextColor(context.getColor(R.color.boss_green));
button.setBackgroundResource(R.drawable.bg_secondary_button);
button.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
button.setAllCaps(false);
return button;
}
private static LinearLayout buildMessageWrapper(
Context context,
String senderLabel,
@Nullable String meta,
boolean outgoing
) {
LinearLayout wrapper = new LinearLayout(context);
wrapper.setOrientation(LinearLayout.VERTICAL);
wrapper.setGravity(outgoing ? Gravity.END : Gravity.START);
LinearLayout.LayoutParams wrapperParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
wrapperParams.bottomMargin = dp(context, 12);
wrapper.setLayoutParams(wrapperParams);
TextView metaView = new TextView(context);
String metaText = buildMessageMetaText(senderLabel, meta, outgoing);
metaView.setText(metaText);
metaView.setTextSize(11);
metaView.setTextColor(context.getColor(R.color.boss_text_soft));
metaView.setPadding(dp(context, 6), 0, dp(context, 6), dp(context, 4));
wrapper.addView(metaView);
return wrapper;
}
private static String buildMessageMetaText(
String senderLabel,
@Nullable String meta,
boolean outgoing
) {
if (outgoing && !TextUtils.isEmpty(meta)) {
return meta;
}
if (TextUtils.isEmpty(meta)) {
return senderLabel;
}
if (TextUtils.isEmpty(senderLabel)) {
return meta;
}
return senderLabel + " · " + meta;
}
private static TextView buildAttachmentPrimaryText(Context context, String text) {
TextView primary = new TextView(context);
primary.setText(TextUtils.isEmpty(text) ? "未命名附件" : text);
primary.setTextSize(15);
primary.setTypeface(Typeface.DEFAULT_BOLD);
primary.setTextColor(context.getColor(R.color.boss_text_primary));
primary.setMaxLines(2);
primary.setEllipsize(TextUtils.TruncateAt.END);
return primary;
}
private static TextView buildAttachmentSecondaryText(Context context, String text) {
TextView secondary = new TextView(context);
secondary.setText(TextUtils.isEmpty(text) ? "已发送" : text);
secondary.setTextSize(13);
secondary.setTextColor(context.getColor(R.color.boss_text_muted));
secondary.setPadding(0, dp(context, 6), 0, 0);
return secondary;
}
private static String joinAttachmentDetail(@Nullable String detail, @Nullable String status) {
if (TextUtils.isEmpty(detail)) {
return TextUtils.isEmpty(status) ? "已发送" : status;
}
if (TextUtils.isEmpty(status)) {
return detail;
}
return detail + " · " + status;
}
public static EditText buildInput(Context context, String hint, boolean multiline) {
EditText input = new EditText(context);
input.setHint(hint);
input.setTextColor(context.getColor(R.color.boss_text_primary));
input.setHintTextColor(context.getColor(R.color.boss_text_muted));
input.setBackgroundResource(R.drawable.bg_secondary_button);
input.setPadding(dp(context, 14), dp(context, 12), dp(context, 14), dp(context, 12));
input.setSingleLine(!multiline);
if (multiline) {
input.setMinLines(3);
input.setMaxLines(6);
}
return input;
}
public static void copyText(Context context, String label, String text) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard != null) {
clipboard.setPrimaryClip(ClipData.newPlainText(label, text));
Toast.makeText(context, label + " 已复制", Toast.LENGTH_SHORT).show();
}
}
public static int dp(Context context, int value) {
return Math.round(value * context.getResources().getDisplayMetrics().density);
}
public static int dp(Context context, float value) {
return Math.round(value * context.getResources().getDisplayMetrics().density);
}
private static View buildConversationAvatar(Context context, WechatSurfaceMapper.ConversationRow row) {
if (!row.isGroup) {
return buildAvatarCircle(
context,
firstNonEmpty(row.avatarPrimary, row.threadTitle, ""),
AVATAR_BG_COLORS[0],
Color.WHITE,
52
);
}
FrameLayout groupWrap = new FrameLayout(context);
LinearLayout.LayoutParams wrapParams = new LinearLayout.LayoutParams(dp(context, 52), dp(context, 52));
groupWrap.setLayoutParams(wrapParams);
GradientDrawable bg = createRoundedBackground(Color.parseColor("#F2F6F3"), dp(context, 18));
bg.setStroke(dp(context, 1), Color.parseColor("#E5ECE7"));
groupWrap.setBackground(bg);
int visibleCount = Math.min(row.groupAvatarMembers.length, 4);
if (visibleCount == 0) {
groupWrap.addView(buildCenteredAvatarTile(context, "", AVATAR_BG_COLORS[1], 32, 10, 10));
return groupWrap;
}
int[][] offsets = {
{4, 4},
{24, 4},
{4, 24},
{24, 24}
};
for (int i = 0; i < visibleCount; i++) {
WechatSurfaceMapper.GroupAvatarMember member = row.groupAvatarMembers[i];
groupWrap.addView(buildCenteredAvatarTile(
context,
firstNonEmpty(member.avatarLabel, member.title, ""),
AVATAR_BG_COLORS[(i + 1) % AVATAR_BG_COLORS.length],
22,
offsets[i][0],
offsets[i][1]
));
}
return groupWrap;
}
private static TextView buildAvatarCircle(
Context context,
String labelSource,
int backgroundColor,
int textColor,
int sizeDp
) {
TextView avatarView = new TextView(context);
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(dp(context, sizeDp), dp(context, sizeDp));
avatarView.setLayoutParams(avatarParams);
avatarView.setGravity(Gravity.CENTER);
avatarView.setText(firstLetter(labelSource));
avatarView.setTextSize(sizeDp >= 48 ? 24 : 12);
avatarView.setTypeface(Typeface.DEFAULT_BOLD);
avatarView.setTextColor(textColor);
avatarView.setBackground(createRoundedBackground(backgroundColor, dp(context, sizeDp / 2)));
return avatarView;
}
private static View buildCenteredAvatarTile(
Context context,
String labelSource,
int backgroundColor,
int sizeDp,
int leftDp,
int topDp
) {
TextView tile = buildAvatarCircle(context, labelSource, backgroundColor, context.getColor(R.color.boss_text_primary), sizeDp);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(dp(context, sizeDp), dp(context, sizeDp));
params.leftMargin = dp(context, leftDp);
params.topMargin = dp(context, topDp);
tile.setLayoutParams(params);
return tile;
}
private static GradientDrawable createRoundedBackground(int color, int radiusPx) {
GradientDrawable drawable = new GradientDrawable();
drawable.setColor(color);
drawable.setCornerRadius(radiusPx);
return drawable;
}
private static int resolveDeviceAccentColor(String statusKey) {
if ("online".equals(statusKey)) {
return DEVICE_STATUS_ONLINE;
}
if ("abnormal".equals(statusKey)) {
return DEVICE_STATUS_ABNORMAL;
}
return DEVICE_STATUS_OFFLINE;
}
private static int resolveDeviceAvatarBackground(String statusKey) {
if ("online".equals(statusKey)) {
return Color.parseColor("#E5F6EC");
}
if ("abnormal".equals(statusKey)) {
return Color.parseColor("#FCE9E7");
}
return Color.parseColor("#F2F4F7");
}
private static int resolveDeviceMetaColor(Context context, String statusKey) {
if ("online".equals(statusKey)) {
return context.getColor(R.color.boss_text_muted);
}
return resolveDeviceAccentColor(statusKey);
}
private static View buildAnimatedActivityDot(Context context, int index) {
View dot = new View(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dp(context, 7), dp(context, 7));
dot.setLayoutParams(params);
GradientDrawable drawable = createRoundedBackground(
ACTIVITY_ICON_COLORS[index % ACTIVITY_ICON_COLORS.length],
dp(context, 4)
);
dot.setBackground(drawable);
dot.setAlpha(0.92f - (Math.min(index, 2) * 0.12f));
dot.setScaleX(1f);
dot.setScaleY(1f);
return dot;
}
private static int resolveConversationContextColor(Context context, @Nullable String level) {
if (TextUtils.isEmpty(level)) {
return context.getColor(R.color.boss_text_soft);
}
switch (level) {
case "critical":
return Color.parseColor("#D94B4B");
case "urgent":
return Color.parseColor("#E0832A");
case "watch":
return Color.parseColor("#7B8A82");
default:
return context.getColor(R.color.boss_text_soft);
}
}
private static View buildContextUsageRing(
Context context,
int usagePercent,
@Nullable String level,
boolean mustFinish
) {
final int clampedUsage = Math.max(0, Math.min(100, usagePercent));
final int ringColor = resolveContextRingColor(level, mustFinish);
final float strokeWidth = dp(context, 2f);
return new View(context) {
private final Paint trackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final RectF arcRect = new RectF();
{
trackPaint.setStyle(Paint.Style.STROKE);
trackPaint.setStrokeCap(Paint.Cap.ROUND);
trackPaint.setStrokeWidth(strokeWidth);
trackPaint.setColor(CONTEXT_RING_TRACK);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeCap(Paint.Cap.ROUND);
progressPaint.setStrokeWidth(strokeWidth);
progressPaint.setColor(ringColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float inset = strokeWidth / 2f + dp(context, 0.5f);
arcRect.set(inset, inset, getWidth() - inset, getHeight() - inset);
canvas.drawArc(arcRect, -90, 360, false, trackPaint);
canvas.drawArc(arcRect, -90, (360f * clampedUsage) / 100f, false, progressPaint);
}
};
}
private static int resolveContextRingColor(@Nullable String level, boolean mustFinish) {
if (mustFinish) {
return Color.parseColor("#D94B4B");
}
if (TextUtils.isEmpty(level)) {
return Color.parseColor("#8E8E93");
}
switch (level) {
case "critical":
return Color.parseColor("#D94B4B");
case "urgent":
return Color.parseColor("#8A8A8A");
case "watch":
return Color.parseColor("#9A9A9A");
default:
return Color.parseColor("#B0B0B0");
}
}
private static String firstLetter(String value) {
String text = value == null ? "" : value.trim();
if (text.isEmpty()) {
return "";
}
return text.substring(0, 1);
}
private static String firstNonEmpty(String first, String second, String fallback) {
if (!TextUtils.isEmpty(first)) {
return first;
}
if (!TextUtils.isEmpty(second)) {
return second;
}
return fallback;
}
}