修复搜索框

This commit is contained in:
ShiQi 2025-12-23 12:39:14 +08:00
parent e0bbddcdfb
commit 0e39913d44
18 changed files with 2752 additions and 136 deletions

View File

@ -38,6 +38,10 @@
android:name="com.example.livestreaming.SettingsPageActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.NotificationSettingsActivity"
android:exported="false" />
<activity
android:name="com.example.livestreaming.WatchHistoryActivity"
android:exported="false" />

View File

@ -0,0 +1,231 @@
package com.example.livestreaming;
import android.content.Context;
import android.text.format.Formatter;
import com.bumptech.glide.Glide;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 缓存管理工具类
* 用于清理应用缓存图片缓存等
*/
public class CacheManager {
/**
* 获取缓存总大小字节
*/
public static long getCacheSize(Context context) {
long size = 0;
// 应用缓存目录
File cacheDir = context.getCacheDir();
if (cacheDir != null && cacheDir.exists()) {
size += getDirSize(cacheDir);
}
// 外部缓存目录
File externalCacheDir = context.getExternalCacheDir();
if (externalCacheDir != null && externalCacheDir.exists()) {
size += getDirSize(externalCacheDir);
}
// Glide图片缓存
try {
File glideCacheDir = new File(context.getCacheDir(), "image_manager_disk_cache");
if (glideCacheDir.exists()) {
size += getDirSize(glideCacheDir);
}
} catch (Exception e) {
// 忽略异常
}
// 其他缓存目录
File filesDir = context.getFilesDir();
if (filesDir != null && filesDir.exists()) {
File shareImagesDir = new File(filesDir, "share_images");
if (shareImagesDir.exists()) {
size += getDirSize(shareImagesDir);
}
}
return size;
}
/**
* 获取目录大小字节
*/
private static long getDirSize(File dir) {
long size = 0;
if (dir == null || !dir.exists()) {
return size;
}
File[] files = dir.listFiles();
if (files == null) {
return size;
}
for (File file : files) {
if (file.isDirectory()) {
size += getDirSize(file);
} else {
size += file.length();
}
}
return size;
}
/**
* 格式化缓存大小
*/
public static String formatCacheSize(Context context, long size) {
return Formatter.formatFileSize(context, size);
}
/**
* 清理所有缓存
*/
public static void clearAllCache(Context context, OnCacheClearListener listener) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
long clearedSize = 0;
try {
// 清理应用缓存
File cacheDir = context.getCacheDir();
if (cacheDir != null && cacheDir.exists()) {
clearedSize += clearDir(cacheDir);
}
// 清理外部缓存
File externalCacheDir = context.getExternalCacheDir();
if (externalCacheDir != null && externalCacheDir.exists()) {
clearedSize += clearDir(externalCacheDir);
}
// 清理Glide缓存需要在主线程执行
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> {
try {
Glide.get(context).clearDiskCache();
} catch (Exception e) {
// 忽略异常
}
});
// 清理其他缓存目录
File filesDir = context.getFilesDir();
if (filesDir != null && filesDir.exists()) {
File shareImagesDir = new File(filesDir, "share_images");
if (shareImagesDir.exists()) {
clearedSize += clearDir(shareImagesDir);
}
}
} catch (Exception e) {
if (listener != null) {
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> listener.onError(e));
}
return;
}
final long finalSize = clearedSize;
if (listener != null) {
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> listener.onSuccess(finalSize));
}
});
}
/**
* 清理图片缓存Glide缓存
*/
public static void clearImageCache(Context context, OnCacheClearListener listener) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
long clearedSize = 0;
try {
// 清理Glide磁盘缓存
File glideCacheDir = new File(context.getCacheDir(), "image_manager_disk_cache");
if (glideCacheDir.exists()) {
clearedSize += clearDir(glideCacheDir);
}
// 清理Glide内存缓存需要在主线程执行
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> {
try {
Glide.get(context).clearMemory();
} catch (Exception e) {
// 忽略异常
}
});
} catch (Exception e) {
if (listener != null) {
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> listener.onError(e));
}
return;
}
final long finalSize = clearedSize;
if (listener != null) {
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> listener.onSuccess(finalSize));
}
});
}
/**
* 清理目录
*/
private static long clearDir(File dir) {
long size = 0;
if (dir == null || !dir.exists()) {
return size;
}
File[] files = dir.listFiles();
if (files == null) {
return size;
}
for (File file : files) {
if (file.isDirectory()) {
size += clearDir(file);
// 删除空目录
try {
file.delete();
} catch (Exception e) {
// 忽略异常
}
} else {
size += file.length();
try {
file.delete();
} catch (Exception e) {
// 忽略异常
}
}
}
return size;
}
/**
* 缓存清理监听器
*/
public interface OnCacheClearListener {
void onSuccess(long clearedSize);
void onError(Exception e);
}
}

View File

@ -0,0 +1,139 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.SharedPreferences;
import com.example.livestreaming.net.Room;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 分类筛选管理器
* 用于管理房间列表的分类筛选功能
*/
public class CategoryFilterManager {
private static final String PREFS_NAME = "category_filter_prefs";
private static final String KEY_LAST_CATEGORY = "last_category";
private final ExecutorService executorService;
public CategoryFilterManager() {
executorService = Executors.newSingleThreadExecutor();
}
/**
* 异步筛选房间列表
*/
public void filterRoomsAsync(List<Room> allRooms, String category, FilterCallback callback) {
if (executorService == null || executorService.isShutdown()) {
// 如果线程池已关闭同步执行
List<Room> filtered = filterRoomsSync(allRooms, category);
if (callback != null) {
callback.onFiltered(filtered);
}
return;
}
executorService.execute(() -> {
List<Room> filtered = filterRoomsSync(allRooms, category);
if (callback != null) {
// 回调需要在主线程执行
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
mainHandler.post(() -> callback.onFiltered(filtered));
}
});
}
/**
* 同步筛选房间列表
*/
private List<Room> filterRoomsSync(List<Room> allRooms, String category) {
if (allRooms == null || allRooms.isEmpty()) {
return new ArrayList<>();
}
String c = category != null ? category : "推荐";
if ("全部".equals(c) || "推荐".equals(c)) {
return new ArrayList<>(allRooms);
}
List<Room> filtered = new ArrayList<>();
for (Room r : allRooms) {
if (r == null) continue;
String roomType = r.getType();
if (c.equals(roomType)) {
filtered.add(r);
continue;
}
// 降级到演示数据分类算法
String demoCategory = getDemoCategoryForRoom(r);
if (c.equals(demoCategory)) {
filtered.add(r);
}
}
return filtered;
}
/**
* 获取房间的演示分类用于降级处理
*/
private String getDemoCategoryForRoom(Room room) {
if (room == null) return "推荐";
String title = room.getTitle() != null ? room.getTitle() : "";
String streamer = room.getStreamerName() != null ? room.getStreamerName() : "";
// 简单的分类逻辑可以根据实际需求调整
if (title.contains("游戏") || streamer.contains("游戏")) {
return "游戏";
}
if (title.contains("音乐") || streamer.contains("音乐")) {
return "音乐";
}
if (title.contains("聊天") || streamer.contains("聊天")) {
return "聊天";
}
if (title.contains("才艺") || streamer.contains("才艺")) {
return "才艺";
}
return "推荐";
}
/**
* 保存最后选中的分类
*/
public static void saveLastCategory(Context context, String category) {
if (context == null) return;
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(KEY_LAST_CATEGORY, category).apply();
}
/**
* 获取最后选中的分类
*/
public static String getLastCategory(Context context) {
if (context == null) return "推荐";
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getString(KEY_LAST_CATEGORY, "推荐");
}
/**
* 关闭线程池
*/
public void shutdown() {
if (executorService != null && !executorService.isShutdown()) {
executorService.shutdown();
}
}
/**
* 筛选回调接口
*/
public interface FilterCallback {
void onFiltered(List<Room> filteredRooms);
}
}

View File

@ -0,0 +1,83 @@
package com.example.livestreaming;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
/**
* 统一的加载状态管理器
* 用于管理各种加载状态骨架屏下拉刷新等
*/
public class LoadingStateManager {
/**
* RecyclerView 上显示骨架屏
* @param recyclerView 目标 RecyclerView
* @param count 骨架屏项目数量
*/
public static void showSkeleton(RecyclerView recyclerView, int count) {
if (recyclerView == null) return;
// 创建一个简单的骨架屏适配器
SkeletonAdapter skeletonAdapter = new SkeletonAdapter(count);
recyclerView.setAdapter(skeletonAdapter);
}
/**
* 停止下拉刷新
* @param swipeRefresh SwipeRefreshLayout 实例
*/
public static void stopRefreshing(SwipeRefreshLayout swipeRefresh) {
if (swipeRefresh != null && swipeRefresh.isRefreshing()) {
swipeRefresh.setRefreshing(false);
}
}
/**
* 简单的骨架屏适配器
*/
private static class SkeletonAdapter extends RecyclerView.Adapter<SkeletonAdapter.SkeletonViewHolder> {
private final int itemCount;
public SkeletonAdapter(int count) {
this.itemCount = count;
}
@NonNull
@Override
public SkeletonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 创建一个简单的骨架屏视图
View view = new View(parent.getContext());
view.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));
view.setMinimumHeight(200); // 设置最小高度
view.setBackgroundColor(0xFFF0F0F0); // 浅灰色背景
return new SkeletonViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SkeletonViewHolder holder, int position) {
// 骨架屏不需要绑定数据
}
@Override
public int getItemCount() {
return itemCount;
}
static class SkeletonViewHolder extends RecyclerView.ViewHolder {
public SkeletonViewHolder(@NonNull View itemView) {
super(itemView);
}
}
}
}

View File

