UI界面的编写

This commit is contained in:
ShiQi 2025-12-19 15:11:49 +08:00
parent e91dbf43d3
commit 8802e066f1
18 changed files with 833 additions and 157 deletions

View File

@ -80,6 +80,10 @@
android:name="com.example.livestreaming.MessagesActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.ConversationActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.PlayerActivity"
android:exported="false" />

View File

@ -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<ChatMessage> 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));
}
}

View File

@ -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<ChatMessage, RecyclerView.ViewHolder> {
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<ChatMessage> DIFF = new DiffUtil.ItemCallback<ChatMessage>() {
@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();
}
};
}

View File

@ -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, "");

View File

@ -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);

View File

@ -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);
}

View File

@ -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<ConversationItem> 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<ConversationItem> buildDemoConversations() {

View File

@ -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, "加好友"));
}

View File

@ -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 {

View File

@ -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); } } }

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#F1F3F5" />
<corners android:radius="16dp" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#4F7BFF" />
<corners android:radius="16dp" />
</shape>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topBar"
android:layout_width="0dp"
android:layout_height="56dp"
android:background="@android:color/white"
android:paddingStart="8dp"
android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/backButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:contentDescription="back"
android:padding="8dp"
android:src="@drawable/ic_arrow_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/avatarView"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="4dp"
android:background="@drawable/bg_avatar_circle"
android:padding="5dp"
android:src="@drawable/ic_account_circle_24"
app:layout_constraintBottom_toBottomOf="@id/backButton"
app:layout_constraintStart_toEndOf="@id/backButton"
app:layout_constraintTop_toTopOf="@id/backButton" />
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:maxLines="1"
android:text="会话"
android:textColor="#111111"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/avatarView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatarView"
app:layout_constraintTop_toTopOf="@id/avatarView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
android:id="@+id/topDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#EEEEEE"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messagesRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingTop="6dp"
android:paddingBottom="6dp"
app:layout_constraintBottom_toTopOf="@id/inputBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topDivider" />
<LinearLayout
android:id="@+id/inputBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#FAFAFA"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/messageInput"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
android:background="@drawable/bg_white_16"
android:hint="输入消息..."
android:imeOptions="actionSend"
android:inputType="text"
android:maxLines="4"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="#111111"
android:textSize="14sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sendButton"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginStart="10dp"
android:minWidth="64dp"
android:text="发送"
android:textAllCaps="false"
android:textSize="13sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -101,6 +101,18 @@
app:use_controller="true"
app:show_buffering="when_playing" />
<ImageButton
android:id="@+id/exitFullscreenButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_gravity="top|start"
android:layout_margin="12dp"
android:background="@drawable/bg_circle_white_60"
android:contentDescription="退出全屏"
android:src="@drawable/ic_arrow_back_24"
android:tint="@android:color/white"
android:visibility="gone" />
<!-- 横屏按钮 -->
<ImageButton
android:id="@+id/fullscreenButton"
@ -202,12 +214,19 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/followButton"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_width="92dp"
android:layout_height="40dp"
android:paddingHorizontal="14dp"
android:text="关注"
android:textColor="@android:color/white"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"
android:textAllCaps="false"
app:backgroundTint="#FF2FD3C7"
app:icon="@drawable/ic_heart_24"
app:iconSize="14dp" />
app:iconSize="14dp"
app:iconTint="@android:color/white" />
</LinearLayout>

View File

@ -52,19 +52,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/backButton" />
<ImageView
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatar"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_margin="3dp"
android:background="@drawable/bg_avatar_circle_transparent"
android:clipToOutline="true"
android:scaleType="centerCrop"
android:src="@drawable/wish_tree_checker_backup"
app:layout_constraintBottom_toBottomOf="@id/avatarRing"
app:layout_constraintEnd_toEndOf="@id/avatarRing"
app:layout_constraintStart_toStartOf="@id/avatarRing"
app:layout_constraintTop_toTopOf="@id/avatarRing" />
app:layout_constraintTop_toTopOf="@id/avatarRing"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.MaterialComponents.Circular" />
<TextView
android:id="@+id/name"

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="1dp"
android:paddingBottom="1dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/maxBubbleWidthGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<ImageView
android:id="@+id/avatarView"
android:layout_width="34dp"
android:layout_height="34dp"
android:background="@drawable/bg_avatar_circle"
android:padding="5dp"
android:src="@drawable/ic_account_circle_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/nameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:text="对方"
android:textColor="#888888"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatarView"
app:layout_constraintTop_toTopOf="@id/avatarView" />
<TextView
android:id="@+id/messageText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="0dp"
android:background="@drawable/bg_bubble_incoming"
android:includeFontPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:text="消息内容"
android:textColor="#111111"
android:textSize="14sp"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_default="wrap"
app:layout_constraintEnd_toStartOf="@id/maxBubbleWidthGuideline"
app:layout_constraintStart_toEndOf="@id/avatarView"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintTop_toTopOf="@id/avatarView" />
<TextView
android:id="@+id/timeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:includeFontPadding="false"
android:text="12:30"
android:textColor="#AAAAAA"
android:textSize="11sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/messageText"
app:layout_constraintTop_toBottomOf="@id/messageText" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="1dp"
android:paddingBottom="1dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/maxBubbleWidthGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<ImageView
android:id="@+id/avatarView"
android:layout_width="34dp"
android:layout_height="34dp"
android:background="@drawable/bg_avatar_circle"
android:padding="5dp"
android:src="@drawable/ic_account_circle_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/messageText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:background="@drawable/bg_bubble_outgoing"
android:includeFontPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:text="消息内容"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constrainedWidth="true"
app:layout_constraintWidth_default="wrap"
app:layout_constraintStart_toStartOf="@id/maxBubbleWidthGuideline"
app:layout_constraintEnd_toStartOf="@id/avatarView"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/timeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:includeFontPadding="false"
android:text="12:31"
android:textColor="#AAAAAA"
android:textSize="11sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/messageText"
app:layout_constraintTop_toBottomOf="@id/messageText" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -26,4 +26,12 @@
<item name="android:textSize">14sp</item>
</style>
<style name="ShapeAppearanceOverlay" parent="ShapeAppearanceOverlay.MaterialComponents" />
<style name="ShapeAppearanceOverlay.MaterialComponents" />
<style name="ShapeAppearanceOverlay.MaterialComponents.Circular" parent="ShapeAppearanceOverlay.MaterialComponents">
<item name="cornerSize">50%</item>
</style>
</resources>