diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index e11542d1..d77b2ef8 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -80,6 +80,10 @@
android:name="com.example.livestreaming.MessagesActivity"
android:exported="false" />
+
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
new file mode 100644
index 00000000..09d5a707
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
@@ -0,0 +1,97 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.example.livestreaming.databinding.ActivityConversationBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ConversationActivity extends AppCompatActivity {
+
+ private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id";
+ private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title";
+
+ private ActivityConversationBinding binding;
+
+ private ConversationMessagesAdapter adapter;
+ private final List messages = new ArrayList<>();
+
+ public static void start(Context context, String conversationId, String title) {
+ Intent intent = new Intent(context, ConversationActivity.class);
+ intent.putExtra(EXTRA_CONVERSATION_ID, conversationId);
+ intent.putExtra(EXTRA_CONVERSATION_TITLE, title);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityConversationBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null;
+ binding.titleText.setText(title != null ? title : "会话");
+
+ binding.backButton.setOnClickListener(v -> finish());
+
+ setupMessages();
+ setupInput();
+ }
+
+ private void setupMessages() {
+ adapter = new ConversationMessagesAdapter();
+
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ layoutManager.setStackFromEnd(false);
+ binding.messagesRecyclerView.setLayoutManager(layoutManager);
+ binding.messagesRecyclerView.setAdapter(adapter);
+
+ messages.clear();
+ String title = binding.titleText.getText() != null ? binding.titleText.getText().toString() : "";
+ messages.add(new ChatMessage(title, "你好~"));
+ messages.add(new ChatMessage("我", "在的,有什么需要帮忙?"));
+ adapter.submitList(new ArrayList<>(messages));
+ scrollToBottom();
+ }
+
+ private void setupInput() {
+ binding.sendButton.setOnClickListener(v -> sendMessage());
+
+ binding.messageInput.setOnEditorActionListener((v, actionId, event) -> {
+ if (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
+ sendMessage();
+ return true;
+ }
+ return false;
+ });
+
+ binding.messageInput.setOnFocusChangeListener((v, hasFocus) -> {
+ if (hasFocus) scrollToBottom();
+ });
+ }
+
+ private void sendMessage() {
+ String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
+ if (TextUtils.isEmpty(text)) return;
+
+ messages.add(new ChatMessage("我", text));
+ adapter.submitList(new ArrayList<>(messages));
+ binding.messageInput.setText("");
+ scrollToBottom();
+ }
+
+ private void scrollToBottom() {
+ if (messages.isEmpty()) return;
+ binding.messagesRecyclerView.post(() -> binding.messagesRecyclerView.scrollToPosition(messages.size() - 1));
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java
new file mode 100644
index 00000000..6593f8d4
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java
@@ -0,0 +1,106 @@
+package com.example.livestreaming;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class ConversationMessagesAdapter extends ListAdapter {
+
+ private static final int TYPE_INCOMING = 1;
+ private static final int TYPE_OUTGOING = 2;
+
+ private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm", Locale.getDefault());
+
+ public ConversationMessagesAdapter() {
+ super(DIFF);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ ChatMessage msg = getItem(position);
+ if (msg == null) return TYPE_INCOMING;
+ String u = msg.getUsername();
+ return "我".equals(u) ? TYPE_OUTGOING : TYPE_INCOMING;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ if (viewType == TYPE_OUTGOING) {
+ View v = inflater.inflate(R.layout.item_conversation_message_outgoing, parent, false);
+ return new OutgoingVH(v);
+ }
+ View v = inflater.inflate(R.layout.item_conversation_message_incoming, parent, false);
+ return new IncomingVH(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ ChatMessage msg = getItem(position);
+ if (holder instanceof IncomingVH) {
+ ((IncomingVH) holder).bind(msg);
+ } else if (holder instanceof OutgoingVH) {
+ ((OutgoingVH) holder).bind(msg);
+ }
+ }
+
+ static class IncomingVH extends RecyclerView.ViewHolder {
+ private final TextView nameText;
+ private final TextView msgText;
+ private final TextView timeText;
+
+ IncomingVH(@NonNull View itemView) {
+ super(itemView);
+ nameText = itemView.findViewById(R.id.nameText);
+ msgText = itemView.findViewById(R.id.messageText);
+ timeText = itemView.findViewById(R.id.timeText);
+ }
+
+ void bind(ChatMessage message) {
+ 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())));
+ }
+ }
+
+ static class OutgoingVH extends RecyclerView.ViewHolder {
+ private final TextView msgText;
+ private final TextView timeText;
+
+ OutgoingVH(@NonNull View itemView) {
+ super(itemView);
+ msgText = itemView.findViewById(R.id.messageText);
+ timeText = itemView.findViewById(R.id.timeText);
+ }
+
+ void bind(ChatMessage message) {
+ if (message == null) return;
+ msgText.setText(message.getMessage() != null ? message.getMessage() : "");
+ timeText.setText(TIME_FORMAT.format(new Date(message.getTimestamp())));
+ }
+ }
+
+ private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
+ return oldItem.getTimestamp() == newItem.getTimestamp();
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
+ return oldItem.getTimestamp() == newItem.getTimestamp();
+ }
+ };
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
index 119bb5de..346b4248 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
@@ -20,6 +20,9 @@ import com.example.livestreaming.databinding.ActivityEditProfileBinding;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
public class EditProfileActivity extends AppCompatActivity {
@@ -29,6 +32,7 @@ public class EditProfileActivity extends AppCompatActivity {
private static final String KEY_NAME = "profile_name";
private static final String KEY_BIO = "profile_bio";
private static final String KEY_AVATAR_URI = "profile_avatar_uri";
+ private static final String KEY_AVATAR_RES = "profile_avatar_res";
private static final String KEY_BIRTHDAY = "profile_birthday";
private static final String KEY_GENDER = "profile_gender";
private static final String KEY_LOCATION = "profile_location";
@@ -107,7 +111,14 @@ public class EditProfileActivity extends AppCompatActivity {
}
if (selectedAvatarUri != null) {
- getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_AVATAR_URI, selectedAvatarUri.toString()).apply();
+ Uri persisted = persistAvatarToInternalStorage(selectedAvatarUri);
+ if (persisted != null) {
+ getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
+ .edit()
+ .putString(KEY_AVATAR_URI, persisted.toString())
+ .remove(KEY_AVATAR_RES)
+ .apply();
+ }
}
if (TextUtils.isEmpty(birthday)) {
@@ -132,6 +143,42 @@ public class EditProfileActivity extends AppCompatActivity {
});
}
+ private Uri persistAvatarToInternalStorage(Uri sourceUri) {
+ if (sourceUri == null) return null;
+ InputStream in = null;
+ OutputStream out = null;
+ try {
+ File dir = new File(getFilesDir(), "avatars");
+ if (!dir.exists()) dir.mkdirs();
+ File file = new File(dir, "avatar.jpg");
+
+ in = getContentResolver().openInputStream(sourceUri);
+ if (in == null) return null;
+ out = new FileOutputStream(file, false);
+
+ byte[] buf = new byte[8 * 1024];
+ int n;
+ while ((n = in.read(buf)) > 0) {
+ out.write(buf, 0, n);
+ }
+ out.flush();
+
+ return Uri.fromFile(file);
+ } catch (Exception e) {
+ Toast.makeText(this, "头像保存失败", Toast.LENGTH_SHORT).show();
+ return null;
+ } finally {
+ try {
+ if (in != null) in.close();
+ } catch (Exception ignored) {
+ }
+ try {
+ if (out != null) out.close();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
private void loadFromPrefs() {
String name = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, "");
String bio = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIO, "");
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java
index 04e12fc2..8bd453de 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java
@@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintSet;
import com.example.livestreaming.databinding.ActivityFishPondBinding;
+import com.bumptech.glide.Glide;
import com.google.android.material.bottomnavigation.BottomNavigationView;
public class FishPondActivity extends AppCompatActivity {
@@ -265,7 +266,10 @@ public class FishPondActivity extends AppCompatActivity {
String bio = "真诚交友 · " + c + " · 在线";
- avatarView.setImageResource(a);
+ Glide.with(avatarView)
+ .load(a)
+ .circleCrop()
+ .into(avatarView);
nameView.setText(n);
locationView.setText(c);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
index 38811e8d..c1aa0dec 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
@@ -202,14 +202,22 @@ public class MainActivity extends AppCompatActivity {
String avatarUri = getSharedPreferences("profile_prefs", MODE_PRIVATE)
.getString("profile_avatar_uri", null);
if (!TextUtils.isEmpty(avatarUri)) {
- Glide.with(this).load(avatarUri).circleCrop().into(binding.avatarButton);
+ Glide.with(this)
+ .load(Uri.parse(avatarUri))
+ .circleCrop()
+ .error(R.drawable.ic_account_circle_24)
+ .into(binding.avatarButton);
return;
}
int avatarRes = getSharedPreferences("profile_prefs", MODE_PRIVATE)
.getInt("profile_avatar_res", 0);
if (avatarRes != 0) {
- binding.avatarButton.setImageResource(avatarRes);
+ Glide.with(this)
+ .load(avatarRes)
+ .circleCrop()
+ .error(R.drawable.ic_account_circle_24)
+ .into(binding.avatarButton);
} else {
binding.avatarButton.setImageResource(R.drawable.ic_account_circle_24);
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java
index 4afb12b5..206ce2c8 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java
@@ -2,11 +2,21 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import com.example.livestreaming.databinding.ActivityMessagesBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
@@ -18,6 +28,22 @@ public class MessagesActivity extends AppCompatActivity {
private ActivityMessagesBinding binding;
+ private final List conversations = new ArrayList<>();
+ private ConversationsAdapter conversationsAdapter;
+
+ private int swipedPosition = RecyclerView.NO_POSITION;
+ private RectF deleteButtonRect;
+ private final Paint deleteBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint deleteTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private int lastSwipePosition = RecyclerView.NO_POSITION;
+ private float lastSwipeDx;
+
+ private float touchDownX;
+ private float touchDownY;
+ private boolean touchIsClick;
+ private int touchSlop;
+
public static void start(Context context) {
Intent intent = new Intent(context, MessagesActivity.class);
context.startActivity(intent);
@@ -63,13 +89,203 @@ public class MessagesActivity extends AppCompatActivity {
}
private void setupConversationList() {
- ConversationsAdapter adapter = new ConversationsAdapter(item -> {
+ deleteBackgroundPaint.setColor(0xFFE53935);
+ deleteTextPaint.setColor(Color.WHITE);
+ deleteTextPaint.setTextAlign(Paint.Align.CENTER);
+ deleteTextPaint.setTextSize(sp(14));
+
+ conversationsAdapter = new ConversationsAdapter(item -> {
if (item == null) return;
- Toast.makeText(this, "打开会话:" + item.getTitle(), Toast.LENGTH_SHORT).show();
+ ConversationActivity.start(this, item.getId(), item.getTitle());
});
binding.conversationsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
- binding.conversationsRecyclerView.setAdapter(adapter);
- adapter.submitList(buildDemoConversations());
+ binding.conversationsRecyclerView.setAdapter(conversationsAdapter);
+
+ conversations.clear();
+ conversations.addAll(buildDemoConversations());
+ conversationsAdapter.submitList(new ArrayList<>(conversations));
+
+ attachSwipeToDelete(binding.conversationsRecyclerView);
+ }
+
+ private void attachSwipeToDelete(RecyclerView recyclerView) {
+ final float actionWidth = dp(96);
+ touchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+ ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
+ return false;
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+ // 不允许滑动直接删除(保持为“露出删除按钮”的交互)
+ if (conversationsAdapter != null) {
+ conversationsAdapter.notifyItemChanged(viewHolder.getBindingAdapterPosition());
+ }
+ }
+
+ @Override
+ public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+
+ int pos = viewHolder.getBindingAdapterPosition();
+ if (pos == RecyclerView.NO_POSITION) return;
+
+ float dx = (pos == lastSwipePosition) ? lastSwipeDx : viewHolder.itemView.getTranslationX();
+ boolean shouldOpen = dx <= -actionWidth * 0.2f;
+
+ if (shouldOpen) {
+ if (swipedPosition != RecyclerView.NO_POSITION && swipedPosition != pos && conversationsAdapter != null) {
+ int old = swipedPosition;
+ swipedPosition = RecyclerView.NO_POSITION;
+ deleteButtonRect = null;
+ conversationsAdapter.notifyItemChanged(old);
+ }
+ swipedPosition = pos;
+ viewHolder.itemView.setTranslationX(-actionWidth);
+ } else {
+ if (swipedPosition == pos) {
+ swipedPosition = RecyclerView.NO_POSITION;
+ deleteButtonRect = null;
+ }
+ viewHolder.itemView.setTranslationX(0f);
+ }
+
+ if (pos == lastSwipePosition) {
+ lastSwipePosition = RecyclerView.NO_POSITION;
+ lastSwipeDx = 0f;
+ }
+ }
+
+ @Override
+ public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
+ // 返回 1f 防止触发“滑动就删除”的默认行为
+ return 1f;
+ }
+
+ @Override
+ public float getSwipeEscapeVelocity(float defaultValue) {
+ return defaultValue * 3f;
+ }
+
+ @Override
+ public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
+ float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) {
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+ return;
+ }
+
+ int position = viewHolder.getBindingAdapterPosition();
+ if (position == RecyclerView.NO_POSITION) {
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+ return;
+ }
+
+ float clampedDx;
+ if (position == swipedPosition) {
+ if (!isCurrentlyActive) {
+ clampedDx = -actionWidth;
+ } else {
+ if (dX > 0) {
+ // 已展开时右滑:从 -actionWidth 平滑回到 0
+ clampedDx = Math.min(0f, -actionWidth + dX);
+ } else {
+ // 正常左滑打开
+ clampedDx = Math.min(0, Math.max(dX, -actionWidth));
+ }
+ }
+ } else {
+ // 未展开的 item:只允许左滑露出
+ clampedDx = Math.min(0, Math.max(dX, -actionWidth));
+ }
+
+ if (isCurrentlyActive) {
+ lastSwipePosition = position;
+ lastSwipeDx = clampedDx;
+ }
+
+ View itemView = viewHolder.itemView;
+ float left;
+ float top = itemView.getTop();
+ float bottom = itemView.getBottom();
+ float right = itemView.getRight();
+ left = right + clampedDx;
+
+ deleteButtonRect = new RectF(left, top, right, bottom);
+ c.drawRect(deleteButtonRect, deleteBackgroundPaint);
+
+ float centerX = deleteButtonRect.centerX();
+ float centerY = deleteButtonRect.centerY();
+ Paint.FontMetrics fm = deleteTextPaint.getFontMetrics();
+ float textY = centerY - (fm.ascent + fm.descent) / 2f;
+ c.drawText("删除", centerX, textY, deleteTextPaint);
+
+ super.onChildDraw(c, recyclerView, viewHolder, clampedDx, dY, actionState, isCurrentlyActive);
+ }
+ };
+
+ new ItemTouchHelper(callback).attachToRecyclerView(recyclerView);
+
+ recyclerView.setOnTouchListener((v, event) -> {
+ if (swipedPosition == RecyclerView.NO_POSITION || deleteButtonRect == null) return false;
+
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ touchDownX = event.getX();
+ touchDownY = event.getY();
+ touchIsClick = true;
+ return false;
+ }
+
+ if (action == MotionEvent.ACTION_MOVE) {
+ float dx = Math.abs(event.getX() - touchDownX);
+ float dy = Math.abs(event.getY() - touchDownY);
+ if (dx > touchSlop || dy > touchSlop) {
+ touchIsClick = false;
+ }
+ return false;
+ }
+
+ if (action == MotionEvent.ACTION_UP) {
+ if (!touchIsClick) return false;
+
+ boolean hit = deleteButtonRect.contains(event.getX(), event.getY());
+ int pos = swipedPosition;
+ recoverSwipedItem();
+ if (hit) {
+ deleteConversationAt(pos);
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+
+ private void recoverSwipedItem() {
+ int pos = swipedPosition;
+ swipedPosition = RecyclerView.NO_POSITION;
+ deleteButtonRect = null;
+ if (pos != RecyclerView.NO_POSITION && conversationsAdapter != null) {
+ conversationsAdapter.notifyItemChanged(pos);
+ }
+ }
+
+ private void deleteConversationAt(int position) {
+ if (position < 0 || position >= conversations.size()) return;
+ conversations.remove(position);
+ if (conversationsAdapter != null) {
+ conversationsAdapter.submitList(new ArrayList<>(conversations));
+ }
+ }
+
+ private float dp(float value) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, getResources().getDisplayMetrics());
+ }
+
+ private float sp(float value) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, getResources().getDisplayMetrics());
}
private List buildDemoConversations() {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
index 5e958a1d..7ae09465 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.os.Bundle;
+import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
@@ -94,11 +95,21 @@ public class ProfileActivity extends AppCompatActivity {
String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null);
if (!TextUtils.isEmpty(avatarUri)) {
- Glide.with(this).load(avatarUri).circleCrop().into(binding.avatar);
+ Glide.with(this)
+ .load(Uri.parse(avatarUri))
+ .circleCrop()
+ .error(R.drawable.ic_account_circle_24)
+ .into(binding.avatar);
} else {
int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0);
if (avatarRes != 0) {
- binding.avatar.setImageResource(avatarRes);
+ Glide.with(this)
+ .load(avatarRes)
+ .circleCrop()
+ .error(R.drawable.ic_account_circle_24)
+ .into(binding.avatar);
+ } else {
+ binding.avatar.setImageResource(R.drawable.ic_account_circle_24);
}
}
@@ -157,7 +168,20 @@ public class ProfileActivity extends AppCompatActivity {
binding.action3.setOnClickListener(v -> startActivity(new Intent(this, MyFriendsActivity.class)));
binding.editProfile.setOnClickListener(v -> EditProfileActivity.start(this));
- binding.shareHome.setOnClickListener(v -> TabPlaceholderActivity.start(this, "分享主页"));
+ binding.shareHome.setOnClickListener(v -> {
+ // TabPlaceholderActivity.start(this, "分享主页");
+ String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
+ String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
+ if (TextUtils.isEmpty(digits)) digits = "24187196";
+
+ String url = "https://live.example.com/u/" + digits;
+
+ ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ if (cm != null) {
+ cm.setPrimaryClip(ClipData.newPlainText("profile_url", url));
+ Toast.makeText(this, "主页链接已复制", Toast.LENGTH_SHORT).show();
+ }
+ });
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
index 3b8d923e..7457f197 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
@@ -2,6 +2,7 @@ package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
@@ -361,7 +362,11 @@ public class TabPlaceholderActivity extends AppCompatActivity {
}
if (!TextUtils.isEmpty(avatarUri)) {
- Glide.with(this).load(avatarUri).circleCrop().into(binding.moreAvatar);
+ Glide.with(this)
+ .load(Uri.parse(avatarUri))
+ .circleCrop()
+ .error(R.drawable.ic_account_circle_24)
+ .into(binding.moreAvatar);
} else if (avatarRes != 0) {
binding.moreAvatar.setImageResource(avatarRes);
} else {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
index c728f773..10dcd6d9 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
@@ -1,138 +1 @@
-package com.example.livestreaming;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import com.example.livestreaming.databinding.ActivityWishTreeBinding;
-import com.google.android.material.bottomnavigation.BottomNavigationView;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-public class WishTreeActivity extends AppCompatActivity {
-
- private ActivityWishTreeBinding binding;
-
- private final Handler handler = new Handler(Looper.getMainLooper());
- private Runnable timerRunnable;
-
- public static void start(Context context) {
- Intent intent = new Intent(context, WishTreeActivity.class);
- context.startActivity(intent);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- startBannerCountdown();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- stopBannerCountdown();
- }
-
- private void startBannerCountdown() {
- stopBannerCountdown();
- timerRunnable = new Runnable() {
- @Override
- public void run() {
- updateBannerTimer();
- handler.postDelayed(this, 1000);
- }
- };
- handler.post(timerRunnable);
- }
-
- private void stopBannerCountdown() {
- if (timerRunnable != null) {
- handler.removeCallbacks(timerRunnable);
- timerRunnable = null;
- }
- }
-
- private void updateBannerTimer() {
- if (binding == null) return;
- try {
- long now = System.currentTimeMillis();
- TimeZone tz = TimeZone.getDefault();
- Calendar c = Calendar.getInstance(tz);
- c.setTimeInMillis(now);
- c.set(Calendar.HOUR_OF_DAY, 0);
- c.set(Calendar.MINUTE, 0);
- c.set(Calendar.SECOND, 0);
- c.set(Calendar.MILLISECOND, 0);
- c.add(Calendar.DAY_OF_MONTH, 1);
- long nextMidnight = c.getTimeInMillis();
- long diff = Math.max(0, nextMidnight - now);
-
- long totalSeconds = diff / 1000;
- long hours = totalSeconds / 3600;
- long minutes = (totalSeconds % 3600) / 60;
- long seconds = totalSeconds % 60;
-
- SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
- fmt.setTimeZone(tz);
- String current = fmt.format(new Date(now));
- String remain = String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
- binding.bannerTimer.setText(current + " " + remain);
- } catch (Exception ignored) {
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- startBannerCountdown();
-
- BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
- bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
- bottomNavigation.setOnItemSelectedListener(item -> {
- int id = item.getItemId();
- if (id == R.id.nav_wish_tree) {
- return true;
- }
- if (id == R.id.nav_home) {
- startActivity(new Intent(this, MainActivity.class));
- finish();
- return true;
- }
- if (id == R.id.nav_friends) {
- startActivity(new Intent(this, FishPondActivity.class));
- finish();
- return true;
- }
- if (id == R.id.nav_messages) {
- MessagesActivity.start(this);
- finish();
- return true;
- }
- if (id == R.id.nav_profile) {
- ProfileActivity.start(this);
- finish();
- return true;
- }
- return true;
- });
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (binding != null) {
- binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
- }
- }
-}
+package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.databinding.ActivityWishTreeBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
public class WishTreeActivity extends AppCompatActivity {
private ActivityWishTreeBinding binding;
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable timerRunnable;
public static void start(Context context) {
Intent intent = new Intent(context, WishTreeActivity.class);
context.startActivity(intent);
}
@Override
protected void onStart() {
super.onStart();
startBannerCountdown();
}
@Override
protected void onStop() {
super.onStop();
stopBannerCountdown();
}
private void startBannerCountdown() {
stopBannerCountdown();
timerRunnable = new Runnable() {
@Override
public void run() {
updateBannerTimer();
handler.postDelayed(this, 1000);
}
};
handler.post(timerRunnable);
}
private void stopBannerCountdown() {
if (timerRunnable != null) {
handler.removeCallbacks(timerRunnable);
timerRunnable = null;
}
}
private void updateBannerTimer() {
if (binding == null) return;
try {
long now = System.currentTimeMillis();
TimeZone tz = TimeZone.getDefault();
Calendar c = Calendar.getInstance(tz);
c.setTimeInMillis(now);
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
c.add(Calendar.DAY_OF_MONTH, 1);
long nextMidnight = c.getTimeInMillis();
long diff = Math.max(0, nextMidnight - now);
long totalSeconds = diff / 1000;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
fmt.setTimeZone(tz);
String current = fmt.format(new Date(now));
String remain = String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
binding.bannerTimer.setText(current + " " + remain);
} catch (Exception ignored) {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
startBannerCountdown();
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_wish_tree) {
return true;
}
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
finish();
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
ProfileActivity.start(this);
finish();
return true;
}
return true;
});
}
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
}
}
}
\ No newline at end of file
diff --git a/android-app/app/src/main/res/drawable/bg_bubble_incoming.xml b/android-app/app/src/main/res/drawable/bg_bubble_incoming.xml
new file mode 100644
index 00000000..53128ee8
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_bubble_incoming.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/bg_bubble_outgoing.xml b/android-app/app/src/main/res/drawable/bg_bubble_outgoing.xml
new file mode 100644
index 00000000..b77f0e54
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_bubble_outgoing.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_conversation.xml b/android-app/app/src/main/res/layout/activity_conversation.xml
new file mode 100644
index 00000000..0b6712f1
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_conversation.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_room_detail_new.xml b/android-app/app/src/main/res/layout/activity_room_detail_new.xml
index 47f3634f..d3c5c6ef 100644
--- a/android-app/app/src/main/res/layout/activity_room_detail_new.xml
+++ b/android-app/app/src/main/res/layout/activity_room_detail_new.xml
@@ -101,6 +101,18 @@
app:use_controller="true"
app:show_buffering="when_playing" />
+
+
+ app:iconSize="14dp"
+ app:iconTint="@android:color/white" />
diff --git a/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml b/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml
index b9c033b4..85f2c408 100644
--- a/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml
+++ b/android-app/app/src/main/res/layout/activity_user_profile_read_only.xml
@@ -52,19 +52,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/backButton" />
-
+ app:layout_constraintTop_toTopOf="@id/avatarRing"
+ app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.MaterialComponents.Circular" />
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml b/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml
new file mode 100644
index 00000000..5e8a35cc
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml
index 4c4b3402..1204dd61 100644
--- a/android-app/app/src/main/res/values/themes.xml
+++ b/android-app/app/src/main/res/values/themes.xml
@@ -26,4 +26,12 @@
- 14sp
+
+
+
+
+
+