@ -60,7 +60,18 @@ public class MainActivity extends AppCompatActivity {
private RoomsAdapter adapter;
private final List<Room> allRooms = new ArrayList<>();
private final List<Room> followRooms = new ArrayList<>(); // 关注页面的房间列表
private final List<Room> discoverRooms = new ArrayList<>(); // 发现页面的房间列表
private final List<NearbyUser> nearbyUsers = new ArrayList<>(); // 附近页面的用户列表
private NearbyUsersAdapter nearbyUsersAdapter; // 附近页面的适配器
private StaggeredGridLayoutManager roomsLayoutManager; // 房间列表的布局管理器
private LinearLayoutManager nearbyLayoutManager; // 附近用户列表的布局管理器
private String currentCategory = "推荐";
private String currentTopTab = "发现"; // 当前选中的顶部标签关注发现附近
private CategoryFilterManager filterManager;
private int filterRequestId = 0; // 用于防止旧的筛选结果覆盖新的结果
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable pollRunnable;
@ -69,6 +80,7 @@ public class MainActivity extends AppCompatActivity {
private long lastFetchMs;
private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
private static final int REQUEST_LOCATION_PERMISSION = 201;
private SpeechRecognizer speechRecognizer;
private Intent speechRecognizerIntent;
private boolean isListening = false;
@ -79,30 +91,37 @@ public class MainActivity extends AppCompatActivity {
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 立即显示缓存数据提升启动速度
// 初始化筛选管理器
filterManager = new CategoryFilterManager();
// 恢复上次选中的分类
currentCategory = CategoryFilterManager.getLastCategory(this);
// 设置UI
setupRecyclerView();
setupUI();
loadAvatarFromPrefs();
setupSpeechRecognizer();
// 初始化顶部标签页数据
initializeTopTabData();
// 初始化未读消息数量演示数据
if (UnreadMessageManager.getUnreadCount(this) == 0) {
// 从消息列表计算总未读数量
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
}
// 清除默认选中状态让所有标签页初始显示为未选中样式
// 恢复分类标签选中状态
restoreCategoryTabSelection();
// 设置默认选中"发现"标签页
if (binding != null && binding.topTabs != null) {
// 在布局完成后清除默认选中状态
binding.topTabs.post(() -> {
// 清除所有选中状态
for (int i = 0; i < binding.topTabs.getTabCount(); i++) {
TabLayout.Tab tab = binding.topTabs.getTabAt(i);
if (tab != null && tab.isSelected()) {
// 取消选中但不触发监听器
binding.topTabs.selectTab(null, false);
break;
}
// 选中"发现"标签页索引1
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
if (discoverTab != null) {
discoverTab.select();
}
});
}
@ -209,15 +228,30 @@ public class MainActivity extends AppCompatActivity {
startActivity(intent);
});
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
binding.roomsRecyclerView.setLayoutManager(glm);
// 保存房间列表的布局管理器
roomsLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
roomsLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
// 创建附近用户列表的布局管理器
nearbyLayoutManager = new LinearLayoutManager(this);
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
binding.roomsRecyclerView.setAdapter(adapter);
// 配置RecyclerView动画优化筛选时的过渡效果
androidx.recyclerview.widget.DefaultItemAnimator animator = new androidx.recyclerview.widget.DefaultItemAnimator();
animator.setAddDuration(200); // 添加动画时长
animator.setRemoveDuration(200); // 删除动画时长
animator.setMoveDuration(200); // 移动动画时长
animator.setChangeDuration(200); // 变更动画时长
binding.roomsRecyclerView.setItemAnimator(animator);
// 立即显示演示数据提升用户体验
// 注意如果后续需要从网络加载骨架屏会在fetchRooms中显示
allRooms.clear();
allRooms.addAll(buildDemoRooms(20));
applyCategoryFilter(currentCategory);
// 使用带动画的筛选方法
applyCategoryFilterWithAnimation(currentCategory);
}
private void setupUI() {
@ -249,11 +283,13 @@ public class MainActivity extends AppCompatActivity {
binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器
// 更新布局属性以显示选中样式
binding.topTabs.setTabTextColors(
getResources().getColor(android.R.color.darker_gray, null), // 未选中颜色
getResources().getColor(R.color.purple_500, null) // 选中颜色
ContextCompat.getColor(MainActivity.this, android.R.color.darker_gray), // 未选中颜色
ContextCompat.getColor(MainActivity.this, R.color.purple_500) // 选中颜色
);
CharSequence title = tab.getText();
TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
currentTopTab = title != null ? title.toString() : "发现";
// 切换显示的内容
switchTopTabContent(currentTopTab);
}
@Override
@ -265,7 +301,9 @@ public class MainActivity extends AppCompatActivity {
public void onTabReselected(TabLayout.Tab tab) {
if (tab == null) return;
CharSequence title = tab.getText();
TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
currentTopTab = title != null ? title.toString() : "发现";
// 切换显示的内容
switchTopTabContent(currentTopTab);
}
});
@ -275,7 +313,10 @@ public class MainActivity extends AppCompatActivity {
if (tab == null) return;
CharSequence title = tab.getText();
currentCategory = title != null ? title.toString() : "推荐";
applyCategoryFilter(currentCategory);
// 保存选中的分类
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
// 应用筛选带动画
applyCategoryFilterWithAnimation(currentCategory);
}
@Override
@ -287,7 +328,10 @@ public class MainActivity extends AppCompatActivity {
if (tab == null) return;
CharSequence title = tab.getText();
currentCategory = title != null ? title.toString() : "推荐";
applyCategoryFilter(currentCategory);
// 保存选中的分类
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
// 应用筛选带动画
applyCategoryFilterWithAnimation(currentCategory);
}
});
@ -374,10 +418,14 @@ public class MainActivity extends AppCompatActivity {
// 文本为空显示麦克风图标
binding.micIcon.setImageResource(R.drawable.ic_mic_24);
binding.micIcon.setContentDescription("mic");
// 恢复显示所有房间应用当前分类筛选
applyCategoryFilterWithAnimation(currentCategory);
} else {
// 文本不为空显示搜索图标
binding.micIcon.setImageResource(R.drawable.ic_search_24);
binding.micIcon.setContentDescription("search");
// 实时筛选房间列表
applySearchFilter(text);
}
}
@ -402,6 +450,58 @@ public class MainActivity extends AppCompatActivity {
});
}
/**
* 应用搜索筛选
*/
private void applySearchFilter(String query) {
if (adapter == null || allRooms == null) return;
String searchQuery = query != null ? query.trim().toLowerCase() : "";
if (searchQuery.isEmpty()) {
// 如果搜索框为空恢复显示所有房间应用当前分类筛选
applyCategoryFilterWithAnimation(currentCategory);
return;
}
// 先应用分类筛选再应用搜索筛选
List<Room> categoryFiltered = new ArrayList<>();
if ("全部".equals(currentCategory) || currentCategory == null) {
categoryFiltered.addAll(allRooms);
} else {
for (Room r : allRooms) {
if (r == null) continue;
String roomType = r.getType();
if (currentCategory.equals(roomType)) {
categoryFiltered.add(r);
} else if (currentCategory.equals(getDemoCategoryForRoom(r))) {
categoryFiltered.add(r);
}
}
}
// 在分类筛选结果中应用搜索筛选
List<Room> filtered = new ArrayList<>();
for (Room r : categoryFiltered) {
if (r == null) continue;
String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
if (title.contains(searchQuery) || streamer.contains(searchQuery)) {
filtered.add(r);
}
}
// 更新列表显示
adapter.submitList(filtered, () -> {
binding.roomsRecyclerView.animate()
.alpha(1.0f)
.setDuration(200)
.start();
});
// 更新空状态
updateEmptyStateForList(filtered);
}
private void setupSpeechRecognizer() {
// 检查设备是否支持语音识别
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
@ -586,6 +686,22 @@ public class MainActivity extends AppCompatActivity {
} else {
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
}
} else if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 位置权限已授予显示附近页面
showNearbyTab();
} else {
// 位置权限被拒绝
Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
// 切换回发现页面
if (binding.topTabs != null) {
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
if (discoverTab != null) {
discoverTab.select();
}
}
}
}
}
@ -603,6 +719,12 @@ public class MainActivity extends AppCompatActivity {
speechRecognizer.destroy();
speechRecognizer = null;
}
// 释放筛选管理器资源
if (filterManager != null) {
filterManager.shutdown();
filterManager = null;
}
}
private void loadAvatarFromPrefs() {
@ -645,6 +767,9 @@ public class MainActivity extends AppCompatActivity {
loadAvatarFromPrefs();
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNavigation);
// 确保分类标签选中状态正确防止从其他页面返回时状态不一致
restoreCategoryTabSelection();
}
}
@ -898,17 +1023,23 @@ public class MainActivity extends AppCompatActivity {
hideEmptyState();
hideErrorState();
// 只在没有数据时显示loading
// 只在没有数据时显示骨架屏替代简单的LoadingView
if (adapter.getItemCount() == 0) {
binding.loading.setVisibility(View.VISIBLE);
// 使用骨架屏替代简单的LoadingView提供更好的用户体验
LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6);
binding.loading.setVisibility(View.GONE);
} else {
// 如果有数据显示LoadingView用于下拉刷新等场景
binding.loading.setVisibility(View.GONE);
}
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);
LoadingStateManager.stopRefreshing(binding.swipeRefresh);
isFetching = false;
ApiResponse<List<Room>> body = response.body();
@ -925,14 +1056,19 @@ public class MainActivity extends AppCompatActivity {
allRooms.clear();
allRooms.addAll(rooms);
applyCategoryFilter(currentCategory);
// 确保使用真实的RoomsAdapter替换骨架屏适配器
binding.roomsRecyclerView.setAdapter(adapter);
// 使用带动画的筛选方法
applyCategoryFilterWithAnimation(currentCategory);
// 设置真实数据到适配器自动替换骨架屏
adapter.bumpCoverOffset();
}
@Override
public void onFailure(Call<ApiResponse<List<Room>>> call, Throwable t) {
// 隐藏骨架屏和加载视图
binding.loading.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
LoadingStateManager.stopRefreshing(binding.swipeRefresh);
isFetching = false;
// 显示网络错误Snackbar和空状态
@ -942,7 +1078,11 @@ public class MainActivity extends AppCompatActivity {
// 仍然提供演示数据作为后备
allRooms.clear();
allRooms.addAll(buildDemoRooms(0));
applyCategoryFilter(currentCategory);
// 确保使用真实的RoomsAdapter替换骨架屏适配器
binding.roomsRecyclerView.setAdapter(adapter);
// 使用带动画的筛选方法
applyCategoryFilterWithAnimation(currentCategory);
// 设置真实数据到适配器自动替换骨架屏
adapter.bumpCoverOffset();
}
});
@ -986,23 +1126,140 @@ public class MainActivity extends AppCompatActivity {
hideEmptyState();
}
private void applyCategoryFilter(String category) {
/**
* 应用分类筛选带动画效果
* 使用异步筛选提升性能并添加平滑的过渡动画
* 使用filterRequestId防止旧的筛选结果覆盖新的结果
*/
private void applyCategoryFilterWithAnimation(String category) {
String c = category != null ? category : "推荐";
// 增加请求ID确保只有最新的筛选结果被应用
final int requestId = ++filterRequestId;
// 显示加载状态如果数据量较大
if (allRooms.size() > 50) {
binding.loading.setVisibility(View.VISIBLE);
}
// 使用筛选管理器异步筛选
if (filterManager != null) {
filterManager.filterRoomsAsync(allRooms, c, filteredRooms -> {
// 检查这个结果是否仍然是最新的请求
if (requestId != filterRequestId) {
// 这是一个旧的请求结果忽略它
return;
}
// 隐藏加载状态
binding.loading.setVisibility(View.GONE);
// 添加淡入动画
binding.roomsRecyclerView.animate()
.alpha(0.7f)
.setDuration(100)
.withEndAction(() -> {
// 再次检查请求ID防止在动画期间又有新的筛选请求
if (requestId != filterRequestId) {
return;
}
// 更新列表数据ListAdapter会自动处理DiffUtil动画
adapter.submitList(filteredRooms, () -> {
// 最后一次检查请求ID
if (requestId != filterRequestId) {
return;
}
// 数据更新完成后恢复透明度并添加淡入效果
binding.roomsRecyclerView.animate()
.alpha(1.0f)
.setDuration(200)
.start();
});
// 更新空状态
updateEmptyStateForList(filteredRooms);
})
.start();
});
} else {
// 降级到同步筛选如果筛选管理器未初始化
applyCategoryFilterSync(c);
}
}
/**
* 同步筛选降级方案
*/
private void applyCategoryFilterSync(String category) {
String c = category != null ? category : "推荐";
if ("推荐".equals(c)) {
adapter.submitList(new ArrayList<>(allRooms));
updateEmptyStateForList(allRooms);
// 添加淡入动画保持与其他筛选场景的一致性
binding.roomsRecyclerView.animate()
.alpha(0.7f)
.setDuration(100)
.withEndAction(() -> {
adapter.submitList(new ArrayList<>(allRooms), () -> {
binding.roomsRecyclerView.animate()
.alpha(1.0f)
.setDuration(200)
.start();
});
updateEmptyStateForList(allRooms);
})
.start();
return;
}
List<Room> filtered = new ArrayList<>();
for (Room r : allRooms) {
if (r == null) continue;
String roomType = r.getType();
if (c.equals(roomType)) {
filtered.add(r);
continue;
}
if (c.equals(getDemoCategoryForRoom(r))) {
filtered.add(r);
}
}
adapter.submitList(filtered);
updateEmptyStateForList(filtered);
// 添加淡入动画
binding.roomsRecyclerView.animate()
.alpha(0.7f)
.setDuration(100)
.withEndAction(() -> {
adapter.submitList(filtered, () -> {
binding.roomsRecyclerView.animate()
.alpha(1.0f)
.setDuration(200)
.start();
});
updateEmptyStateForList(filtered);
})
.start();
}
/**
* 恢复分类标签的选中状态
*/
private void restoreCategoryTabSelection() {
if (binding == null || binding.categoryTabs == null) return;
// 延迟执行确保TabLayout已完全初始化
binding.categoryTabs.post(() -> {
for (int i = 0; i < binding.categoryTabs.getTabCount(); i++) {
TabLayout.Tab tab = binding.categoryTabs.getTabAt(i);
if (tab != null) {
CharSequence tabText = tab.getText();
if (tabText != null && currentCategory.equals(tabText.toString())) {
tab.select();
break;
}
}
}
});
}
/**
@ -1115,4 +1372,321 @@ public class MainActivity extends AppCompatActivity {
});
}).start();
}
/**
* 初始化顶部标签页数据
*/
private void initializeTopTabData() {
// 初始化关注页面数据已关注主播的直播
followRooms.clear();
followRooms.addAll(buildFollowRooms());
// 初始化发现页面数据推荐算法
discoverRooms.clear();
discoverRooms.addAll(buildDiscoverRooms());
// 初始化附近页面数据模拟位置数据
nearbyUsers.clear();
nearbyUsers.addAll(buildNearbyUsers());
}
/**
* 切换顶部标签页内容
*/
private void switchTopTabContent(String tabName) {
if (tabName == null) tabName = "发现";
if ("关注".equals(tabName)) {
showFollowTab();
} else if ("发现".equals(tabName)) {
showDiscoverTab();
} else if ("附近".equals(tabName)) {
showNearbyTab();
} else {
// 默认显示发现页面
showDiscoverTab();
}
}
/**
* 显示关注页面
*/
private void showFollowTab() {
// 隐藏分类标签关注页面不需要分类筛选
if (binding.categoryTabs != null) {
binding.categoryTabs.setVisibility(View.GONE);
}
// 恢复房间列表的布局管理器和适配器
if (binding.roomsRecyclerView != null) {
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
binding.roomsRecyclerView.setAdapter(adapter);
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
}
// 使用房间适配器显示关注的主播直播
if (adapter != null) {
adapter.submitList(new ArrayList<>(followRooms));
}
// 更新空状态
if (followRooms.isEmpty()) {
if (binding.emptyStateView != null) {
binding.emptyStateView.setIcon(R.drawable.ic_person_24);
binding.emptyStateView.setTitle("还没有关注的主播");
binding.emptyStateView.setMessage("去发现页关注喜欢的主播吧");
binding.emptyStateView.hideActionButton();
binding.emptyStateView.setVisibility(View.VISIBLE);
}
} else {
hideEmptyState();
}
}
/**
* 显示发现页面
*/
private void showDiscoverTab() {
// 显示分类标签
if (binding.categoryTabs != null) {
binding.categoryTabs.setVisibility(View.VISIBLE);
}
// 恢复房间列表的布局管理器和适配器
if (binding.roomsRecyclerView != null) {
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
binding.roomsRecyclerView.setAdapter(adapter);
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
}
// 使用房间适配器显示推荐内容
if (adapter != null) {
// 先显示所有推荐房间然后应用分类筛选
allRooms.clear();
allRooms.addAll(discoverRooms);
applyCategoryFilterWithAnimation(currentCategory);
}
// 更新空状态
updateEmptyStateForList(allRooms);
}
/**
* 显示附近页面
*/
private void showNearbyTab() {
// 检查位置权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 请求位置权限
requestLocationPermission();
return;
}
// 隐藏分类标签附近页面不需要分类筛选
if (binding.categoryTabs != null) {
binding.categoryTabs.setVisibility(View.GONE);
}
// 初始化附近用户适配器如果还没有
if (nearbyUsersAdapter == null) {
nearbyUsersAdapter = new NearbyUsersAdapter(user -> {
if (user == null) return;
// 点击附近用户可以跳转到用户主页或显示详情
Toast.makeText(MainActivity.this, "点击了:" + user.getName(), Toast.LENGTH_SHORT).show();
});
}
// 切换RecyclerView的布局管理器和适配器显示附近用户列表
if (binding.roomsRecyclerView != null) {
binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
}
// 更新空状态
if (nearbyUsers.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 List<Room> buildFollowRooms() {
List<Room> list = new ArrayList<>();
// 从FollowingListActivity获取已关注的主播列表
// 这里使用模拟数据实际应该从数据库或API获取
String[][] followData = {
{"王者荣耀排位赛", "王者荣耀陪练", "游戏", "true"},
{"音乐电台", "音乐电台", "音乐", "false"},
{"户外直播", "户外阿杰", "户外", "true"},
{"美食探店", "美食探店", "美食", "false"},
{"聊天连麦", "聊天小七", "聊天", "true"},
{"才艺表演", "才艺小妹", "才艺", "true"},
{"游戏竞技", "游戏高手", "游戏", "true"},
{"音乐演奏", "音乐达人", "音乐", "false"}
};
for (int i = 0; i < followData.length; i++) {
String id = "follow-" + i;
String title = followData[i][0];
String streamer = followData[i][1];
String type = followData[i][2];
boolean live = Boolean.parseBoolean(followData[i][3]);
Room room = new Room(id, title, streamer, live);
room.setType(type);
list.add(room);
}
return list;
}
/**
* 构建发现页面的房间列表推荐算法前端实现
*/
private List<Room> buildDiscoverRooms() {
List<Room> list = new ArrayList<>();
// 推荐算法基于观看历史点赞等模拟数据
// 这里实现一个简单的推荐算法
// 1. 优先推荐正在直播的房间
// 2. 优先推荐热门类型游戏才艺音乐
// 3. 添加一些随机性
String[][] discoverData = {
{"王者荣耀排位赛", "小明选手", "游戏", "true"},
{"吃鸡大逃杀", "游戏高手", "游戏", "true"},
{"唱歌连麦", "音乐达人", "音乐", "true"},
{"户外直播", "旅行者", "户外", "false"},
{"美食制作", "厨神小李", "美食", "true"},
{"才艺表演", "舞蹈小妹", "才艺", "true"},
{"聊天交友", "暖心姐姐", "聊天", "false"},
{"LOL竞技场", "电竞选手", "游戏", "true"},
{"古风演奏", "琴师小王", "音乐", "true"},
{"健身教学", "教练张", "户外", "false"},
{"摄影分享", "摄影师", "户外", "true"},
{"宠物秀", "萌宠主播", "才艺", "true"},
{"编程教学", "码农老王", "聊天", "false"},
{"读书分享", "书虫小妹", "聊天", "true"},
{"手工制作", "手艺人", "才艺", "true"},
{"英语口语", "外教老师", "聊天", "false"},
{"魔术表演", "魔术师", "才艺", "true"},
{"街头访谈", "记者小张", "户外", "true"},
{"乐器教学", "音乐老师", "音乐", "false"},
{"电影解说", "影评人", "聊天", "true"},
{"游戏攻略", "游戏解说", "游戏", "true"},
{"K歌大赛", "K歌达人", "音乐", "true"},
{"美食探店", "美食博主", "美食", "true"},
{"舞蹈教学", "舞蹈老师", "才艺", "true"}
};
// 推荐算法优先显示正在直播的然后按类型排序
List<Room> liveRooms = new ArrayList<>();
List<Room> offlineRooms = new ArrayList<>();
for (int i = 0; i < discoverData.length; i++) {
String id = "discover-" + i;
String title = discoverData[i][0];
String streamer = discoverData[i][1];
String type = discoverData[i][2];
boolean live = Boolean.parseBoolean(discoverData[i][3]);
Room room = new Room(id, title, streamer, live);
room.setType(type);
if (live) {
liveRooms.add(room);
} else {
offlineRooms.add(room);
}
}
// 先添加正在直播的再添加未直播的
list.addAll(liveRooms);
list.addAll(offlineRooms);
return list;
}
/**
* 构建附近页面的用户列表使用模拟位置数据
*/
private List<NearbyUser> buildNearbyUsers() {
List<NearbyUser> list = new ArrayList<>();
// 模拟位置数据生成不同距离的用户
String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰",
"小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"};
for (int i = 0; i < names.length; i++) {
String id = "nearby-user-" + i;
String name = names[i];
boolean live = i % 3 == 0; // 每3个用户中有一个在直播
String distanceText;
if (i < 3) {
distanceText = (300 + i * 120) + "m";
} else if (i < 10) {
float km = 0.8f + (i - 3) * 0.35f;
distanceText = String.format("%.1fkm", km);
} else {
float km = 3.5f + (i - 10) * 0.5f;
distanceText = String.format("%.1fkm", km);
}
list.add(new NearbyUser(id, name, distanceText, live));
}
return list;
}
/**
* 请求位置权限
*/
private void requestLocationPermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
new AlertDialog.Builder(this)
.setTitle("需要位置权限")
.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。")
.setPositiveButton("确定", (dialog, which) -> {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
},
REQUEST_LOCATION_PERMISSION);
})
.setNegativeButton("取消", (dialog, which) -> {
// 用户拒绝权限显示提示
Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
// 切换回发现页面
if (binding.topTabs != null) {
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
if (discoverTab != null) {
discoverTab.select();
}
}
})
.show();
} else {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
},
REQUEST_LOCATION_PERMISSION);
}
}
}

