zhibo/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java
xiao12feng8 2c91a66234 实现作品上热门功能(后端完成)
- 数据库:添加is_hot和hot_time字段
- Service层:实现toggleHot和getHotWorks方法
- 管理端API:添加设置热门和查询热门列表接口
- 用户端API:添加获取热门作品列表接口
- 实体类:更新Works和WorksResponse添加热门字段
- 文档:创建功能实现说明文档

后续需要:
- 后台管理界面(Vue)添加热门设置按钮
- Android App添加热门Tab页
2026-01-08 16:44:12 +08:00

1044 lines
44 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.example.livestreaming;
import android.graphics.Outline;
import android.net.Uri;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, RecyclerView.ViewHolder> {
public interface OnMessageLongClickListener {
void onMessageLongClick(ChatMessage message, int position, View view);
}
public interface OnImageClickListener {
void onImageClick(ChatMessage message, ImageView imageView);
}
public interface OnVoiceClickListener {
void onVoiceClick(ChatMessage message);
}
private OnMessageLongClickListener longClickListener;
private OnImageClickListener imageClickListener;
private OnVoiceClickListener voiceClickListener;
private static final int TYPE_TEXT_INCOMING = 1;
private static final int TYPE_TEXT_OUTGOING = 2;
private static final int TYPE_IMAGE_INCOMING = 3;
private static final int TYPE_IMAGE_OUTGOING = 4;
private static final int TYPE_VOICE_INCOMING = 5;
private static final int TYPE_VOICE_OUTGOING = 6;
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm", Locale.getDefault());
public ConversationMessagesAdapter() {
super(DIFF);
}
public void setOnMessageLongClickListener(OnMessageLongClickListener listener) {
this.longClickListener = listener;
}
public void setOnImageClickListener(OnImageClickListener listener) {
this.imageClickListener = listener;
}
public void setOnVoiceClickListener(OnVoiceClickListener listener) {
this.voiceClickListener = listener;
}
@Override
public int getItemViewType(int position) {
ChatMessage msg = getItem(position);
if (msg == null) return TYPE_TEXT_INCOMING;
// 优先使用 isOutgoing 字段,如果没有设置则回退到检查用户名
boolean isOutgoing = msg.isOutgoing() || "".equals(msg.getUsername());
ChatMessage.MessageType type = msg.getMessageType();
if (type == null) type = ChatMessage.MessageType.TEXT;
switch (type) {
case IMAGE:
return isOutgoing ? TYPE_IMAGE_OUTGOING : TYPE_IMAGE_INCOMING;
case BURN_IMAGE:
return isOutgoing ? TYPE_IMAGE_OUTGOING : TYPE_IMAGE_INCOMING;
case VOICE:
return isOutgoing ? TYPE_VOICE_OUTGOING : TYPE_VOICE_INCOMING;
case TEXT:
default:
return isOutgoing ? TYPE_TEXT_OUTGOING : TYPE_TEXT_INCOMING;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v;
switch (viewType) {
case TYPE_TEXT_OUTGOING:
v = inflater.inflate(R.layout.item_conversation_message_outgoing, parent, false);
return new OutgoingTextVH(v);
case TYPE_TEXT_INCOMING:
v = inflater.inflate(R.layout.item_conversation_message_incoming, parent, false);
return new IncomingTextVH(v);
case TYPE_IMAGE_OUTGOING:
v = inflater.inflate(R.layout.item_conversation_image_outgoing, parent, false);
return new OutgoingImageVH(v);
case TYPE_IMAGE_INCOMING:
v = inflater.inflate(R.layout.item_conversation_image_incoming, parent, false);
return new IncomingImageVH(v);
case TYPE_VOICE_OUTGOING:
v = inflater.inflate(R.layout.item_conversation_voice_outgoing, parent, false);
return new OutgoingVoiceVH(v);
case TYPE_VOICE_INCOMING:
v = inflater.inflate(R.layout.item_conversation_voice_incoming, parent, false);
return new IncomingVoiceVH(v);
default:
v = inflater.inflate(R.layout.item_conversation_message_incoming, parent, false);
return new IncomingTextVH(v);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ChatMessage msg = getItem(position);
if (holder instanceof IncomingTextVH) {
((IncomingTextVH) holder).bind(msg, longClickListener);
} else if (holder instanceof OutgoingTextVH) {
((OutgoingTextVH) holder).bind(msg, longClickListener);
} else if (holder instanceof IncomingImageVH) {
((IncomingImageVH) holder).bind(msg, longClickListener, imageClickListener);
} else if (holder instanceof OutgoingImageVH) {
((OutgoingImageVH) holder).bind(msg, longClickListener, imageClickListener);
} else if (holder instanceof IncomingVoiceVH) {
((IncomingVoiceVH) holder).bind(msg, longClickListener, voiceClickListener);
} else if (holder instanceof OutgoingVoiceVH) {
((OutgoingVoiceVH) holder).bind(msg, longClickListener, voiceClickListener);
}
}
// ========== 文本消息 ViewHolder ==========
static class IncomingTextVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final TextView nameText;
private final TextView msgText;
private final TextView timeText;
private final com.google.android.flexbox.FlexboxLayout reactionsContainer;
IncomingTextVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
nameText = itemView.findViewById(R.id.nameText);
msgText = itemView.findViewById(R.id.messageText);
timeText = itemView.findViewById(R.id.timeText);
reactionsContainer = itemView.findViewById(R.id.reactionsContainer);
setupAvatarOutline();
}
private void setupAvatarOutline() {
if (avatarView == null) return;
// 设置圆形裁剪,确保在模拟器上也能正常工作
// 使用post确保在View布局完成后再设置outline
avatarView.post(() -> {
if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) {
avatarView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
avatarView.setClipToOutline(true);
}
});
}
void bind(ChatMessage message, OnMessageLongClickListener listener) {
if (message == null) return;
nameText.setText(message.getUsername() != null ? message.getUsername() : "");
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
// 显示表情回应
displayReactions(message);
// 清除之前的监听器,避免重复绑定
itemView.setOnClickListener(null);
itemView.setOnLongClickListener(null);
// 设置长按监听
itemView.setOnLongClickListener(v -> {
if (listener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onMessageLongClick(message, position, v);
return true;
}
}
return false;
});
// 确保头像圆形裁剪设置正确
setupAvatarOutline();
// 加载对方头像(优先使用消息中的头像 URL如果没有则使用默认头像
loadAvatar(message);
}
private void displayReactions(ChatMessage message) {
if (reactionsContainer == null) return;
reactionsContainer.removeAllViews();
if (message.getReactions() == null || message.getReactions().isEmpty()) {
reactionsContainer.setVisibility(View.GONE);
return;
}
reactionsContainer.setVisibility(View.VISIBLE);
for (com.example.livestreaming.net.MessageReaction reaction : message.getReactions()) {
View reactionView = LayoutInflater.from(itemView.getContext())
.inflate(R.layout.item_message_reaction, reactionsContainer, false);
TextView emojiText = reactionView.findViewById(R.id.emojiText);
TextView countText = reactionView.findViewById(R.id.countText);
emojiText.setText(reaction.getEmoji());
countText.setText(String.valueOf(reaction.getCount()));
// 如果当前用户已回应,高亮显示
if (reaction.isReactedByMe()) {
reactionView.setBackgroundResource(R.drawable.reaction_background_selected);
} else {
reactionView.setBackgroundResource(R.drawable.reaction_background);
}
reactionsContainer.addView(reactionView);
}
}
private void loadAvatar(ChatMessage message) {
// TODO: 接入后端接口 - 加载消息发送者头像
// 接口路径: GET /api/users/{userId}/avatar 或直接从ChatMessage的avatarUrl字段获取
// ChatMessage对象应包含avatarUrl字段如果为空则根据userId调用接口获取
// 建议后端在返回消息列表时每条消息都包含发送者的avatarUrl避免额外请求
if (avatarView == null) return;
try {
// 优先使用消息中的头像 URL从后端获取
String avatarUrl = message != null ? message.getAvatarUrl() : null;
if (!TextUtils.isEmpty(avatarUrl)) {
Glide.with(avatarView)
.load(avatarUrl)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(avatarView);
return;
}
// 如果没有头像 URL暂时根据用户名生成不同的默认头像
// 这样不同发送者至少能通过不同的默认头像区分开
String username = message != null ? message.getUsername() : null;
int defaultAvatarRes = getDefaultAvatarForUsername(username);
Glide.with(avatarView)
.load(defaultAvatarRes)
.circleCrop()
.into(avatarView);
} catch (Exception e) {
// 如果加载失败,使用默认头像
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
/**
* 根据用户名生成一个稳定的默认头像资源
* 这样同一个用户名的消息会显示相同的默认头像
* TODO: 后续接入后端接口后,这个方法可以改为从后端获取头像 URL
*/
private int getDefaultAvatarForUsername(String username) {
if (TextUtils.isEmpty(username)) {
return R.drawable.ic_account_circle_24;
}
// 使用用户名的哈希值来选择不同的默认头像
// 这样不同的发送者至少能通过不同的默认头像区分开
int hash = Math.abs(username.hashCode());
int[] defaultAvatars = {
R.drawable.ic_account_circle_24,
// 如果有其他默认头像资源,可以在这里添加
// R.drawable.default_avatar_1,
// R.drawable.default_avatar_2,
};
return defaultAvatars[hash % defaultAvatars.length];
}
}
static class OutgoingTextVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final TextView msgText;
private final TextView timeText;
private final ImageView statusIcon;
private final com.google.android.flexbox.FlexboxLayout reactionsContainer;
OutgoingTextVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
msgText = itemView.findViewById(R.id.messageText);
timeText = itemView.findViewById(R.id.timeText);
statusIcon = itemView.findViewById(R.id.statusIcon);
reactionsContainer = itemView.findViewById(R.id.reactionsContainer);
setupAvatarOutline();
}
private void setupAvatarOutline() {
if (avatarView == null) return;
// 设置圆形裁剪,确保在模拟器上也能正常工作
// 使用post确保在View布局完成后再设置outline
avatarView.post(() -> {
if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) {
avatarView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
avatarView.setClipToOutline(true);
}
});
}
void bind(ChatMessage message, OnMessageLongClickListener listener) {
if (message == null) return;
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
// 显示表情回应
displayReactions(message);
// 显示消息状态图标(仅对发送的消息显示)
if (statusIcon != null && message.getStatus() != null) {
switch (message.getStatus()) {
case SENDING:
statusIcon.setImageResource(R.drawable.ic_clock_24);
statusIcon.setColorFilter(android.graphics.Color.GRAY, android.graphics.PorterDuff.Mode.SRC_IN);
statusIcon.setVisibility(View.VISIBLE);
break;
case SENT:
statusIcon.setImageResource(R.drawable.ic_check_24);
statusIcon.setColorFilter(android.graphics.Color.GRAY, android.graphics.PorterDuff.Mode.SRC_IN);
statusIcon.setVisibility(View.VISIBLE);
break;
case READ:
statusIcon.setImageResource(R.drawable.ic_check_double_24);
statusIcon.setColorFilter(android.graphics.Color.parseColor("#4CAF50"), android.graphics.PorterDuff.Mode.SRC_IN);
statusIcon.setVisibility(View.VISIBLE);
break;
default:
statusIcon.setVisibility(View.GONE);
break;
}
}
// 清除之前的监听器,避免重复绑定
itemView.setOnClickListener(null);
itemView.setOnLongClickListener(null);
// 设置长按监听
itemView.setOnLongClickListener(v -> {
if (listener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onMessageLongClick(message, position, v);
return true;
}
}
return false;
});
// 确保头像圆形裁剪设置正确
setupAvatarOutline();
// 加载用户头像
loadAvatar();
}
private void displayReactions(ChatMessage message) {
if (reactionsContainer == null) return;
reactionsContainer.removeAllViews();
if (message.getReactions() == null || message.getReactions().isEmpty()) {
reactionsContainer.setVisibility(View.GONE);
return;
}
reactionsContainer.setVisibility(View.VISIBLE);
for (com.example.livestreaming.net.MessageReaction reaction : message.getReactions()) {
View reactionView = LayoutInflater.from(itemView.getContext())
.inflate(R.layout.item_message_reaction, reactionsContainer, false);
TextView emojiText = reactionView.findViewById(R.id.emojiText);
TextView countText = reactionView.findViewById(R.id.countText);
emojiText.setText(reaction.getEmoji());
countText.setText(String.valueOf(reaction.getCount()));
// 如果当前用户已回应,高亮显示
if (reaction.isReactedByMe()) {
reactionView.setBackgroundResource(R.drawable.reaction_background_selected);
} else {
reactionView.setBackgroundResource(R.drawable.reaction_background);
}
reactionsContainer.addView(reactionView);
}
}
private void loadAvatar() {
if (avatarView == null) return;
try {
String avatarUri = avatarView.getContext()
.getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE)
.getString("profile_avatar_uri", null);
if (!TextUtils.isEmpty(avatarUri)) {
Uri uri = Uri.parse(avatarUri);
// 如果是 file:// 协议,尝试转换为 FileProvider URI
if ("file".equals(uri.getScheme())) {
try {
java.io.File file = new java.io.File(uri.getPath());
if (file.exists()) {
uri = FileProvider.getUriForFile(
avatarView.getContext(),
avatarView.getContext().getPackageName() + ".fileprovider",
file
);
}
} catch (Exception e) {
// 如果转换失败,使用原始 URI
}
}
Glide.with(avatarView)
.load(uri)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(avatarView);
return;
}
int avatarRes = avatarView.getContext()
.getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE)
.getInt("profile_avatar_res", 0);
if (avatarRes != 0) {
Glide.with(avatarView)
.load(avatarRes)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.into(avatarView);
} else {
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
} catch (Exception e) {
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
}
// ========== 图片消息 ViewHolder ==========
static class IncomingImageVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final TextView nameText;
private final ImageView messageImageView;
private final TextView timeText;
private final View imageContainer;
IncomingImageVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
nameText = itemView.findViewById(R.id.nameText);
messageImageView = itemView.findViewById(R.id.messageImageView);
timeText = itemView.findViewById(R.id.timeText);
imageContainer = itemView.findViewById(R.id.imageContainer);
setupAvatarOutline();
}
private void setupAvatarOutline() {
if (avatarView == null) return;
avatarView.post(() -> {
if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) {
avatarView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
avatarView.setClipToOutline(true);
}
});
}
void bind(ChatMessage message, OnMessageLongClickListener longClickListener, OnImageClickListener imageClickListener) {
if (message == null) return;
nameText.setText(message.getUsername() != null ? message.getUsername() : "");
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
// 加载图片
loadImage(message);
// 设置点击事件
if (imageContainer != null) {
imageContainer.setOnClickListener(v -> {
if (imageClickListener != null) {
imageClickListener.onImageClick(message, messageImageView);
}
});
}
// 设置长按监听
itemView.setOnLongClickListener(v -> {
if (longClickListener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
longClickListener.onMessageLongClick(message, position, v);
return true;
}
}
return false;
});
setupAvatarOutline();
loadAvatar(message);
}
private void loadImage(ChatMessage message) {
if (messageImageView == null) return;
// TODO: 接入后端接口 - 加载图片消息
// 优先使用 mediaUrl从后端获取的图片 URL
// 如果 mediaUrl 为空,使用 localMediaPath本地路径用于发送前预览
//
// 后端接口说明:
// 接口路径: GET /api/messages/{messageId}/media
// 或者直接使用 message.getMediaUrl() 返回的完整图片 URL
//
// 返回数据: 图片文件流或图片 URL
//
// 前端处理:
// 1. 如果 message.getMediaUrl() 不为空,直接加载该 URL
// 2. 如果为空但 message.getLocalMediaPath() 不为空,加载本地文件
// 3. 都为空则显示占位图
if (message.getMessageType() != null && message.getMessageType() == ChatMessage.MessageType.BURN_IMAGE) {
messageImageView.setImageResource(R.drawable.ic_visibility_24);
return;
}
String imageUrl = message.getMediaUrl();
String localPath = message.getLocalMediaPath();
if (!TextUtils.isEmpty(imageUrl)) {
// 从后端 URL 加载图片
Glide.with(messageImageView)
.load(imageUrl)
.placeholder(R.drawable.ic_image_24)
.error(R.drawable.ic_broken_image_24)
.into(messageImageView);
} else if (!TextUtils.isEmpty(localPath)) {
// 从本地路径加载图片(发送前预览)
Glide.with(messageImageView)
.load(localPath)
.placeholder(R.drawable.ic_image_24)
.error(R.drawable.ic_broken_image_24)
.into(messageImageView);
} else {
// 显示占位图
messageImageView.setImageResource(R.drawable.ic_image_24);
}
}
private void loadAvatar(ChatMessage message) {
if (avatarView == null) return;
try {
String avatarUrl = message != null ? message.getAvatarUrl() : null;
if (!TextUtils.isEmpty(avatarUrl)) {
Glide.with(avatarView)
.load(avatarUrl)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(avatarView);
return;
}
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
} catch (Exception e) {
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
}
static class OutgoingImageVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final ImageView messageImageView;
private final TextView timeText;
private final ImageView statusIcon;
private final View imageContainer;
OutgoingImageVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
messageImageView = itemView.findViewById(R.id.messageImageView);
timeText = itemView.findViewById(R.id.timeText);
statusIcon = itemView.findViewById(R.id.statusIcon);
imageContainer = itemView.findViewById(R.id.imageContainer);
setupAvatarOutline();
}
private void setupAvatarOutline() {
if (avatarView == null) return;
avatarView.post(() -> {
if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) {
avatarView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
avatarView.setClipToOutline(true);
}
});
}
void bind(ChatMessage message, OnMessageLongClickListener longClickListener, OnImageClickListener imageClickListener) {
if (message == null) return;
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
// 显示消息状态
updateStatus(message);
// 加载图片
loadImage(message);
// 设置点击事件
if (imageContainer != null) {
imageContainer.setOnClickListener(v -> {
if (imageClickListener != null) {
imageClickListener.onImageClick(message, messageImageView);
}
});
}
// 设置长按监听
itemView.setOnLongClickListener(v -> {
if (longClickListener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
longClickListener.onMessageLongClick(message, position, v);
return true;
}
}
return false;
});
setupAvatarOutline();
loadAvatar();
}
private void updateStatus(ChatMessage message) {
if (statusIcon != null && message.getStatus() != null) {
switch (message.getStatus()) {
case SENDING:
statusIcon.setImageResource(R.drawable.ic_clock_24);
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
statusIcon.setVisibility(View.VISIBLE);
break;
case SENT:
statusIcon.setImageResource(R.drawable.ic_check_24);
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
statusIcon.setVisibility(View.VISIBLE);
break;
case READ:
statusIcon.setImageResource(R.drawable.ic_check_double_24);
statusIcon.setColorFilter(0xFF4CAF50);
statusIcon.setVisibility(View.VISIBLE);
break;
default:
statusIcon.setVisibility(View.GONE);
break;
}
}
}
private void loadImage(ChatMessage message) {
if (messageImageView == null) return;
if (message.getMessageType() != null && message.getMessageType() == ChatMessage.MessageType.BURN_IMAGE) {
messageImageView.setImageResource(R.drawable.ic_visibility_24);
return;
}
String imageUrl = message.getMediaUrl();
String localPath = message.getLocalMediaPath();
if (!TextUtils.isEmpty(imageUrl)) {
Glide.with(messageImageView)
.load(imageUrl)
.placeholder(R.drawable.ic_image_24)
.error(R.drawable.ic_broken_image_24)
.into(messageImageView);
} else if (!TextUtils.isEmpty(localPath)) {
Glide.with(messageImageView)
.load(localPath)
.placeholder(R.drawable.ic_image_24)
.error(R.drawable.ic_broken_image_24)
.into(messageImageView);
} else {
messageImageView.setImageResource(R.drawable.ic_image_24);
}
}
private void loadAvatar() {
if (avatarView == null) return;
try {
String avatarUri = avatarView.getContext()
.getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE)
.getString("profile_avatar_uri", null);
if (!TextUtils.isEmpty(avatarUri)) {
Uri uri = Uri.parse(avatarUri);
if ("file".equals(uri.getScheme())) {
try {
java.io.File file = new java.io.File(uri.getPath());
if (file.exists()) {
uri = FileProvider.getUriForFile(
avatarView.getContext(),
avatarView.getContext().getPackageName() + ".fileprovider",
file
);
}
} catch (Exception e) {
// 使用原始 URI
}
}
Glide.with(avatarView)
.load(uri)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(avatarView);
return;
}
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
} catch (Exception e) {
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
}
// ========== 语音消息 ViewHolder ==========
static class IncomingVoiceVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final TextView nameText;
private final View voiceContainer;
private final ImageView voiceIcon;
private final TextView durationText;
private final TextView timeText;
IncomingVoiceVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
nameText = itemView.findViewById(R.id.nameText);
voiceContainer = itemView.findViewById(R.id.voiceContainer);
voiceIcon = itemView.findViewById(R.id.voiceIcon);
durationText = itemView.findViewById(R.id.durationText);
timeText = itemView.findViewById(R.id.timeText);
setupAvatarOutline();
}
private void setupAvatarOutline() {
if (avatarView == null) return;
avatarView.post(() -> {
if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) {
avatarView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
avatarView.setClipToOutline(true);
}
});
}
void bind(ChatMessage message, OnMessageLongClickListener longClickListener, OnVoiceClickListener voiceClickListener) {
if (message == null) return;
nameText.setText(message.getUsername() != null ? message.getUsername() : "");
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
// 显示语音时长
int duration = message.getVoiceDuration();
durationText.setText(duration + "\"");
// 设置点击事件
if (voiceContainer != null) {
voiceContainer.setOnClickListener(v -> {
if (voiceClickListener != null) {
voiceClickListener.onVoiceClick(message);
}
});
}
// 设置长按监听
itemView.setOnLongClickListener(v -> {
if (longClickListener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
longClickListener.onMessageLongClick(message, position, v);
return true;
}
}
return false;
});
setupAvatarOutline();
loadAvatar(message);
}
private void loadAvatar(ChatMessage message) {
if (avatarView == null) return;
try {
String avatarUrl = message != null ? message.getAvatarUrl() : null;
if (!TextUtils.isEmpty(avatarUrl)) {
Glide.with(avatarView)
.load(avatarUrl)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(avatarView);
return;
}
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
} catch (Exception e) {
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
}
static class OutgoingVoiceVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final View voiceContainer;
private final ImageView voiceIcon;
private final TextView durationText;
private final TextView timeText;
private final ImageView statusIcon;
OutgoingVoiceVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
voiceContainer = itemView.findViewById(R.id.voiceContainer);
voiceIcon = itemView.findViewById(R.id.voiceIcon);
durationText = itemView.findViewById(R.id.durationText);
timeText = itemView.findViewById(R.id.timeText);
statusIcon = itemView.findViewById(R.id.statusIcon);
setupAvatarOutline();
}
private void setupAvatarOutline() {
if (avatarView == null) return;
avatarView.post(() -> {
if (avatarView.getWidth() > 0 && avatarView.getHeight() > 0) {
avatarView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
avatarView.setClipToOutline(true);
}
});
}
void bind(ChatMessage message, OnMessageLongClickListener longClickListener, OnVoiceClickListener voiceClickListener) {
if (message == null) return;
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
// 显示消息状态
updateStatus(message);
// 显示语音时长
int duration = message.getVoiceDuration();
durationText.setText(duration + "\"");
// 设置点击事件
if (voiceContainer != null) {
voiceContainer.setOnClickListener(v -> {
if (voiceClickListener != null) {
voiceClickListener.onVoiceClick(message);
}
});
}
// 设置长按监听
itemView.setOnLongClickListener(v -> {
if (longClickListener != null) {
int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
longClickListener.onMessageLongClick(message, position, v);
return true;
}
}
return false;
});
setupAvatarOutline();
loadAvatar();
}
private void updateStatus(ChatMessage message) {
if (statusIcon != null && message.getStatus() != null) {
switch (message.getStatus()) {
case SENDING:
statusIcon.setImageResource(R.drawable.ic_clock_24);
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
statusIcon.setVisibility(View.VISIBLE);
break;
case SENT:
statusIcon.setImageResource(R.drawable.ic_check_24);
statusIcon.setColorFilter(itemView.getContext().getColor(android.R.color.darker_gray));
statusIcon.setVisibility(View.VISIBLE);
break;
case READ:
statusIcon.setImageResource(R.drawable.ic_check_double_24);
statusIcon.setColorFilter(0xFF4CAF50);
statusIcon.setVisibility(View.VISIBLE);
break;
default:
statusIcon.setVisibility(View.GONE);
break;
}
}
}
private void loadAvatar() {
if (avatarView == null) return;
try {
String avatarUri = avatarView.getContext()
.getSharedPreferences("profile_prefs", android.content.Context.MODE_PRIVATE)
.getString("profile_avatar_uri", null);
if (!TextUtils.isEmpty(avatarUri)) {
Uri uri = Uri.parse(avatarUri);
if ("file".equals(uri.getScheme())) {
try {
java.io.File file = new java.io.File(uri.getPath());
if (file.exists()) {
uri = FileProvider.getUriForFile(
avatarView.getContext(),
avatarView.getContext().getPackageName() + ".fileprovider",
file
);
}
} catch (Exception e) {
// 使用原始 URI
}
}
Glide.with(avatarView)
.load(uri)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
.placeholder(R.drawable.ic_account_circle_24)
.into(avatarView);
return;
}
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
} catch (Exception e) {
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
}
private static final DiffUtil.ItemCallback<ChatMessage> DIFF = new DiffUtil.ItemCallback<ChatMessage>() {
@Override
public boolean areItemsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
// 使用messageId作为唯一标识
return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId());
}
@Override
public boolean areContentsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
// 比较消息内容、状态等是否相同
return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId()) &&
oldItem.getStatus() == newItem.getStatus() &&
oldItem.getMessage() != null && oldItem.getMessage().equals(newItem.getMessage());
}
};
}