From 8802e066f134b17ef00eadb570e936fdb0e660d3 Mon Sep 17 00:00:00 2001 From: ShiQi <3572915148@qq.com> Date: Fri, 19 Dec 2025 15:11:49 +0800 Subject: [PATCH] =?UTF-8?q?UI=E7=95=8C=E9=9D=A2=E7=9A=84=E7=BC=96=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android-app/app/src/main/AndroidManifest.xml | 4 + .../livestreaming/ConversationActivity.java | 97 ++++++++ .../ConversationMessagesAdapter.java | 106 +++++++++ .../livestreaming/EditProfileActivity.java | 49 +++- .../livestreaming/FishPondActivity.java | 6 +- .../example/livestreaming/MainActivity.java | 12 +- .../livestreaming/MessagesActivity.java | 224 +++++++++++++++++- .../livestreaming/ProfileActivity.java | 30 ++- .../livestreaming/TabPlaceholderActivity.java | 7 +- .../livestreaming/WishTreeActivity.java | 139 +---------- .../main/res/drawable/bg_bubble_incoming.xml | 5 + .../main/res/drawable/bg_bubble_outgoing.xml | 5 + .../main/res/layout/activity_conversation.xml | 125 ++++++++++ .../res/layout/activity_room_detail_new.xml | 25 +- .../activity_user_profile_read_only.xml | 7 +- .../item_conversation_message_incoming.xml | 79 ++++++ .../item_conversation_message_outgoing.xml | 62 +++++ .../app/src/main/res/values/themes.xml | 8 + 18 files changed, 833 insertions(+), 157 deletions(-) create mode 100644 android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java create mode 100644 android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java create mode 100644 android-app/app/src/main/res/drawable/bg_bubble_incoming.xml create mode 100644 android-app/app/src/main/res/drawable/bg_bubble_outgoing.xml create mode 100644 android-app/app/src/main/res/layout/activity_conversation.xml create mode 100644 android-app/app/src/main/res/layout/item_conversation_message_incoming.xml create mode 100644 android-app/app/src/main/res/layout/item_conversation_message_outgoing.xml 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 + +