修复了界面中的样式一些问题,还有内存泄漏等问题
This commit is contained in:
parent
a6b56bceb1
commit
d3854e20c8
|
|
@ -72,4 +72,7 @@ dependencies {
|
|||
implementation("androidx.media3:media3-exoplayer:$media3Version")
|
||||
implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
|
||||
implementation("androidx.media3:media3-ui:$media3Version")
|
||||
|
||||
// 内存泄漏检测 (仅在debug版本中使用)
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.13")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<application
|
||||
android:name=".LiveStreamingApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Live Streaming"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
/**
|
||||
* 统一的空状态视图组件
|
||||
* 支持自定义图标、标题、描述和操作按钮
|
||||
*/
|
||||
public class EmptyStateView extends ConstraintLayout {
|
||||
|
||||
private ImageView iconView;
|
||||
private TextView titleView;
|
||||
private TextView messageView;
|
||||
private Button actionButton;
|
||||
|
||||
public EmptyStateView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public EmptyStateView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public EmptyStateView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_empty_state, this, true);
|
||||
|
||||
iconView = findViewById(R.id.emptyIcon);
|
||||
titleView = findViewById(R.id.emptyTitle);
|
||||
messageView = findViewById(R.id.emptyMessage);
|
||||
actionButton = findViewById(R.id.emptyActionButton);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EmptyStateView);
|
||||
try {
|
||||
// 从XML属性设置内容
|
||||
String title = a.getString(R.styleable.EmptyStateView_emptyTitle);
|
||||
String message = a.getString(R.styleable.EmptyStateView_emptyMessage);
|
||||
String actionText = a.getString(R.styleable.EmptyStateView_emptyActionText);
|
||||
Drawable icon = a.getDrawable(R.styleable.EmptyStateView_emptyIcon);
|
||||
|
||||
if (title != null) setTitle(title);
|
||||
if (message != null) setMessage(message);
|
||||
if (actionText != null) setActionText(actionText);
|
||||
if (icon != null) setIcon(icon);
|
||||
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置空状态图标
|
||||
*/
|
||||
public void setIcon(@DrawableRes int iconRes) {
|
||||
iconView.setImageResource(iconRes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置空状态图标
|
||||
*/
|
||||
public void setIcon(Drawable icon) {
|
||||
iconView.setImageDrawable(icon);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标题
|
||||
*/
|
||||
public void setTitle(String title) {
|
||||
titleView.setText(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置描述信息
|
||||
*/
|
||||
public void setMessage(String message) {
|
||||
messageView.setText(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作按钮文本
|
||||
*/
|
||||
public void setActionText(String text) {
|
||||
actionButton.setText(text);
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏操作按钮
|
||||
*/
|
||||
public void hideActionButton() {
|
||||
actionButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作按钮点击监听器
|
||||
*/
|
||||
public void setOnActionClickListener(OnClickListener listener) {
|
||||
actionButton.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为网络错误状态
|
||||
*/
|
||||
public void setNetworkErrorState() {
|
||||
setIcon(R.drawable.ic_globe_24);
|
||||
setTitle("网络连接失败");
|
||||
setMessage("请检查网络连接后重试");
|
||||
setActionText("重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为无直播间状态
|
||||
*/
|
||||
public void setNoRoomsState() {
|
||||
setIcon(R.drawable.ic_home_24);
|
||||
setTitle("暂无直播间");
|
||||
setMessage("当前没有正在直播的房间,快去创建一个吧");
|
||||
setActionText("刷新");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为无消息状态
|
||||
*/
|
||||
public void setNoMessagesState() {
|
||||
setIcon(R.drawable.ic_chat_24);
|
||||
setTitle("暂无消息");
|
||||
setMessage("还没有人给你发消息,快去和朋友聊天吧");
|
||||
hideActionButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为无搜索结果状态
|
||||
*/
|
||||
public void setNoSearchResultsState() {
|
||||
setIcon(R.drawable.ic_search_24);
|
||||
setTitle("没有找到相关内容");
|
||||
setMessage("换个关键词试试吧");
|
||||
hideActionButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为无好友状态
|
||||
*/
|
||||
public void setNoFriendsState() {
|
||||
setIcon(R.drawable.ic_people_24);
|
||||
setTitle("暂无好友");
|
||||
setMessage("去添加一些好友一起玩耍吧");
|
||||
setActionText("去添加");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为无作品状态
|
||||
*/
|
||||
public void setNoWorksState() {
|
||||
setIcon(R.drawable.ic_person_24);
|
||||
setTitle("暂无作品");
|
||||
setMessage("快来发布你的精彩作品吧");
|
||||
setActionText("去发布");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
/**
|
||||
* 统一的错误处理工具类
|
||||
* 提供统一的错误提示和重试机制
|
||||
*/
|
||||
public class ErrorHandler {
|
||||
|
||||
/**
|
||||
* 显示网络错误Snackbar,带重试按钮
|
||||
*/
|
||||
public static void showNetworkErrorWithRetry(@NonNull View view, @NonNull Runnable retryAction) {
|
||||
showErrorWithRetry(view, "网络连接失败,请检查网络后重试", "重试", retryAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示服务器错误Snackbar,带重试按钮
|
||||
*/
|
||||
public static void showServerErrorWithRetry(@NonNull View view, @NonNull Runnable retryAction) {
|
||||
showErrorWithRetry(view, "服务器响应异常,请稍后重试", "重试", retryAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载失败Snackbar,带重试按钮
|
||||
*/
|
||||
public static void showLoadErrorWithRetry(@NonNull View view, @NonNull Runnable retryAction) {
|
||||
showErrorWithRetry(view, "加载失败,请重试", "重试", retryAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通用错误Snackbar,带重试按钮
|
||||
*/
|
||||
public static void showErrorWithRetry(@NonNull View view, @NonNull String message,
|
||||
@NonNull String actionText, @NonNull Runnable retryAction) {
|
||||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.setAction(actionText, v -> retryAction.run());
|
||||
snackbar.setActionTextColor(view.getContext().getResources().getColor(R.color.purple_500));
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示简单错误提示,不带重试按钮
|
||||
*/
|
||||
public static void showSimpleError(@NonNull View view, @NonNull String message) {
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功提示
|
||||
*/
|
||||
public static void showSuccess(@NonNull View view, @NonNull String message) {
|
||||
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_SHORT);
|
||||
snackbar.setBackgroundTint(view.getContext().getResources().getColor(R.color.purple_500));
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示信息提示
|
||||
*/
|
||||
public static void showInfo(@NonNull View view, @NonNull String message) {
|
||||
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理API响应错误
|
||||
*/
|
||||
public static void handleApiError(@NonNull View view, Throwable error, @NonNull Runnable retryAction) {
|
||||
String message;
|
||||
if (error.getMessage() != null && error.getMessage().contains("timeout")) {
|
||||
message = "请求超时,请检查网络连接";
|
||||
} else if (error.getMessage() != null && error.getMessage().contains("Unable to resolve host")) {
|
||||
message = "网络连接失败,请检查网络设置";
|
||||
} else {
|
||||
message = "请求失败,请稍后重试";
|
||||
}
|
||||
showErrorWithRetry(view, message, "重试", retryAction);
|
||||
}
|
||||
}
|
||||
|
|
@ -447,4 +447,20 @@ public class FishPondActivity extends AppCompatActivity {
|
|||
stopOrbitAnimation();
|
||||
stopPulseLoop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
// 清理Handler和Runnable,防止内存泄漏
|
||||
if (uiHandler != null) {
|
||||
uiHandler.removeCallbacks(pulseRunnable);
|
||||
}
|
||||
|
||||
// 确保动画完全停止和清理
|
||||
if (orbitAnimator != null) {
|
||||
orbitAnimator.cancel();
|
||||
orbitAnimator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
/**
|
||||
* 自定义Application类,用于初始化各种组件
|
||||
* 包括内存泄漏检测等
|
||||
*/
|
||||
public class LiveStreamingApplication extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// 初始化LeakCanary内存泄漏检测(仅在debug版本中生效)
|
||||
// LeakCanary会自动在debug版本中初始化,无需手动调用
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import android.text.Editable;
|
|||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
|
@ -37,6 +38,8 @@ import com.example.livestreaming.databinding.ActivityMainBinding;
|
|||
import com.example.livestreaming.databinding.DialogCreateRoomBinding;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.CreateRoomRequest;
|
||||
|
|
@ -213,12 +216,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
// 立即显示演示数据,提升用户体验
|
||||
allRooms.clear();
|
||||
allRooms.addAll(buildDemoRooms(12));
|
||||
allRooms.addAll(buildDemoRooms(20));
|
||||
applyCategoryFilter(currentCategory);
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
|
||||
// 注释掉下拉刷新,使用静态数据
|
||||
// binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
|
||||
|
||||
setupDrawerCards();
|
||||
|
||||
|
|
@ -307,7 +311,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (lastVisible >= total - 4) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (!isFetching && now - lastFetchMs > 1500) {
|
||||
fetchRooms();
|
||||
// 注释掉加载更多,使用静态数据
|
||||
// fetchRooms();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -587,6 +592,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
// 清理Handler和Runnable,防止内存泄漏
|
||||
if (handler != null && pollRunnable != null) {
|
||||
handler.removeCallbacks(pollRunnable);
|
||||
}
|
||||
|
||||
// 清理语音识别器
|
||||
if (speechRecognizer != null) {
|
||||
speechRecognizer.destroy();
|
||||
speechRecognizer = null;
|
||||
|
|
@ -660,7 +672,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
startPolling();
|
||||
// 注释掉网络轮询,使用静态数据
|
||||
// startPolling();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -701,11 +714,24 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void showCreateRoomDialog() {
|
||||
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.inflate(getLayoutInflater());
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null);
|
||||
DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView);
|
||||
|
||||
// 设置直播类型选择器
|
||||
String[] liveTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, liveTypes);
|
||||
|
||||
// 使用正确的资源ID获取typeSpinner
|
||||
int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName());
|
||||
MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId);
|
||||
|
||||
if (typeSpinner != null) {
|
||||
typeSpinner.setAdapter(adapter);
|
||||
}
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle("创建直播间")
|
||||
.setView(dialogBinding.getRoot())
|
||||
.setView(dialogView)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("创建", null)
|
||||
.create();
|
||||
|
|
@ -713,7 +739,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
dialog.setOnShowListener(d -> {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
|
||||
String streamer = dialogBinding.streamerEdit.getText() != null ? dialogBinding.streamerEdit.getText().toString().trim() : "";
|
||||
String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
|
||||
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
dialogBinding.titleLayout.setError("标题不能为空");
|
||||
|
|
@ -722,16 +748,28 @@ public class MainActivity extends AppCompatActivity {
|
|||
dialogBinding.titleLayout.setError(null);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(streamer)) {
|
||||
dialogBinding.streamerLayout.setError("主播名称不能为空");
|
||||
if (TextUtils.isEmpty(type)) {
|
||||
int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
|
||||
TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
|
||||
if (typeLayout != null) {
|
||||
typeLayout.setError("请先选择直播类型");
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
dialogBinding.streamerLayout.setError(null);
|
||||
int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
|
||||
TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
|
||||
if (typeLayout != null) {
|
||||
typeLayout.setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户昵称
|
||||
String streamerName = getSharedPreferences("profile_prefs", MODE_PRIVATE)
|
||||
.getString("profile_name", "未知用户");
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
|
||||
ApiClient.getService().createRoom(new CreateRoomRequest(title, streamer))
|
||||
ApiClient.getService().createRoom(new CreateRoomRequest(title, streamerName, type))
|
||||
.enqueue(new Callback<ApiResponse<Room>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
|
||||
|
|
@ -852,26 +890,39 @@ public class MainActivity extends AppCompatActivity {
|
|||
private void fetchRooms() {
|
||||
// 避免重复请求
|
||||
if (isFetching) return;
|
||||
|
||||
|
||||
isFetching = true;
|
||||
lastFetchMs = System.currentTimeMillis();
|
||||
|
||||
|
||||
// 隐藏空状态和错误状态
|
||||
hideEmptyState();
|
||||
hideErrorState();
|
||||
|
||||
// 只在没有数据时显示loading
|
||||
if (adapter.getItemCount() == 0) {
|
||||
binding.loading.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ApiClient.getService().getRooms().enqueue(new Callback<ApiResponse<List<Room>>>() {
|
||||
Call<ApiResponse<List<Room>>> call = ApiClient.getService().getRooms();
|
||||
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<List<Room>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<Room>>> call, Response<ApiResponse<List<Room>>> response) {
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
isFetching = false;
|
||||
|
||||
ApiResponse<List<Room>> body = response.body();
|
||||
List<Room> rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList();
|
||||
|
||||
if (rooms == null || rooms.isEmpty()) {
|
||||
rooms = buildDemoRooms(12);
|
||||
// 使用演示数据,但显示空状态
|
||||
rooms = buildDemoRooms(0); // 生成0个演示房间
|
||||
showNoRoomsState();
|
||||
} else {
|
||||
// 有真实数据,隐藏空状态
|
||||
hideEmptyState();
|
||||
}
|
||||
|
||||
allRooms.clear();
|
||||
allRooms.addAll(rooms);
|
||||
applyCategoryFilter(currentCategory);
|
||||
|
|
@ -883,18 +934,63 @@ public class MainActivity extends AppCompatActivity {
|
|||
binding.loading.setVisibility(View.GONE);
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
isFetching = false;
|
||||
|
||||
// 显示网络错误Snackbar和空状态
|
||||
ErrorHandler.handleApiError(binding.getRoot(), t, () -> fetchRooms());
|
||||
showNetworkErrorState();
|
||||
|
||||
// 仍然提供演示数据作为后备
|
||||
allRooms.clear();
|
||||
allRooms.addAll(buildDemoRooms(12));
|
||||
allRooms.addAll(buildDemoRooms(0));
|
||||
applyCategoryFilter(currentCategory);
|
||||
adapter.bumpCoverOffset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示无直播间空状态
|
||||
*/
|
||||
private void showNoRoomsState() {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setNoRoomsState();
|
||||
binding.emptyStateView.setOnActionClickListener(v -> fetchRooms());
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示网络错误状态
|
||||
*/
|
||||
private void showNetworkErrorState() {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setNetworkErrorState();
|
||||
binding.emptyStateView.setOnActionClickListener(v -> fetchRooms());
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏空状态视图
|
||||
*/
|
||||
private void hideEmptyState() {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏错误状态(实际上和hideEmptyState一样)
|
||||
*/
|
||||
private void hideErrorState() {
|
||||
hideEmptyState();
|
||||
}
|
||||
|
||||
private void applyCategoryFilter(String category) {
|
||||
String c = category != null ? category : "推荐";
|
||||
if ("推荐".equals(c)) {
|
||||
adapter.submitList(new ArrayList<>(allRooms));
|
||||
updateEmptyStateForList(allRooms);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -906,6 +1002,25 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
adapter.submitList(filtered);
|
||||
updateEmptyStateForList(filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列表数据更新空状态显示
|
||||
*/
|
||||
private void updateEmptyStateForList(List<Room> rooms) {
|
||||
if (rooms == null || rooms.isEmpty()) {
|
||||
// 显示分类筛选无结果的状态
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setIcon(R.drawable.ic_search_24);
|
||||
binding.emptyStateView.setTitle("该分类下暂无直播间");
|
||||
binding.emptyStateView.setMessage("换个分类看看吧");
|
||||
binding.emptyStateView.hideActionButton();
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
hideEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
private String getDemoCategoryForRoom(Room room) {
|
||||
|
|
@ -921,13 +1036,55 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
private List<Room> buildDemoRooms(int count) {
|
||||
List<Room> list = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
||||
// 预定义的演示数据,包含不同类型的直播内容
|
||||
String[][] demoData = {
|
||||
{"王者荣耀排位赛", "小明选手", "游戏", "true"},
|
||||
{"吃鸡大逃杀", "游戏高手", "游戏", "true"},
|
||||
{"唱歌连麦", "音乐达人", "音乐", "true"},
|
||||
{"户外直播", "旅行者", "户外", "false"},
|
||||
{"美食制作", "厨神小李", "美食", "true"},
|
||||
{"才艺表演", "舞蹈小妹", "才艺", "true"},
|
||||
{"聊天交友", "暖心姐姐", "聊天", "false"},
|
||||
{"LOL竞技场", "电竞选手", "游戏", "true"},
|
||||
{"古风演奏", "琴师小王", "音乐", "true"},
|
||||
{"健身教学", "教练张", "户外", "false"},
|
||||
{"摄影分享", "摄影师", "户外", "true"},
|
||||
{"宠物秀", "萌宠主播", "才艺", "true"},
|
||||
{"编程教学", "码农老王", "聊天", "false"},
|
||||
{"读书分享", "书虫小妹", "聊天", "true"},
|
||||
{"手工制作", "手艺人", "才艺", "true"},
|
||||
{"英语口语", "外教老师", "聊天", "false"},
|
||||
{"魔术表演", "魔术师", "才艺", "true"},
|
||||
{"街头访谈", "记者小张", "户外", "true"},
|
||||
{"乐器教学", "音乐老师", "音乐", "false"},
|
||||
{"电影解说", "影评人", "聊天", "true"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < count && i < demoData.length; i++) {
|
||||
String id = "demo-" + i;
|
||||
String title = "王者荣耀陪练" + (i + 1);
|
||||
String streamer = "虚拟主播" + (i + 1);
|
||||
boolean live = i % 3 != 0;
|
||||
list.add(new Room(id, title, streamer, live));
|
||||
String title = demoData[i][0];
|
||||
String streamer = demoData[i][1];
|
||||
String type = demoData[i][2];
|
||||
boolean live = Boolean.parseBoolean(demoData[i][3]);
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
list.add(room);
|
||||
}
|
||||
|
||||
// 如果需要更多数据,继续生成
|
||||
String[] categories = new String[]{"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
|
||||
for (int i = demoData.length; i < count; i++) {
|
||||
String id = "demo-" + i;
|
||||
String title = "直播房间" + (i + 1);
|
||||
String streamer = "主播" + (i + 1);
|
||||
String type = categories[i % categories.length];
|
||||
boolean live = i % 3 != 0;
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
list.add(room);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
conversations.addAll(buildDemoConversations());
|
||||
conversationsAdapter.submitList(new ArrayList<>(conversations));
|
||||
|
||||
// 检查是否需要显示空状态
|
||||
updateEmptyState();
|
||||
|
||||
attachSwipeToDelete(binding.conversationsRecyclerView);
|
||||
}
|
||||
|
||||
|
|
@ -459,4 +462,20 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新空状态显示
|
||||
*/
|
||||
private void updateEmptyState() {
|
||||
if (conversations.isEmpty()) {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setNoMessagesState();
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.example.livestreaming;
|
|||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
|
@ -59,6 +60,7 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
private void applyFilter(String query) {
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
updateEmptyState(all);
|
||||
return;
|
||||
}
|
||||
String q = query.toLowerCase();
|
||||
|
|
@ -72,6 +74,23 @@ public class MyFriendsActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
adapter.submitList(filtered);
|
||||
updateEmptyState(filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新空状态显示
|
||||
*/
|
||||
private void updateEmptyState(List<FriendItem> friends) {
|
||||
if (friends == null || friends.isEmpty()) {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setNoFriendsState();
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<FriendItem> buildDemoFriends() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import retrofit2.Call;
|
||||
|
||||
/**
|
||||
* 网络请求管理器
|
||||
* 用于管理Activity/Fragment的网络请求,在生命周期结束时自动取消
|
||||
*/
|
||||
public class NetworkRequestManager implements LifecycleEventObserver {
|
||||
|
||||
private final Set<Call<?>> activeCalls = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 添加网络请求到管理器
|
||||
*/
|
||||
public synchronized void addCall(Call<?> call) {
|
||||
if (call != null && !call.isCanceled()) {
|
||||
activeCalls.add(call);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从管理器中移除网络请求
|
||||
*/
|
||||
public synchronized void removeCall(Call<?> call) {
|
||||
if (call != null) {
|
||||
activeCalls.remove(call);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有活跃的网络请求
|
||||
*/
|
||||
public synchronized void cancelAll() {
|
||||
for (Call<?> call : activeCalls) {
|
||||
if (call != null && !call.isCanceled()) {
|
||||
call.cancel();
|
||||
}
|
||||
}
|
||||
activeCalls.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃请求的数量
|
||||
*/
|
||||
public synchronized int getActiveCallCount() {
|
||||
return activeCalls.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定到LifecycleOwner,在DESTROY时自动取消所有请求
|
||||
*/
|
||||
public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
lifecycleOwner.getLifecycle().addObserver(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑Lifecycle
|
||||
*/
|
||||
public void unbindFromLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
lifecycleOwner.getLifecycle().removeObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
|
||||
if (event == Lifecycle.Event.ON_DESTROY) {
|
||||
cancelAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* 网络工具类
|
||||
* 提供生命周期感知的网络请求管理
|
||||
*/
|
||||
public class NetworkUtils {
|
||||
|
||||
/**
|
||||
* 执行网络请求,并自动管理生命周期
|
||||
* 当LifecycleOwner销毁时,自动取消请求
|
||||
*/
|
||||
public static <T> void enqueueWithLifecycle(
|
||||
@NonNull Call<T> call,
|
||||
@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull Callback<T> callback) {
|
||||
|
||||
// 创建请求管理器并绑定到生命周期
|
||||
NetworkRequestManager manager = new NetworkRequestManager();
|
||||
manager.bindToLifecycle(lifecycleOwner);
|
||||
manager.addCall(call);
|
||||
|
||||
// 执行请求
|
||||
call.enqueue(new Callback<T>() {
|
||||
@Override
|
||||
public void onResponse(Call<T> call, Response<T> response) {
|
||||
manager.removeCall(call);
|
||||
callback.onResponse(call, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<T> call, Throwable t) {
|
||||
manager.removeCall(call);
|
||||
callback.onFailure(call, t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的网络请求执行,不带生命周期管理
|
||||
* 注意:需要手动管理请求取消
|
||||
*/
|
||||
public static <T> void enqueue(@NonNull Call<T> call, @NonNull Callback<T> callback) {
|
||||
call.enqueue(callback);
|
||||
}
|
||||
}
|
||||
|
|
@ -298,11 +298,12 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
binding.loading.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
ApiClient.getService().getRoom(roomId).enqueue(new Callback<ApiResponse<Room>>() {
|
||||
Call<ApiResponse<Room>> call = ApiClient.getService().getRoom(roomId);
|
||||
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<Room>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
|
||||
if (isFinishing() || isDestroyed()) return;
|
||||
|
||||
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
isFirstLoad = false;
|
||||
|
||||
|
|
@ -452,5 +453,22 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
// 确保Handler回调被清理,防止内存泄漏
|
||||
if (handler != null) {
|
||||
if (pollRunnable != null) {
|
||||
handler.removeCallbacks(pollRunnable);
|
||||
}
|
||||
if (chatSimulationRunnable != null) {
|
||||
handler.removeCallbacks(chatSimulationRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
// 释放播放器资源
|
||||
releasePlayer();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.content.Intent;
|
|||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
|
@ -99,6 +100,7 @@ public class SearchActivity extends AppCompatActivity {
|
|||
String query = q != null ? q.trim() : "";
|
||||
if (query.isEmpty()) {
|
||||
adapter.submitList(new ArrayList<>(all));
|
||||
updateEmptyState(all);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +114,23 @@ public class SearchActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
adapter.submitList(filtered);
|
||||
updateEmptyState(filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新空状态显示
|
||||
*/
|
||||
private void updateEmptyState(List<Room> rooms) {
|
||||
if (rooms == null || rooms.isEmpty()) {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setNoSearchResultsState();
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Room> buildDemoRooms(int count) {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ public class CreateRoomRequest {
|
|||
@SerializedName("streamerName")
|
||||
private final String streamerName;
|
||||
|
||||
public CreateRoomRequest(String title, String streamerName) {
|
||||
@SerializedName("type")
|
||||
private final String type;
|
||||
|
||||
public CreateRoomRequest(String title, String streamerName, String type) {
|
||||
this.title = title;
|
||||
this.streamerName = streamerName;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
|
|
@ -22,4 +26,8 @@ public class CreateRoomRequest {
|
|||
public String getStreamerName() {
|
||||
return streamerName;
|
||||
}
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,9 @@ public class Room {
|
|||
@SerializedName("streamerName")
|
||||
private String streamerName;
|
||||
|
||||
@SerializedName("type")
|
||||
private String type;
|
||||
|
||||
@SerializedName("streamKey")
|
||||
private String streamKey;
|
||||
|
||||
|
|
@ -49,6 +52,10 @@ public class Room {
|
|||
this.streamerName = streamerName;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void setLive(boolean live) {
|
||||
isLive = live;
|
||||
}
|
||||
|
|
@ -77,6 +84,10 @@ public class Room {
|
|||
return streamerName;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getStreamKey() {
|
||||
return streamKey;
|
||||
}
|
||||
|
|
@ -103,12 +114,13 @@ public class Room {
|
|||
&& Objects.equals(id, room.id)
|
||||
&& Objects.equals(title, room.title)
|
||||
&& Objects.equals(streamerName, room.streamerName)
|
||||
&& Objects.equals(type, room.type)
|
||||
&& Objects.equals(streamKey, room.streamKey)
|
||||
&& Objects.equals(streamUrls, room.streamUrls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, title, streamerName, streamKey, isLive, viewerCount, streamUrls);
|
||||
return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, streamUrls);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,6 +212,14 @@
|
|||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAddLive"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@
|
|||
android:paddingBottom="88dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<include
|
||||
android:id="@+id/bottomNavInclude"
|
||||
layout="@layout/include_bottom_nav"
|
||||
|
|
|
|||
|
|
@ -96,4 +96,11 @@
|
|||
android:paddingBottom="16dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
|
|
@ -76,4 +76,11 @@
|
|||
android:paddingBottom="24dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<com.example.livestreaming.EmptyStateView
|
||||
android:id="@+id/emptyStateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
61
android-app/app/src/main/res/layout/view_empty_state.xml
Normal file
61
android-app/app/src/main/res/layout/view_empty_state.xml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?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:padding="32dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/emptyIcon"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:background="@drawable/bg_gray_12"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_person_24"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="#CCCCCC" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="暂无数据"
|
||||
android:textColor="#666666"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/emptyIcon" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="这里空空如也,快来添加一些内容吧"
|
||||
android:textColor="#999999"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/emptyTitle" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/emptyActionButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/bg_purple_20"
|
||||
android:minWidth="120dp"
|
||||
android:text="刷新试试"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/emptyMessage" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
9
android-app/app/src/main/res/values/attrs.xml
Normal file
9
android-app/app/src/main/res/values/attrs.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="EmptyStateView">
|
||||
<attr name="emptyIcon" format="reference" />
|
||||
<attr name="emptyTitle" format="string" />
|
||||
<attr name="emptyMessage" format="string" />
|
||||
<attr name="emptyActionText" format="string" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user