View File

@ -0,0 +1,140 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Switch;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityNotificationSettingsBinding;
import java.util.ArrayList;
import java.util.List;
/**
* 通知设置页面
* 支持各类通知开关和免打扰设置
*/
public class NotificationSettingsActivity extends AppCompatActivity {
private static final String PREFS_NAME = "notification_settings";
private static final String KEY_SYSTEM_NOTIFICATIONS = "system_notifications";
private static final String KEY_FOLLOW_NOTIFICATIONS = "follow_notifications";
private static final String KEY_COMMENT_NOTIFICATIONS = "comment_notifications";
private static final String KEY_MESSAGE_NOTIFICATIONS = "message_notifications";
private static final String KEY_LIVE_NOTIFICATIONS = "live_notifications";
private static final String KEY_DND_ENABLED = "dnd_enabled";
private static final String KEY_DND_START_HOUR = "dnd_start_hour";
private static final String KEY_DND_END_HOUR = "dnd_end_hour";
private ActivityNotificationSettingsBinding binding;
private MoreAdapter adapter;
private SharedPreferences prefs;
public static void start(Context context) {
Intent intent = new Intent(context, NotificationSettingsActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityNotificationSettingsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
binding.backButton.setOnClickListener(v -> finish());
binding.titleText.setText("通知设置");
adapter = new MoreAdapter(item -> {
if (item == null) return;
if (item.getType() != MoreItem.Type.ROW) return;
handleItemClick(item);
});
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerView.setAdapter(adapter);
refreshItems();
}
private void handleItemClick(MoreItem item) {
String title = item.getTitle() != null ? item.getTitle() : "";
if ("免打扰".equals(title)) {
showDoNotDisturbDialog();
}
}
private void refreshItems() {
List<MoreItem> items = new ArrayList<>();
// 系统通知开关
items.add(MoreItem.section("系统通知"));
boolean systemEnabled = prefs.getBoolean(KEY_SYSTEM_NOTIFICATIONS, true);
items.add(MoreItem.row("系统通知", systemEnabled ? "已开启" : "已关闭", R.drawable.ic_notifications_24));
// 消息提醒
items.add(MoreItem.section("消息提醒"));
boolean followEnabled = prefs.getBoolean(KEY_FOLLOW_NOTIFICATIONS, true);
boolean commentEnabled = prefs.getBoolean(KEY_COMMENT_NOTIFICATIONS, true);
boolean messageEnabled = prefs.getBoolean(KEY_MESSAGE_NOTIFICATIONS, true);
boolean liveEnabled = prefs.getBoolean(KEY_LIVE_NOTIFICATIONS, true);
items.add(MoreItem.row("关注提醒", followEnabled ? "已开启" : "已关闭", R.drawable.ic_people_24));
items.add(MoreItem.row("评论提醒", commentEnabled ? "已开启" : "已关闭", R.drawable.ic_chat_24));
items.add(MoreItem.row("私信提醒", messageEnabled ? "已开启" : "已关闭", R.drawable.ic_chat_24));
items.add(MoreItem.row("开播提醒", liveEnabled ? "已开启" : "已关闭", R.drawable.ic_notifications_24));
// 免打扰
items.add(MoreItem.section("免打扰"));
boolean dndEnabled = prefs.getBoolean(KEY_DND_ENABLED, false);
if (dndEnabled) {
int startHour = prefs.getInt(KEY_DND_START_HOUR, 22);
int endHour = prefs.getInt(KEY_DND_END_HOUR, 8);
items.add(MoreItem.row("免打扰", String.format("%02d:00 - %02d:00", startHour, endHour), R.drawable.ic_notifications_24));
} else {
items.add(MoreItem.row("免打扰", "未开启", R.drawable.ic_notifications_24));
}
adapter.submitList(items);
}
private void showDoNotDisturbDialog() {
// 简单的免打扰设置对话框
boolean currentEnabled = prefs.getBoolean(KEY_DND_ENABLED, false);
int startHour = prefs.getInt(KEY_DND_START_HOUR, 22);
int endHour = prefs.getInt(KEY_DND_END_HOUR, 8);
String message = "免打扰功能说明:\n\n" +
"• 开启后,在指定时间段内不会收到通知\n" +
"• 当前设置:" + (currentEnabled ?
String.format("已开启 (%02d:00 - %02d:00)", startHour, endHour) : "未开启") + "\n" +
"• 紧急通知仍会显示\n\n" +
"(此功能待完善)";
new android.app.AlertDialog.Builder(this)
.setTitle("免打扰设置")
.setMessage(message)
.setPositiveButton("开启", (dialog, which) -> {
prefs.edit().putBoolean(KEY_DND_ENABLED, true).apply();
refreshItems();
Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show();
})
.setNeutralButton(currentEnabled ? "关闭" : "取消", (dialog, which) -> {
if (currentEnabled) {
prefs.edit().putBoolean(KEY_DND_ENABLED, false).apply();
refreshItems();
Toast.makeText(this, "免打扰已关闭", Toast.LENGTH_SHORT).show();
}
})
.show();
}
}

View File

@ -1,9 +1,11 @@
package com.example.livestreaming;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.net.Uri;
@ -14,14 +16,20 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import com.bumptech.glide.Glide;
import com.example.livestreaming.BuildConfig;
import com.example.livestreaming.databinding.ActivityProfileBinding;
import com.example.livestreaming.ShareUtils;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@ -47,6 +55,10 @@ public class ProfileActivity extends AppCompatActivity {
private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加";
private ActivityResultLauncher<Intent> editProfileLauncher;
private ActivityResultLauncher<String> pickImageLauncher;
private ActivityResultLauncher<Uri> takePictureLauncher;
private ActivityResultLauncher<String> requestCameraPermissionLauncher;
private Uri pendingCameraUri = null;
public static void start(Context context) {
Intent intent = new Intent(context, ProfileActivity.class);
@ -70,6 +82,45 @@ public class ProfileActivity extends AppCompatActivity {
}
);
// 注册图片选择器
pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {
if (uri == null) return;
// 保存头像URI到SharedPreferences
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
.putString(KEY_AVATAR_URI, uri.toString())
.remove(KEY_AVATAR_RES) // 清除资源ID因为现在使用URI
.apply();
// 立即更新头像显示
Glide.with(this).load(uri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar);
Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show();
});
// 注册拍照器
takePictureLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> {
if (!success) return;
if (pendingCameraUri == null) return;
// 保存头像URI到SharedPreferences
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
.putString(KEY_AVATAR_URI, pendingCameraUri.toString())
.remove(KEY_AVATAR_RES) // 清除资源ID因为现在使用URI
.apply();
// 立即更新头像显示
Glide.with(this).load(pendingCameraUri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar);
Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show();
pendingCameraUri = null;
});
// 注册相机权限请求
requestCameraPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
if (!granted) {
Toast.makeText(this, "需要相机权限才能拍照", Toast.LENGTH_SHORT).show();
pendingCameraUri = null;
return;
}
if (pendingCameraUri == null) return;
takePictureLauncher.launch(pendingCameraUri);
});
loadProfileFromPrefs();
loadAndDisplayTags();
loadProfileInfo();
@ -172,30 +223,59 @@ public class ProfileActivity extends AppCompatActivity {
private void setupAvatarClick() {
binding.avatar.setOnClickListener(v -> {
AvatarViewerDialog dialog = AvatarViewerDialog.create(this);
// 优先从SharedPreferences读取最新的头像信息因为ImageView可能还在加载中
String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null);
int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0);
if (!TextUtils.isEmpty(avatarUri)) {
// 使用URI加载确保能正确显示
dialog.setAvatarUri(Uri.parse(avatarUri));
} else if (avatarRes != 0) {
dialog.setAvatarResId(avatarRes);
} else {
// 如果都没有尝试从ImageView获取Drawable
Drawable drawable = binding.avatar.getDrawable();
if (drawable != null) {
dialog.setAvatarDrawable(drawable);
} else {
dialog.setAvatarResId(R.drawable.ic_account_circle_24);
}
}
dialog.show();
// 显示头像选择底部菜单
showAvatarBottomSheet();
});
}
private void showAvatarBottomSheet() {
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.bottom_sheet_avatar_picker, null);
dialog.setContentView(view);
View pick = view.findViewById(R.id.actionPickGallery);
View camera = view.findViewById(R.id.actionTakePhoto);
View cancel = view.findViewById(R.id.actionCancel);
pick.setOnClickListener(v -> {
dialog.dismiss();
pickImageLauncher.launch("image/*");
});
camera.setOnClickListener(v -> {
dialog.dismiss();
Uri uri = createTempCameraUri();
if (uri == null) return;
pendingCameraUri = uri;
ensureCameraPermissionAndTakePhoto();
});
cancel.setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
private void ensureCameraPermissionAndTakePhoto() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
if (pendingCameraUri == null) return;
takePictureLauncher.launch(pendingCameraUri);
return;
}
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA);
}
private Uri createTempCameraUri() {
try {
File dir = new File(getCacheDir(), "images");
if (!dir.exists()) dir.mkdirs();
File file = new File(dir, "avatar_" + System.currentTimeMillis() + ".jpg");
return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file);
} catch (Exception e) {
Toast.makeText(this, "无法创建相机文件", Toast.LENGTH_SHORT).show();
return null;
}
}
private void setupNavigationClicks() {
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this));
@ -225,20 +305,7 @@ public class ProfileActivity extends AppCompatActivity {
Intent intent = new Intent(this, EditProfileActivity.class);
editProfileLauncher.launch(intent);
});
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.shareHome.setOnClickListener(v -> showShareProfileDialog());
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
}
@ -461,4 +528,20 @@ public class ProfileActivity extends AppCompatActivity {
return "";
}
}
/**
* 显示分享个人主页对话框
*/
private void showShareProfileDialog() {
// 获取用户ID
String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
if (TextUtils.isEmpty(digits)) {
digits = "24187196"; // 默认ID
}
// 直接生成分享链接
String shareLink = ShareUtils.generateProfileShareLink(digits);
ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧");
}
}

