2025-12-19 15:11:49 +08:00
|
|
|
|
package com.example.livestreaming;
|
|
|
|
|
|
|
2025-12-22 16:31:46 +08:00
|
|
|
|
import android.graphics.Outline;
|
|
|
|
|
|
import android.net.Uri;
|
|
|
|
|
|
import android.text.TextUtils;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import android.view.LayoutInflater;
|
|
|
|
|
|
import android.view.View;
|
|
|
|
|
|
import android.view.ViewGroup;
|
2025-12-22 16:31:46 +08:00
|
|
|
|
import android.view.ViewOutlineProvider;
|
|
|
|
|
|
import android.widget.ImageView;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import androidx.core.content.FileProvider;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import androidx.recyclerview.widget.DiffUtil;
|
|
|
|
|
|
import androidx.recyclerview.widget.ListAdapter;
|
|
|
|
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
|
|
|
|
|
2025-12-22 16:31:46 +08:00
|
|
|
|
import com.bumptech.glide.Glide;
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import java.text.SimpleDateFormat;
|
|
|
|
|
|
import java.util.Date;
|
|
|
|
|
|
import java.util.Locale;
|
|
|
|
|
|
|
|
|
|
|
|
public class ConversationMessagesAdapter extends ListAdapter<ChatMessage, RecyclerView.ViewHolder> {
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
public interface OnMessageLongClickListener {
|
|
|
|
|
|
void onMessageLongClick(ChatMessage message, int position, View view);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
public interface OnImageClickListener {
|
|
|
|
|
|
void onImageClick(ChatMessage message, ImageView imageView);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public interface OnVoiceClickListener {
|
|
|
|
|
|
void onVoiceClick(ChatMessage message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
private OnMessageLongClickListener longClickListener;
|
2025-12-24 17:43:14 +08:00
|
|
|
|
private OnImageClickListener imageClickListener;
|
|
|
|
|
|
private OnVoiceClickListener voiceClickListener;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
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;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm", Locale.getDefault());
|
|
|
|
|
|
|
|
|
|
|
|
public ConversationMessagesAdapter() {
|
|
|
|
|
|
super(DIFF);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
public void setOnMessageLongClickListener(OnMessageLongClickListener listener) {
|
|
|
|
|
|
this.longClickListener = listener;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
public void setOnImageClickListener(OnImageClickListener listener) {
|
|
|
|
|
|
this.imageClickListener = listener;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void setOnVoiceClickListener(OnVoiceClickListener listener) {
|
|
|
|
|
|
this.voiceClickListener = listener;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
@Override
|
|
|
|
|
|
public int getItemViewType(int position) {
|
|
|
|
|
|
ChatMessage msg = getItem(position);
|
2025-12-24 17:43:14 +08:00
|
|
|
|
if (msg == null) return TYPE_TEXT_INCOMING;
|
|
|
|
|
|
|
|
|
|
|
|
boolean 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 VOICE:
|
|
|
|
|
|
return isOutgoing ? TYPE_VOICE_OUTGOING : TYPE_VOICE_INCOMING;
|
|
|
|
|
|
case TEXT:
|
|
|
|
|
|
default:
|
|
|
|
|
|
return isOutgoing ? TYPE_TEXT_OUTGOING : TYPE_TEXT_INCOMING;
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@NonNull
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
|
|
|
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
2025-12-24 17:43:14 +08:00
|
|
|
|
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);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
|
|
|
|
|
ChatMessage msg = getItem(position);
|
2025-12-24 17:43:14 +08:00
|
|
|
|
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);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
// ========== 文本消息 ViewHolder ==========
|
|
|
|
|
|
|
|
|
|
|
|
static class IncomingTextVH extends RecyclerView.ViewHolder {
|
2025-12-22 16:31:46 +08:00
|
|
|
|
private final ImageView avatarView;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
private final TextView nameText;
|
|
|
|
|
|
private final TextView msgText;
|
|
|
|
|
|
private final TextView timeText;
|
|
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
IncomingTextVH(@NonNull View itemView) {
|
2025-12-19 15:11:49 +08:00
|
|
|
|
super(itemView);
|
2025-12-22 16:31:46 +08:00
|
|
|
|
avatarView = itemView.findViewById(R.id.avatarView);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
nameText = itemView.findViewById(R.id.nameText);
|
|
|
|
|
|
msgText = itemView.findViewById(R.id.messageText);
|
|
|
|
|
|
timeText = itemView.findViewById(R.id.timeText);
|
2025-12-22 16:31:46 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
void bind(ChatMessage message, OnMessageLongClickListener listener) {
|
2025-12-19 15:11:49 +08:00
|
|
|
|
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())));
|
2025-12-22 16:31:46 +08:00
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 清除之前的监听器,避免重复绑定
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-22 16:31:46 +08:00
|
|
|
|
// 确保头像圆形裁剪设置正确
|
|
|
|
|
|
setupAvatarOutline();
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 加载对方头像(优先使用消息中的头像 URL,如果没有则使用默认头像)
|
|
|
|
|
|
loadAvatar(message);
|
2025-12-22 16:31:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
private void loadAvatar(ChatMessage message) {
|
|
|
|
|
|
// TODO: 接入后端接口 - 加载消息发送者头像
|
|
|
|
|
|
// 接口路径: GET /api/users/{userId}/avatar 或直接从ChatMessage的avatarUrl字段获取
|
|
|
|
|
|
// ChatMessage对象应包含avatarUrl字段,如果为空,则根据userId调用接口获取
|
|
|
|
|
|
// 建议后端在返回消息列表时,每条消息都包含发送者的avatarUrl,避免额外请求
|
2025-12-22 16:31:46 +08:00
|
|
|
|
if (avatarView == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 优先使用消息中的头像 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);
|
2025-12-22 16:31:46 +08:00
|
|
|
|
Glide.with(avatarView)
|
2025-12-23 15:37:37 +08:00
|
|
|
|
.load(defaultAvatarRes)
|
2025-12-22 16:31:46 +08:00
|
|
|
|
.circleCrop()
|
|
|
|
|
|
.into(avatarView);
|
|
|
|
|
|
} catch (Exception e) {
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 如果加载失败,使用默认头像
|
2025-12-22 16:31:46 +08:00
|
|
|
|
Glide.with(avatarView)
|
|
|
|
|
|
.load(R.drawable.ic_account_circle_24)
|
|
|
|
|
|
.circleCrop()
|
|
|
|
|
|
.into(avatarView);
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据用户名生成一个稳定的默认头像资源
|
|
|
|
|
|
* 这样同一个用户名的消息会显示相同的默认头像
|
|
|
|
|
|
* 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];
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
static class OutgoingTextVH extends RecyclerView.ViewHolder {
|
2025-12-22 16:31:46 +08:00
|
|
|
|
private final ImageView avatarView;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
private final TextView msgText;
|
|
|
|
|
|
private final TextView timeText;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
private final ImageView statusIcon;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
OutgoingTextVH(@NonNull View itemView) {
|
2025-12-19 15:11:49 +08:00
|
|
|
|
super(itemView);
|
2025-12-22 16:31:46 +08:00
|
|
|
|
avatarView = itemView.findViewById(R.id.avatarView);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
msgText = itemView.findViewById(R.id.messageText);
|
|
|
|
|
|
timeText = itemView.findViewById(R.id.timeText);
|
2025-12-23 15:37:37 +08:00
|
|
|
|
statusIcon = itemView.findViewById(R.id.statusIcon);
|
2025-12-22 16:31:46 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
void bind(ChatMessage message, OnMessageLongClickListener listener) {
|
2025-12-19 15:11:49 +08:00
|
|
|
|
if (message == null) return;
|
|
|
|
|
|
msgText.setText(message.getMessage() != null ? message.getMessage() : "");
|
|
|
|
|
|
timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
|
2025-12-22 16:31:46 +08:00
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 显示消息状态图标(仅对发送的消息显示)
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的监听器,避免重复绑定
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-22 16:31:46 +08:00
|
|
|
|
// 确保头像圆形裁剪设置正确
|
|
|
|
|
|
setupAvatarOutline();
|
|
|
|
|
|
// 加载用户头像
|
|
|
|
|
|
loadAvatar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)) {
|
2025-12-23 15:37:37 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-22 16:31:46 +08:00
|
|
|
|
Glide.with(avatarView)
|
2025-12-23 15:37:37 +08:00
|
|
|
|
.load(uri)
|
2025-12-22 16:31:46 +08:00
|
|
|
|
.circleCrop()
|
|
|
|
|
|
.error(R.drawable.ic_account_circle_24)
|
2025-12-23 15:37:37 +08:00
|
|
|
|
.placeholder(R.drawable.ic_account_circle_24)
|
2025-12-22 16:31:46 +08:00
|
|
|
|
.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)
|
2025-12-24 17:43:14 +08:00
|
|
|
|
.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. 都为空则显示占位图
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2025-12-22 16:31:46 +08:00
|
|
|
|
.circleCrop()
|
|
|
|
|
|
.into(avatarView);
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static final DiffUtil.ItemCallback<ChatMessage> DIFF = new DiffUtil.ItemCallback<ChatMessage>() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean areItemsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 使用messageId作为唯一标识
|
|
|
|
|
|
return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId());
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean areContentsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// 比较消息内容、状态等是否相同
|
|
|
|
|
|
return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId()) &&
|
|
|
|
|
|
oldItem.getStatus() == newItem.getStatus() &&
|
|
|
|
|
|
oldItem.getMessage() != null && oldItem.getMessage().equals(newItem.getMessage());
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|