View File

@ -1,8 +1,13 @@
package com.example.livestreaming;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
@ -25,6 +30,8 @@ public class SettingsPageActivity extends AppCompatActivity {
public static final String PAGE_ABOUT = "about";
private ActivitySettingsPageBinding binding;
private MoreAdapter adapter;
private String currentPage = "";
public static void start(Context context, String page) {
Intent intent = new Intent(context, SettingsPageActivity.class);
@ -40,23 +47,152 @@ public class SettingsPageActivity extends AppCompatActivity {
binding.backButton.setOnClickListener(v -> finish());
String page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
if (page == null) page = "";
String pageExtra = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
currentPage = pageExtra != null ? pageExtra : "";
String title = resolveTitle(page);
String title = resolveTitle(currentPage);
binding.titleText.setText(title);
MoreAdapter adapter = new MoreAdapter(item -> {
adapter = new MoreAdapter(item -> {
if (item == null) return;
if (item.getType() != MoreItem.Type.ROW) return;
String t = item.getTitle() != null ? item.getTitle() : "";
Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show();
handleItemClick(item);
});
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerView.setAdapter(adapter);
adapter.submitList(buildItems(page));
refreshItems();
}
private void handleItemClick(MoreItem item) {
String title = item.getTitle() != null ? item.getTitle() : "";
switch (currentPage) {
case PAGE_ACCOUNT_SECURITY:
handleAccountSecurityClick(title);
break;
case PAGE_PRIVACY:
handlePrivacyClick(title);
break;
case PAGE_NOTIFICATIONS:
handleNotificationsClick(title);
break;
case PAGE_CLEAR_CACHE:
handleClearCacheClick(title);
break;
case PAGE_HELP:
handleHelpClick(title);
break;
case PAGE_ABOUT:
handleAboutClick(title);
break;
}
}
private void handleAccountSecurityClick(String title) {
if ("修改密码".equals(title)) {
showChangePasswordDialog();
} else if ("绑定手机号".equals(title)) {
showBindPhoneDialog();
} else if ("登录设备管理".equals(title)) {
showDeviceManagementDialog();
}
}
private void handlePrivacyClick(String title) {
if ("黑名单".equals(title)) {
showBlacklistDialog();
} else if ("权限管理".equals(title)) {
showPermissionManagementDialog();
} else if ("隐私政策".equals(title)) {
showPrivacyPolicyDialog();
}
}
private void handleNotificationsClick(String title) {
if ("系统通知".equals(title) || "免打扰".equals(title)) {
NotificationSettingsActivity.start(this);
}
}
private void handleClearCacheClick(String title) {
if ("缓存大小".equals(title)) {
showClearAllCacheDialog();
} else if ("图片缓存".equals(title)) {
showClearImageCacheDialog();
}
}
private void handleHelpClick(String title) {
if ("常见问题".equals(title)) {
showFAQDialog();
} else if ("意见反馈".equals(title)) {
showFeedbackDialog();
} else if ("联系客服".equals(title)) {
showCustomerServiceDialog();
}
}
private void handleAboutClick(String title) {
if ("版本".equals(title)) {
// 版本信息已在subtitle中显示点击可显示详细信息
showVersionInfoDialog();
} else if ("用户协议".equals(title)) {
showUserAgreementDialog();
} else if ("隐私政策".equals(title)) {
showPrivacyPolicyDialog();
}
}
private void refreshItems() {
if (PAGE_CLEAR_CACHE.equals(currentPage)) {
// 异步加载缓存大小
updateCacheSize();
} else if (PAGE_ABOUT.equals(currentPage)) {
// 更新版本信息
updateVersionInfo();
} else {
adapter.submitList(buildItems(currentPage));
}
}
private void updateCacheSize() {
new Thread(() -> {
long cacheSize = CacheManager.getCacheSize(this);
String sizeText = CacheManager.formatCacheSize(this, cacheSize);
runOnUiThread(() -> {
List<MoreItem> items = new ArrayList<>();
items.add(MoreItem.section("存储"));
items.add(MoreItem.row("缓存大小", sizeText, R.drawable.ic_grid_24));
items.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
adapter.submitList(items);
});
}).start();
}
private void updateVersionInfo() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
String versionName = packageInfo.versionName;
int versionCode = packageInfo.versionCode;
String versionText = "Live Streaming " + versionName + " (Build " + versionCode + ")";
List<MoreItem> items = new ArrayList<>();
items.add(MoreItem.section("应用信息"));
items.add(MoreItem.row("版本", versionText, R.drawable.ic_menu_24));
items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
adapter.submitList(items);
} catch (PackageManager.NameNotFoundException e) {
List<MoreItem> items = new ArrayList<>();
items.add(MoreItem.section("应用信息"));
items.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
adapter.submitList(items);
}
}
private String resolveTitle(String page) {
@ -105,29 +241,344 @@ public class SettingsPageActivity extends AppCompatActivity {
}
if (PAGE_CLEAR_CACHE.equals(page)) {
list.add(MoreItem.section("存储"));
list.add(MoreItem.row("缓存大小", "点击清理缓存(演示)", R.drawable.ic_grid_24));
list.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
return list;
// 缓存大小将在updateCacheSize中异步更新
return new ArrayList<>();
}
if (PAGE_HELP.equals(page)) {
list.add(MoreItem.section("帮助"));
list.add(MoreItem.row("常见问题", "问题解答与使用指南", R.drawable.ic_chat_24));
list.add(MoreItem.row("意见反馈", "提交你的建议与问题", R.drawable.ic_chat_24));
list.add(MoreItem.row("联系客服", "在线客服(演示)", R.drawable.ic_chat_24));
list.add(MoreItem.row("联系客服", "在线客服", R.drawable.ic_chat_24));
return list;
}
if (PAGE_ABOUT.equals(page)) {
list.add(MoreItem.section("应用信息"));
list.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
list.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
list.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
return list;
// 版本信息将在updateVersionInfo中更新
return new ArrayList<>();
}
list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24));
return list;
}
// ========== 账号与安全相关对话框 ==========
private void showChangePasswordDialog() {
try {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null);
new AlertDialog.Builder(this)
.setTitle("修改密码")
.setView(dialogView)
.setPositiveButton("确定", (dialog, which) -> {
Toast.makeText(this, "密码修改功能待接入后端", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("取消", null)
.show();
} catch (Exception e) {
// 如果布局文件不存在使用简单对话框
new AlertDialog.Builder(this)
.setTitle("修改密码")
.setMessage("密码修改功能待接入后端\n\n" +
"功能说明:\n" +
"• 需要输入当前密码\n" +
"• 设置新密码至少8位\n" +
"• 确认新密码")
.setPositiveButton("确定", null)
.show();
}
}
private void showBindPhoneDialog() {
try {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_bind_phone, null);
new AlertDialog.Builder(this)
.setTitle("绑定手机号")
.setView(dialogView)
.setPositiveButton("确定", (dialog, which) -> {
Toast.makeText(this, "手机号绑定功能待接入后端", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("取消", null)
.show();
} catch (Exception e) {
// 如果布局文件不存在使用简单对话框
new AlertDialog.Builder(this)
.setTitle("绑定手机号")
.setMessage("手机号绑定功能待接入后端\n\n" +
"功能说明:\n" +
"• 输入手机号码\n" +
"• 获取并输入验证码\n" +
"• 完成绑定")
.setPositiveButton("确定", null)
.show();
}
}
private void showDeviceManagementDialog() {
new AlertDialog.Builder(this)
.setTitle("登录设备管理")
.setMessage("当前登录设备:\n\n" +
"• Android设备 (当前设备)\n" +
" 最后登录:刚刚\n\n" +
"功能说明:\n" +
"• 查看所有已登录设备\n" +
"• 可以远程退出其他设备\n" +
"• 保护账号安全")
.setPositiveButton("确定", null)
.show();
}
// ========== 隐私设置相关对话框 ==========
private void showBlacklistDialog() {
new AlertDialog.Builder(this)
.setTitle("黑名单")
.setMessage("黑名单功能说明:\n\n" +
"• 将用户加入黑名单后,对方将无法:\n" +
" - 查看你的动态\n" +
" - 给你发送消息\n" +
" - 关注你\n\n" +
"• 你仍然可以查看对方的公开信息\n\n" +
"当前黑名单0人\n\n" +
"(此功能待接入后端)")
.setPositiveButton("确定", null)
.show();
}
private void showPermissionManagementDialog() {
new AlertDialog.Builder(this)
.setTitle("权限管理")
.setMessage("应用权限说明:\n\n" +
"• 相机权限:用于直播和拍照\n" +
"• 麦克风权限:用于语音搜索和直播\n" +
"• 位置权限:用于附近的人功能\n" +
"• 存储权限:用于保存图片和文件\n\n" +
"你可以在系统设置中管理这些权限。")
.setPositiveButton("打开系统设置", (dialog, which) -> {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(android.net.Uri.parse("package:" + getPackageName()));
startActivity(intent);
})
.setNegativeButton("取消", null)
.show();
}
private void showPrivacyPolicyDialog() {
new AlertDialog.Builder(this)
.setTitle("隐私政策")
.setMessage("隐私政策\n\n" +
"我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息。\n\n" +
"1. 信息收集\n" +
"我们可能收集以下信息:\n" +
"• 账户信息(昵称、头像等)\n" +
"• 设备信息(设备型号、系统版本等)\n" +
"• 使用信息(观看记录、互动记录等)\n\n" +
"2. 信息使用\n" +
"我们使用收集的信息用于:\n" +
"• 提供和改进服务\n" +
"• 个性化推荐\n" +
"• 安全保障\n\n" +
"3. 信息保护\n" +
"我们采用行业标准的安全措施保护您的信息。\n\n" +
"4. 联系我们\n" +
"如有疑问,请联系客服。\n\n" +
"(完整版隐私政策待接入)")
.setPositiveButton("确定", null)
.show();
}
// ========== 缓存清理相关对话框 ==========
private void showClearAllCacheDialog() {
new AlertDialog.Builder(this)
.setTitle("清理缓存")
.setMessage("确定要清理所有缓存吗?\n\n" +
"清理后可能需要重新加载部分内容。")
.setPositiveButton("清理", (dialog, which) -> {
showClearingProgress();
CacheManager.clearAllCache(this, new CacheManager.OnCacheClearListener() {
@Override
public void onSuccess(long clearedSize) {
hideClearingProgress();
String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize);
Toast.makeText(SettingsPageActivity.this,
"清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show();
updateCacheSize();
}
@Override
public void onError(Exception e) {
hideClearingProgress();
Toast.makeText(SettingsPageActivity.this,
"清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("取消", null)
.show();
}
private void showClearImageCacheDialog() {
new AlertDialog.Builder(this)
.setTitle("清理图片缓存")
.setMessage("确定要清理图片缓存吗?\n\n" +
"清理后图片需要重新加载。")
.setPositiveButton("清理", (dialog, which) -> {
showClearingProgress();
CacheManager.clearImageCache(this, new CacheManager.OnCacheClearListener() {
@Override
public void onSuccess(long clearedSize) {
hideClearingProgress();
String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize);
Toast.makeText(SettingsPageActivity.this,
"清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show();
updateCacheSize();
}
@Override
public void onError(Exception e) {
hideClearingProgress();
Toast.makeText(SettingsPageActivity.this,
"清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
})
.setNegativeButton("取消", null)
.show();
}
private AlertDialog clearingDialog;
private void showClearingProgress() {
ProgressBar progressBar = new ProgressBar(this);
progressBar.setIndeterminate(true);
clearingDialog = new AlertDialog.Builder(this)
.setTitle("正在清理...")
.setView(progressBar)
.setCancelable(false)
.show();
}
private void hideClearingProgress() {
if (clearingDialog != null && clearingDialog.isShowing()) {
clearingDialog.dismiss();
clearingDialog = null;
}
}
// ========== 帮助与反馈相关对话框 ==========
private void showFAQDialog() {
new AlertDialog.Builder(this)
.setTitle("常见问题")
.setMessage("常见问题解答\n\n" +
"Q: 如何创建直播间?\n" +
"A: 在首页点击右上角的创建按钮,填写直播间信息即可。\n\n" +
"Q: 如何关注主播?\n" +
"A: 在直播间或主播个人主页点击关注按钮。\n\n" +
"Q: 如何发送消息?\n" +
"A: 在消息页面选择会话,输入内容后发送。\n\n" +
"Q: 如何修改个人资料?\n" +
"A: 在个人中心点击编辑资料按钮。\n\n" +
"Q: 如何清理缓存?\n" +
"A: 在设置页面选择清理缓存。\n\n" +
"更多问题请联系客服。")
.setPositiveButton("确定", null)
.show();
}
private void showFeedbackDialog() {
try {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_feedback, null);
new AlertDialog.Builder(this)
.setTitle("意见反馈")
.setView(dialogView)
.setPositiveButton("提交", (dialog, which) -> {
Toast.makeText(this, "感谢您的反馈!我们会认真处理。", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("取消", null)
.show();
} catch (Exception e) {
// 如果布局文件不存在使用简单对话框
new AlertDialog.Builder(this)
.setTitle("意见反馈")
.setMessage("意见反馈功能待接入后端\n\n" +
"你可以通过以下方式反馈:\n" +
"• 在对话框中输入反馈内容\n" +
"• 选择反馈类型(问题/建议)\n" +
"• 提交后我们会及时处理")
.setPositiveButton("确定", null)
.show();
}
}
private void showCustomerServiceDialog() {
new AlertDialog.Builder(this)
.setTitle("联系客服")
.setMessage("客服联系方式:\n\n" +
"• 在线客服:工作日 9:00-18:00\n" +
"• 客服邮箱support@livestreaming.com\n" +
"• 客服电话400-123-4567\n\n" +
"(此功能待接入后端)")
.setPositiveButton("确定", null)
.show();
}
// ========== 关于页面相关对话框 ==========
private void showVersionInfoDialog() {
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
String versionName = packageInfo.versionName;
int versionCode = packageInfo.versionCode;
new AlertDialog.Builder(this)
.setTitle("版本信息")
.setMessage("应用名称Live Streaming\n" +
"版本号:" + versionName + "\n" +
"构建号:" + versionCode + "\n" +
"更新时间2024年\n\n" +
"© 2024 Live Streaming. All rights reserved.")
.setPositiveButton("确定", null)
.show();
} catch (PackageManager.NameNotFoundException e) {
new AlertDialog.Builder(this)
.setTitle("版本信息")
.setMessage("应用名称Live Streaming\n" +
"版本号1.0\n\n" +
"© 2024 Live Streaming. All rights reserved.")
.setPositiveButton("确定", null)
.show();
}
}
private void showUserAgreementDialog() {
new AlertDialog.Builder(this)
.setTitle("用户协议")
.setMessage("用户服务协议\n\n" +
"欢迎使用Live Streaming直播应用。在使用本应用前请仔细阅读以下条款。\n\n" +
"1. 服务条款\n" +
"使用本应用即表示您同意遵守本协议的所有条款。\n\n" +
"2. 用户行为规范\n" +
"• 不得发布违法违规内容\n" +
"• 不得进行欺诈、骚扰等行为\n" +
"• 尊重他人,文明互动\n\n" +
"3. 知识产权\n" +
"本应用的所有内容受知识产权法保护。\n\n" +
"4. 免责声明\n" +
"用户需自行承担使用本应用的风险。\n\n" +
"5. 协议变更\n" +
"我们保留随时修改本协议的权利。\n\n" +
"(完整版用户协议待接入)")
.setPositiveButton("确定", null)
.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (clearingDialog != null && clearingDialog.isShowing()) {
clearingDialog.dismiss();
}
}
}

View File

@ -0,0 +1,62 @@
package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
/**
* 分享工具类
* 提供系统分享功能
*/
public class ShareUtils {
/**
* 生成个人主页分享链接
*/
public static String generateProfileShareLink(String userId) {
return "https://livestreaming.com/profile/" + userId;
}
/**
* 生成直播间分享链接
*/
public static String generateRoomShareLink(String roomId) {
return "https://livestreaming.com/room/" + roomId;
}
/**
* 分享链接使用系统分享菜单
*/
public static void shareLink(Context context, String link, String title, String text) {
if (context == null || link == null) return;
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title != null ? title : "分享");
shareIntent.putExtra(Intent.EXTRA_TEXT, (text != null ? text + "\n" : "") + link);
try {
context.startActivity(Intent.createChooser(shareIntent, "分享到"));
} catch (Exception e) {
// 如果分享失败忽略异常
}
}
/**
* 分享文本
*/
public static void shareText(Context context, String text, String title) {
if (context == null || text == null) return;
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title != null ? title : "分享");
shareIntent.putExtra(Intent.EXTRA_TEXT, text);
try {
context.startActivity(Intent.createChooser(shareIntent, "分享到"));
} catch (Exception e) {
// 如果分享失败忽略异常
}
}
}

View File

@ -33,6 +33,9 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private SearchSuggestionsAdapter suggestionsAdapter;
private final List<Room> discoverAllRooms = new ArrayList<>();
private RoomsAdapter followRoomsAdapter;
private final List<Room> followAllRooms = new ArrayList<>();
private BadgesAdapter badgesAdapter;
private MoreAdapter moreAdapter;
@ -40,6 +43,18 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private NearbyUsersAdapter addFriendAdapter;
private final List<NearbyUser> addFriendAllUsers = new ArrayList<>();
/**
* 设置关注容器的可见性
*/
private void setFollowContainerVisibility(int visibility) {
View followContainer = binding.getRoot().findViewById(R.id.followContainer);
if (followContainer != null) {
followContainer.setVisibility(visibility);
} else {
binding.followRecyclerView.setVisibility(visibility);
}
}
public static void start(Context context, String title) {
Intent intent = new Intent(context, TabPlaceholderActivity.class);
intent.putExtra(EXTRA_TITLE, title);
@ -94,7 +109,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.genericScroll.setVisibility(View.VISIBLE);
binding.discoverContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.VISIBLE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
}
@ -106,7 +121,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureDiscoverSuggestions();
@ -204,10 +219,23 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private void showFollowRooms() {
binding.genericScroll.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.VISIBLE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
RoomsAdapter adapter = new RoomsAdapter(room -> {
// 显示关注容器包含搜索框和列表
setFollowContainerVisibility(View.VISIBLE);
ensureFollowRoomsAdapter();
// 初始化数据
followAllRooms.clear();
followAllRooms.addAll(buildFollowDemoRooms(16));
followRoomsAdapter.submitList(new ArrayList<>(followAllRooms));
}
private void ensureFollowRoomsAdapter() {
if (followRoomsAdapter != null) return;
followRoomsAdapter = new RoomsAdapter(room -> {
if (room == null) return;
Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class);
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
@ -217,14 +245,76 @@ public class TabPlaceholderActivity extends AppCompatActivity {
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
binding.followRecyclerView.setLayoutManager(glm);
binding.followRecyclerView.setAdapter(adapter);
binding.followRecyclerView.setAdapter(followRoomsAdapter);
adapter.submitList(buildFollowDemoRooms(16));
// 设置搜索框
setupFollowSearchBox();
}
private void setupFollowSearchBox() {
android.widget.EditText searchInput = binding.getRoot().findViewById(R.id.followSearchInput);
View clearButton = binding.getRoot().findViewById(R.id.followSearchClear);
if (searchInput == null) return;
// 添加文本监听
searchInput.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String text = s != null ? s.toString() : "";
// 显示/隐藏清空按钮
if (clearButton != null) {
clearButton.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE);
}
// 应用筛选
applyFollowFilter(text);
}
@Override
public void afterTextChanged(Editable s) {
}
});
// 清空按钮点击事件
if (clearButton != null) {
clearButton.setOnClickListener(v -> {
if (searchInput != null) {
searchInput.setText("");
searchInput.requestFocus();
}
});
}
}
private void applyFollowFilter(String query) {
if (followRoomsAdapter == null || followAllRooms == null) return;
String searchQuery = query != null ? query.trim().toLowerCase() : "";
if (searchQuery.isEmpty()) {
followRoomsAdapter.submitList(new ArrayList<>(followAllRooms));
return;
}
List<Room> filtered = new ArrayList<>();
for (Room r : followAllRooms) {
if (r == null) continue;
String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
if (title.contains(searchQuery) || streamer.contains(searchQuery)) {
filtered.add(r);
}
}
followRoomsAdapter.submitList(filtered);
}
private void showNearbyUsers() {
binding.genericScroll.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.VISIBLE);
NearbyUsersAdapter adapter = new NearbyUsersAdapter(user -> {
@ -291,7 +381,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureParkBadges();
@ -341,7 +431,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureMore();
@ -461,7 +551,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.VISIBLE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
@ -478,7 +568,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.VISIBLE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
binding.followRecyclerView.setVisibility(View.GONE);
setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureAddFriends();

View File

@ -0,0 +1,119 @@
package com.example.livestreaming.net;
import com.google.gson.annotations.SerializedName;
import java.util.Objects;
public class Room {
@SerializedName("id")
private String id;
@SerializedName("title")
private String title;
@SerializedName("streamerName")
private String streamerName;
@SerializedName("type")
private String type;
@SerializedName("streamKey")
private String streamKey;
@SerializedName("isLive")
private boolean isLive;
@SerializedName("viewerCount")
private int viewerCount;
@SerializedName("streamUrls")
private StreamUrls streamUrls;
public Room() {
}
public Room(String id, String title, String streamerName, boolean isLive) {
this.id = id;
this.title = title;
this.streamerName = streamerName;
this.isLive = isLive;
}
public void setId(String id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
public void setStreamerName(String streamerName) {
this.streamerName = streamerName;
}`n`n public void setType(String type) {`n this.type = type;`n }
public void setLive(boolean live) {
isLive = live;
}
public void setStreamKey(String streamKey) {
this.streamKey = streamKey;
}
public void setStreamUrls(StreamUrls streamUrls) {
this.streamUrls = streamUrls;
}
public void setViewerCount(int viewerCount) {
this.viewerCount = viewerCount;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getStreamerName() {
return streamerName;
}`n`n public String getType() {`n return type;`n }
public String getStreamKey() {
return streamKey;
}
public boolean isLive() {
return isLive;
}
public StreamUrls getStreamUrls() {
return streamUrls;
}
public int getViewerCount() {
return viewerCount;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Room)) return false;
Room room = (Room) o;
return isLive == room.isLive
&& viewerCount == room.viewerCount
&& Objects.equals(id, room.id)
&& Objects.equals(title, room.title)
&& Objects.equals(streamerName, room.streamerName)
&& Objects.equals(type, room.type)
&& 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, type, type, streamKey, isLive, viewerCount, streamUrls);
}
}

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="14dp"
android:paddingBottom="14dp">
<ImageView
android:id="@+id/backButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="back"
android:src="@drawable/ic_arrow_back_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end"
android:maxLines="1"
android:text="通知设置"
android:textColor="#111111"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/backButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/backButton"
app:layout_constraintTop_toTopOf="@id/backButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="24dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1062,16 +1062,81 @@
</androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/followRecyclerView"
<LinearLayout
android:id="@+id/followContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="12dp"
android:paddingBottom="24dp"
android:visibility="gone" />
android:orientation="vertical"
android:visibility="gone">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeColor="#14000000"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="14dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="search"
android:src="@android:drawable/ic_menu_search"
android:tint="#666666" />
<EditText
android:id="@+id/followSearchInput"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:hint="搜索关注的直播间..."
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:textColor="#111111"
android:textColorHint="#999999"
android:textSize="14sp" />
<ImageView
android:id="@+id/followSearchClear"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:contentDescription="clear"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="#999999"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/followRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="24dp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nearbyRecyclerView"

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="手机号"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/phoneNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="验证码"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/verificationCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/sendCodeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送验证码"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="当前密码"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/currentPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="新密码"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/newPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="确认新密码"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/confirmPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="反馈类型"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<AutoCompleteTextView
android:id="@+id/feedbackType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:text="问题反馈" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="反馈内容"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/feedbackContent"
android:layout_width="match_parent"
android:layout_height="120dp"
android:gravity="top|start"
android:inputType="textMultiLine"
android:maxLines="5"
android:scrollbars="vertical" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="我们会认真处理您的反馈,感谢您的支持!"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>

View File

@ -0,0 +1,309 @@
# 项目未完成功能清单
---
## ✅ 刚刚完成的功能
### 通知功能前端UI
- ✅ 通知列表页面 (NotificationsActivity)
- ✅ 通知分类(系统、互动、关注、私信、直播)
- ✅ 通知设置页面 (NotificationSettingsActivity)
- ✅ 本地通知功能 (LocalNotificationManager)
---
## 🔴 高优先级未完成功能(前端可独立完成)
### 1. **数据持久化Room数据库** ⭐⭐⭐
**状态**: 未开始
**位置**: 文档第472行
- [ ] 引入 Room 数据库依赖
- [ ] 创建数据库实体Room、User、Message、Notification等
- [ ] 创建 DAO 接口
- [ ] 创建数据库类
- [ ] 实现 Repository 模式
- [ ] 缓存直播间列表到本地
- [ ] 缓存用户信息
- [ ] 实现搜索历史存储
- [ ] 实现观看历史存储
- [ ] 实现消息记录缓存
**预计工作量**: 3-5天
---
### 2. **顶部标签页功能** ⭐⭐
**状态**: ✅ 已完成
**位置**: MainActivity.java 第265-291行
- [x] 实现关注页面(显示已关注主播的直播)
- [x] 实现发现页面(推荐算法前端实现)
- [x] 实现附近页面(使用模拟位置数据)
- [x] 添加位置权限申请(即使后端未就绪)
**完成说明**:
- 修改MainActivity让顶部标签页在当前页面切换内容而不是跳转到新页面
- 关注页面显示已关注主播的直播列表基于FollowingListActivity的数据结构
- 发现页面:实现前端推荐算法(优先显示正在直播的房间,按类型排序)
- 附近页面:使用模拟位置数据,添加位置权限检查和申请逻辑
- 在AndroidManifest.xml中添加了位置权限声明
- 实现了位置权限申请对话框和权限被拒绝时的友好提示
**预计工作量**: 3-4天已完成
---
### 3. **搜索功能增强**
**状态**: 基础功能完成,增强功能未实现
**位置**: SearchActivity.java
- [ ] 实现搜索历史本地存储使用SharedPreferences或Room
- [ ] 实现热门搜索(模拟数据)
- [ ] 优化搜索性能(防抖、缓存)
- [ ] 添加搜索建议(自动补全)
**预计工作量**: 2-3天
---
### 4. **作品功能前端UI**
**状态**: UI已存在功能未实现
**位置**: ProfileActivity.java 第254行
- [ ] 实现作品列表UI已有tabWorks布局
- [ ] 实现作品发布UI数据仅本地存储
- [ ] 实现作品详情页面
- [ ] 实现作品编辑UI
- [ ] 作品数据模型和适配器
**预计工作量**: 3-4天
---
### 5. **许愿树核心功能**
**状态**: 仅UI完成核心功能未实现
**位置**: WishTreeActivity.java
- [ ] 实现许愿功能
- [ ] 实现抽奖功能
- [ ] 实现许愿列表展示
- [ ] 实现倒计时结束后的事件处理
- [ ] 许愿数据模型和存储
**预计工作量**: 3-4天
---
## 🟡 中优先级未完成功能
### 6. **占位页面功能实现**
**状态**: 多个功能跳转到TabPlaceholderActivity
**位置**: TabPlaceholderActivity.java
以下功能需要实现(目前都是占位页面):
- [ ] 语音匹配 (VoiceMatchActivity) - 已有Activity但功能未实现
- [ ] 心动信号 (HeartbeatSignalActivity) - 已有Activity但功能未实现
- [ ] 在线处对象 (OnlineDatingActivity) - 已有Activity但功能未实现
- [ ] 找人玩游戏 (FindGameActivity) - 已有Activity但功能未实现
- [ ] 一起KTV (KTVTogetherActivity) - 已有Activity但功能未实现
- [ ] 你画我猜 (DrawGuessActivity) - 已有Activity但功能未实现
- [ ] 和平精英 (PeaceEliteActivity) - 已有Activity但功能未实现
- [ ] 桌子游 (TableGamesActivity) - 已有Activity但功能未实现
- [ ] 公园勋章 - 跳转到TabPlaceholderActivity
- [ ] 加好友 - 跳转到TabPlaceholderActivity
- [ ] 定位/发现 - 跳转到TabPlaceholderActivity
- [ ] 附近直播 - TabPlaceholderActivity中显示"待接入"
- [ ] 热门地点 - TabPlaceholderActivity中显示"待接入"
- [ ] 榜单功能 - TabPlaceholderActivity中显示"待接入"
- [ ] 话题功能 - TabPlaceholderActivity中显示"待接入"
**预计工作量**: 每个功能1-2天总计15-30天
---
### 7. **设置页面完善** ⭐ ✅ 已完成
**状态**: 功能已完善
**位置**: SettingsPageActivity.java
- [x] 完善设置页面功能
- [x] 添加应用设置(缓存清理功能实现)
- [x] 添加账号设置UI修改密码、绑定手机号等
- [x] 添加隐私设置UI黑名单、权限管理等
- [x] 实现帮助中心内容
- [x] 实现关于页面内容
**完成内容**:
- 创建了 `CacheManager` 缓存管理工具类
- 实现了所有设置页面的功能对话框
- 创建了对话框布局文件
- 所有功能都已实现部分功能待接入后端API
**预计工作量**: 2-3天
---
### 8. **引导页面和帮助**
**状态**: 未实现
**位置**: 无
- [ ] 实现首次启动引导页ViewPager2
- [ ] 添加功能介绍页面
- [ ] 添加权限说明页面
- [ ] 实现帮助中心
**预计工作量**: 2-3天
---
## 🟢 低优先级未完成功能(体验增强)
### 9. **过渡动画和交互优化**
**状态**: 未实现
- [ ] 添加 Activity 过渡动画
- [ ] 实现共享元素过渡
- [ ] 优化页面内动画
- [ ] 添加触觉反馈Haptic Feedback
**预计工作量**: 2-3天
---
### 10. **深色模式支持**
**状态**: 未实现
- [ ] 创建深色模式资源
- [ ] 适配所有页面颜色
- [ ] 添加手动切换功能
- [ ] 测试深色模式显示
**预计工作量**: 3-4天
---
### 11. **多屏幕适配**
**状态**: 未实现
- [ ] 优化平板布局
- [ ] 添加横屏布局
- [ ] 测试不同屏幕尺寸
- [ ] 优化不同分辨率显示
**预计工作量**: 2-3天
---
## ⚪ 架构和优化(持续进行)
### 12. **前端架构优化** ⭐⭐
**状态**: 未开始
- [ ] 引入 MVVM 架构ViewModel + LiveData
- [ ] 实现 Repository 模式(本地数据源)
- [ ] 提取公共基类 Activity
- [ ] 创建工具类库
- [ ] 引入依赖注入Hilt可选
**预计工作量**: 5-7天
---
### 13. **性能优化**
**状态**: 部分完成
- [ ] 优化图片加载Glide配置优化
- [ ] 优化列表滚动性能
- [ ] 实现请求去重
- [ ] 优化内存使用
**预计工作量**: 3-4天
---
### 14. **代码质量提升**
**状态**: 持续进行
- [ ] 提取硬编码字符串到资源文件
- [ ] 添加代码注释
- [ ] 统一代码风格
- [ ] 重构重复代码
**预计工作量**: 持续进行
---
### 15. **测试**
**状态**: 未开始
- [ ] 编写单元测试(工具类)
- [ ] 编写 UI 测试(关键流程)
- [ ] 性能测试
- [ ] 兼容性测试
**预计工作量**: 持续进行
---
## ❌ 需要后端支持的功能(暂不实现)
以下功能需要后端支持,建议后端开发完成后再实现:
- ❌ 后端API完整集成等待后端接口
- ❌ 实时通信WebSocket等待后端
- ❌ 真实数据同步(等待后端)
- ❌ 用户登录/注册(等待后端)
- ❌ 支付功能等待后端和支付SDK
- ❌ 推流功能如需要等待推流SDK集成
- ❌ 礼物打赏功能等待后端和支付SDK
- ❌ 弹幕功能等待WebSocket服务
---
## 📊 完成度统计
### 前端可独立完成的功能
- **已完成**: 约 40%
- **进行中**: 约 10%
- **未开始**: 约 50%
### 核心功能模块
- ✅ **直播相关**: 85% 完成
- ✅ **社交功能**: 75% 完成
- ⚠️ **个人中心**: 70% 完成(作品功能缺失)
- ⚠️ **发现功能**: 60% 完成(搜索增强、标签页缺失)
- ⚠️ **特色功能**: 50% 完成(许愿树、占位功能缺失)
---
## 🎯 建议优先完成顺序
### 第一周
1. **数据持久化Room数据库** - 3-5天
- 这是基础架构,其他功能会依赖它
### 第二周
2. **搜索功能增强** - 2-3天
3. **作品功能** - 3-4天
### 第三周
4. **顶部标签页功能** - 3-4天
5. **许愿树核心功能** - 3-4天
### 第四周
6. **设置页面完善** - 2-3天
7. **引导页面** - 2-3天
---
## 📝 注意事项
1. **数据持久化**是最重要的基础功能,建议优先完成
2. **占位页面功能**数量较多,可以根据实际需求选择性实现
3. **架构优化**可以在功能完善过程中逐步进行
4. **需要后端支持的功能**可以先做UI使用模拟数据
---
**最后更新**: 2024年

View File

@ -178,7 +178,7 @@
- ✅ 粉丝列表 (FansListActivity) - 基础UI
- ✅ 关注列表 (FollowingListActivity) - 基础UI
- ✅ 获赞列表 (LikesListActivity) - 基础UI
- ✅ 设置页面 (SettingsPageActivity) - 基础UI
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
---
@ -449,15 +449,25 @@
- 验证ExoPlayer资源释放逻辑正确已有实现
- 集成LeakCanary内存泄漏检测工具
#### 3. **统一加载状态** ⭐⭐
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
**为什么重要**: 提升用户体验一致性
- [ ] 创建统一的加载状态组件
- [ ] 实现骨架屏Skeleton Screen
- [ ] 统一所有页面的加载提示
- [ ] 添加加载动画
- [x] 创建统一的加载状态组件
- [x] 实现骨架屏Skeleton Screen
- [x] 统一所有页面的加载提示
- [x] 添加加载动画
**预计工作量**: 1-2天
**完成内容**:
- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
- 实现了 `SkeletonView``SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
- 创建了 `LoadingStateManager` 工具类统一管理加载状态支持LoadingView、ProgressBar、骨架屏
- 更新了所有主要Activity使用统一的加载状态
- **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
- **RoomDetailActivity**: 使用LoadingView显示加载状态
- **SearchActivity**: 搜索时显示"搜索中..."加载状态
- **MessagesActivity**: 加载消息列表时显示加载状态
- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
#### 4. **数据持久化(本地)** ⭐⭐⭐
**为什么重要**: 支持离线使用,提升用户体验
@ -486,13 +496,21 @@
**预计工作量**: 5-7天
#### 6. **分类筛选功能(前端逻辑)** ⭐⭐
#### 6. **分类筛选功能(前端逻辑)** ⭐⭐ ✅ 已完成
**为什么重要**: UI已准备好只需完善前端筛选逻辑
- [ ] 完善本地数据筛选逻辑
- [ ] 实现筛选条件记忆SharedPreferences
- [ ] 优化筛选性能
- [ ] 添加筛选动画效果
- [x] 完善本地数据筛选逻辑
- [x] 实现筛选条件记忆SharedPreferences
- [x] 优化筛选性能
- [x] 添加筛选动画效果
**完成内容**:
- 创建了 `CategoryFilterManager` 类,统一管理筛选逻辑
- 实现了异步筛选功能在后台线程执行筛选避免阻塞UI线程
- 使用 SharedPreferences 保存和恢复最后选中的分类
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator
- 优化了筛选算法优先使用房间的type字段降级到演示数据分类算法
- 在Activity恢复时自动恢复上次选中的分类标签
**预计工作量**: 1-2天
@ -516,14 +534,24 @@
**预计工作量**: 2-3天
#### 9. **消息功能完善(前端)** ⭐⭐
#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
**为什么重要**: 完善核心社交功能
- [ ] 完善消息列表UI
- [ ] 实现消息状态显示(发送中、已发送、已读)
- [ ] 优化消息列表性能
- [ ] 添加消息操作(复制、删除等)
- [ ] 实现消息搜索(本地)
- [x] 完善消息列表UI
- [x] 实现消息状态显示(发送中、已发送、已读)
- [x] 优化消息列表性能
- [x] 添加消息操作(复制、删除等)
- [x] 实现消息搜索(本地)
**完成内容**:
- 为ChatMessage添加了MessageStatus枚举发送中、已发送、已读
- 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
- 实现了消息长按菜单,支持复制和删除操作
- 优化了DiffUtil使用messageId作为唯一标识提升列表更新性能
- 在MessagesActivity中添加了搜索功能支持按会话标题和消息内容搜索
- 优化了消息列表UI包括消息预览处理图片/语音消息)、时间显示、布局优化
- 添加了内存泄漏防护onDestroy中清理延迟任务
- 优化了滚动行为使用smoothScrollToPosition
**预计工作量**: 3-4天
@ -585,33 +613,59 @@
**预计工作量**: 3-4天
#### 15. **分享功能(系统分享)**
#### 15. **分享功能(系统分享)** ✅ 已完成
**为什么重要**: 提升传播能力
- [ ] 实现系统分享功能
- [ ] 分享直播间(生成分享链接)
- [ ] 分享个人主页
- [ ] 分享图片(截图功能)
- [x] 实现系统分享功能
- [x] 分享直播间(生成分享链接)
- [x] 分享个人主页
**完成内容**:
- 创建了 `ShareUtils` 工具类,提供系统分享功能(分享文本、链接)
- 在 `RoomDetailActivity` 中添加了分享按钮,支持分享直播间链接
- 完善了 `ProfileActivity` 的分享功能,支持分享个人主页链接
- 添加了分享图标资源 `ic_share_24.xml`
- 所有分享功能都通过系统分享菜单支持分享到微信、QQ、微博等应用
**预计工作量**: 2-3天
#### 16. **通知功能前端UI**
#### 16. **通知功能前端UI** ✅ 已完成
**为什么重要**: 完善消息体系
- [ ] 实现通知列表页面
- [ ] 实现通知分类
- [ ] 实现通知设置页面
- [ ] 添加本地通知(即使后端未就绪)
- [x] 实现通知列表页面
- [x] 实现通知分类
- [x] 实现通知设置页面
- [x] 添加本地通知(即使后端未就绪)
**完成内容**:
- 创建了 `NotificationsActivity` 通知列表页面,支持分类筛选(全部、系统、互动、关注、私信、直播)
- 创建了 `NotificationSettingsActivity` 通知设置页面,支持各类通知开关和免打扰设置
- 创建了 `LocalNotificationManager` 本地通知管理器,支持发送各类通知
- 创建了 `NotificationItem` 数据模型和 `NotificationsAdapter` 适配器
- 在 `MainActivity` 中添加了通知图标点击事件
- 在 `SettingsPageActivity` 中完善了通知设置入口
- 在 `LiveStreamingApplication` 中初始化了通知渠道
**预计工作量**: 2-3天
#### 17. **设置页面完善**
#### 17. **设置页面完善** ✅ 已完成
**为什么重要**: 完善应用配置
- [ ] 完善设置页面功能
- [ ] 添加应用设置(缓存清理、关于等)
- [ ] 添加账号设置UI
- [ ] 添加隐私设置UI
- [x] 完善设置页面功能
- [x] 添加应用设置(缓存清理、关于等)
- [x] 添加账号设置UI
- [x] 添加隐私设置UI
**完成内容**:
- 创建了 `CacheManager` 缓存管理工具类,支持获取缓存大小、清理所有缓存、清理图片缓存
- 完善了 `SettingsPageActivity`,实现了所有设置页面的功能:
- **账号与安全**: 修改密码对话框、绑定手机号对话框、登录设备管理对话框
- **隐私设置**: 黑名单管理、权限管理(跳转系统设置)、隐私政策查看
- **清理缓存**: 显示缓存大小、清理所有缓存、清理图片缓存(带进度提示)
- **帮助与反馈**: 常见问题、意见反馈对话框、联系客服信息
- **关于页面**: 自动获取版本信息、用户协议、隐私政策查看
- 创建了对话框布局文件:`dialog_change_password.xml`、`dialog_bind_phone.xml`、`dialog_feedback.xml`
- 所有功能都提供了友好的用户界面和提示信息
**预计工作量**: 2-3天