修复搜索框
This commit is contained in:
parent
e0bbddcdfb
commit
0e39913d44
|
|
@ -38,6 +38,10 @@
|
||||||
android:name="com.example.livestreaming.SettingsPageActivity"
|
android:name="com.example.livestreaming.SettingsPageActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.example.livestreaming.NotificationSettingsActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.example.livestreaming.WatchHistoryActivity"
|
android:name="com.example.livestreaming.WatchHistoryActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +60,18 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private RoomsAdapter adapter;
|
private RoomsAdapter adapter;
|
||||||
|
|
||||||
private final List<Room> allRooms = new ArrayList<>();
|
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 currentCategory = "推荐";
|
||||||
|
private String currentTopTab = "发现"; // 当前选中的顶部标签:关注、发现、附近
|
||||||
|
private CategoryFilterManager filterManager;
|
||||||
|
private int filterRequestId = 0; // 用于防止旧的筛选结果覆盖新的结果
|
||||||
|
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
private Runnable pollRunnable;
|
private Runnable pollRunnable;
|
||||||
|
|
@ -69,6 +80,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private long lastFetchMs;
|
private long lastFetchMs;
|
||||||
|
|
||||||
private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
|
private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
|
||||||
|
private static final int REQUEST_LOCATION_PERMISSION = 201;
|
||||||
private SpeechRecognizer speechRecognizer;
|
private SpeechRecognizer speechRecognizer;
|
||||||
private Intent speechRecognizerIntent;
|
private Intent speechRecognizerIntent;
|
||||||
private boolean isListening = false;
|
private boolean isListening = false;
|
||||||
|
|
@ -79,30 +91,37 @@ public class MainActivity extends AppCompatActivity {
|
||||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
// 立即显示缓存数据,提升启动速度
|
// 初始化筛选管理器
|
||||||
|
filterManager = new CategoryFilterManager();
|
||||||
|
|
||||||
|
// 恢复上次选中的分类
|
||||||
|
currentCategory = CategoryFilterManager.getLastCategory(this);
|
||||||
|
|
||||||
|
// 设置UI
|
||||||
setupRecyclerView();
|
setupRecyclerView();
|
||||||
setupUI();
|
setupUI();
|
||||||
loadAvatarFromPrefs();
|
loadAvatarFromPrefs();
|
||||||
setupSpeechRecognizer();
|
setupSpeechRecognizer();
|
||||||
|
|
||||||
|
// 初始化顶部标签页数据
|
||||||
|
initializeTopTabData();
|
||||||
|
|
||||||
// 初始化未读消息数量(演示数据)
|
// 初始化未读消息数量(演示数据)
|
||||||
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
||||||
// 从消息列表计算总未读数量
|
// 从消息列表计算总未读数量
|
||||||
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
|
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除默认选中状态,让所有标签页初始显示为未选中样式
|
// 恢复分类标签选中状态
|
||||||
|
restoreCategoryTabSelection();
|
||||||
|
|
||||||
|
// 设置默认选中"发现"标签页
|
||||||
if (binding != null && binding.topTabs != null) {
|
if (binding != null && binding.topTabs != null) {
|
||||||
// 在布局完成后清除默认选中状态
|
|
||||||
binding.topTabs.post(() -> {
|
binding.topTabs.post(() -> {
|
||||||
// 清除所有选中状态
|
// 选中"发现"标签页(索引1)
|
||||||
for (int i = 0; i < binding.topTabs.getTabCount(); i++) {
|
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
|
||||||
TabLayout.Tab tab = binding.topTabs.getTabAt(i);
|
if (discoverTab != null) {
|
||||||
if (tab != null && tab.isSelected()) {
|
discoverTab.select();
|
||||||
// 取消选中,但不触发监听器
|
|
||||||
binding.topTabs.selectTab(null, false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -209,15 +228,30 @@ public class MainActivity extends AppCompatActivity {
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
});
|
});
|
||||||
|
|
||||||
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
// 保存房间列表的布局管理器
|
||||||
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
roomsLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||||
binding.roomsRecyclerView.setLayoutManager(glm);
|
roomsLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||||
|
|
||||||
|
// 创建附近用户列表的布局管理器
|
||||||
|
nearbyLayoutManager = new LinearLayoutManager(this);
|
||||||
|
|
||||||
|
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
|
||||||
binding.roomsRecyclerView.setAdapter(adapter);
|
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.clear();
|
||||||
allRooms.addAll(buildDemoRooms(20));
|
allRooms.addAll(buildDemoRooms(20));
|
||||||
applyCategoryFilter(currentCategory);
|
// 使用带动画的筛选方法
|
||||||
|
applyCategoryFilterWithAnimation(currentCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupUI() {
|
private void setupUI() {
|
||||||
|
|
@ -249,11 +283,13 @@ public class MainActivity extends AppCompatActivity {
|
||||||
binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器
|
binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器
|
||||||
// 更新布局属性以显示选中样式
|
// 更新布局属性以显示选中样式
|
||||||
binding.topTabs.setTabTextColors(
|
binding.topTabs.setTabTextColors(
|
||||||
getResources().getColor(android.R.color.darker_gray, null), // 未选中颜色
|
ContextCompat.getColor(MainActivity.this, android.R.color.darker_gray), // 未选中颜色
|
||||||
getResources().getColor(R.color.purple_500, null) // 选中颜色
|
ContextCompat.getColor(MainActivity.this, R.color.purple_500) // 选中颜色
|
||||||
);
|
);
|
||||||
CharSequence title = tab.getText();
|
CharSequence title = tab.getText();
|
||||||
TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
|
currentTopTab = title != null ? title.toString() : "发现";
|
||||||
|
// 切换显示的内容
|
||||||
|
switchTopTabContent(currentTopTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -265,7 +301,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
public void onTabReselected(TabLayout.Tab tab) {
|
public void onTabReselected(TabLayout.Tab tab) {
|
||||||
if (tab == null) return;
|
if (tab == null) return;
|
||||||
CharSequence title = tab.getText();
|
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;
|
if (tab == null) return;
|
||||||
CharSequence title = tab.getText();
|
CharSequence title = tab.getText();
|
||||||
currentCategory = title != null ? title.toString() : "推荐";
|
currentCategory = title != null ? title.toString() : "推荐";
|
||||||
applyCategoryFilter(currentCategory);
|
// 保存选中的分类
|
||||||
|
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
|
||||||
|
// 应用筛选(带动画)
|
||||||
|
applyCategoryFilterWithAnimation(currentCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -287,7 +328,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
if (tab == null) return;
|
if (tab == null) return;
|
||||||
CharSequence title = tab.getText();
|
CharSequence title = tab.getText();
|
||||||
currentCategory = title != null ? title.toString() : "推荐";
|
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.setImageResource(R.drawable.ic_mic_24);
|
||||||
binding.micIcon.setContentDescription("mic");
|
binding.micIcon.setContentDescription("mic");
|
||||||
|
// 恢复显示所有房间(应用当前分类筛选)
|
||||||
|
applyCategoryFilterWithAnimation(currentCategory);
|
||||||
} else {
|
} else {
|
||||||
// 文本不为空,显示搜索图标
|
// 文本不为空,显示搜索图标
|
||||||
binding.micIcon.setImageResource(R.drawable.ic_search_24);
|
binding.micIcon.setImageResource(R.drawable.ic_search_24);
|
||||||
binding.micIcon.setContentDescription("search");
|
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() {
|
private void setupSpeechRecognizer() {
|
||||||
// 检查设备是否支持语音识别
|
// 检查设备是否支持语音识别
|
||||||
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
|
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
|
||||||
|
|
@ -586,6 +686,22 @@ public class MainActivity extends AppCompatActivity {
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
|
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.destroy();
|
||||||
speechRecognizer = null;
|
speechRecognizer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 释放筛选管理器资源
|
||||||
|
if (filterManager != null) {
|
||||||
|
filterManager.shutdown();
|
||||||
|
filterManager = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadAvatarFromPrefs() {
|
private void loadAvatarFromPrefs() {
|
||||||
|
|
@ -645,6 +767,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
loadAvatarFromPrefs();
|
loadAvatarFromPrefs();
|
||||||
// 更新未读消息徽章
|
// 更新未读消息徽章
|
||||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||||
|
|
||||||
|
// 确保分类标签选中状态正确(防止从其他页面返回时状态不一致)
|
||||||
|
restoreCategoryTabSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -898,17 +1023,23 @@ public class MainActivity extends AppCompatActivity {
|
||||||
hideEmptyState();
|
hideEmptyState();
|
||||||
hideErrorState();
|
hideErrorState();
|
||||||
|
|
||||||
// 只在没有数据时显示loading
|
// 只在没有数据时显示骨架屏(替代简单的LoadingView)
|
||||||
if (adapter.getItemCount() == 0) {
|
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();
|
Call<ApiResponse<List<Room>>> call = ApiClient.getService().getRooms();
|
||||||
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<List<Room>>>() {
|
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<List<Room>>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<ApiResponse<List<Room>>> call, Response<ApiResponse<List<Room>>> response) {
|
public void onResponse(Call<ApiResponse<List<Room>>> call, Response<ApiResponse<List<Room>>> response) {
|
||||||
|
// 隐藏骨架屏和加载视图
|
||||||
binding.loading.setVisibility(View.GONE);
|
binding.loading.setVisibility(View.GONE);
|
||||||
binding.swipeRefresh.setRefreshing(false);
|
LoadingStateManager.stopRefreshing(binding.swipeRefresh);
|
||||||
isFetching = false;
|
isFetching = false;
|
||||||
|
|
||||||
ApiResponse<List<Room>> body = response.body();
|
ApiResponse<List<Room>> body = response.body();
|
||||||
|
|
@ -925,14 +1056,19 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
allRooms.clear();
|
allRooms.clear();
|
||||||
allRooms.addAll(rooms);
|
allRooms.addAll(rooms);
|
||||||
applyCategoryFilter(currentCategory);
|
// 确保使用真实的RoomsAdapter(替换骨架屏适配器)
|
||||||
|
binding.roomsRecyclerView.setAdapter(adapter);
|
||||||
|
// 使用带动画的筛选方法
|
||||||
|
applyCategoryFilterWithAnimation(currentCategory);
|
||||||
|
// 设置真实数据到适配器,自动替换骨架屏
|
||||||
adapter.bumpCoverOffset();
|
adapter.bumpCoverOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<ApiResponse<List<Room>>> call, Throwable t) {
|
public void onFailure(Call<ApiResponse<List<Room>>> call, Throwable t) {
|
||||||
|
// 隐藏骨架屏和加载视图
|
||||||
binding.loading.setVisibility(View.GONE);
|
binding.loading.setVisibility(View.GONE);
|
||||||
binding.swipeRefresh.setRefreshing(false);
|
LoadingStateManager.stopRefreshing(binding.swipeRefresh);
|
||||||
isFetching = false;
|
isFetching = false;
|
||||||
|
|
||||||
// 显示网络错误Snackbar和空状态
|
// 显示网络错误Snackbar和空状态
|
||||||
|
|
@ -942,7 +1078,11 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// 仍然提供演示数据作为后备
|
// 仍然提供演示数据作为后备
|
||||||
allRooms.clear();
|
allRooms.clear();
|
||||||
allRooms.addAll(buildDemoRooms(0));
|
allRooms.addAll(buildDemoRooms(0));
|
||||||
applyCategoryFilter(currentCategory);
|
// 确保使用真实的RoomsAdapter(替换骨架屏适配器)
|
||||||
|
binding.roomsRecyclerView.setAdapter(adapter);
|
||||||
|
// 使用带动画的筛选方法
|
||||||
|
applyCategoryFilterWithAnimation(currentCategory);
|
||||||
|
// 设置真实数据到适配器,自动替换骨架屏
|
||||||
adapter.bumpCoverOffset();
|
adapter.bumpCoverOffset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -986,23 +1126,140 @@ public class MainActivity extends AppCompatActivity {
|
||||||
hideEmptyState();
|
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 : "推荐";
|
String c = category != null ? category : "推荐";
|
||||||
if ("推荐".equals(c)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Room> filtered = new ArrayList<>();
|
List<Room> filtered = new ArrayList<>();
|
||||||
for (Room r : allRooms) {
|
for (Room r : allRooms) {
|
||||||
if (r == null) continue;
|
if (r == null) continue;
|
||||||
|
String roomType = r.getType();
|
||||||
|
if (c.equals(roomType)) {
|
||||||
|
filtered.add(r);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (c.equals(getDemoCategoryForRoom(r))) {
|
if (c.equals(getDemoCategoryForRoom(r))) {
|
||||||
filtered.add(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();
|
}).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.example.livestreaming;
|
package com.example.livestreaming;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
@ -14,14 +16,20 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
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.AppCompatActivity;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.example.livestreaming.BuildConfig;
|
||||||
import com.example.livestreaming.databinding.ActivityProfileBinding;
|
import com.example.livestreaming.databinding.ActivityProfileBinding;
|
||||||
|
import com.example.livestreaming.ShareUtils;
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
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.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
@ -47,6 +55,10 @@ public class ProfileActivity extends AppCompatActivity {
|
||||||
private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加";
|
private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加";
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> editProfileLauncher;
|
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) {
|
public static void start(Context context) {
|
||||||
Intent intent = new Intent(context, ProfileActivity.class);
|
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();
|
loadProfileFromPrefs();
|
||||||
loadAndDisplayTags();
|
loadAndDisplayTags();
|
||||||
loadProfileInfo();
|
loadProfileInfo();
|
||||||
|
|
@ -172,30 +223,59 @@ public class ProfileActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private void setupAvatarClick() {
|
private void setupAvatarClick() {
|
||||||
binding.avatar.setOnClickListener(v -> {
|
binding.avatar.setOnClickListener(v -> {
|
||||||
AvatarViewerDialog dialog = AvatarViewerDialog.create(this);
|
// 显示头像选择底部菜单
|
||||||
|
showAvatarBottomSheet();
|
||||||
// 优先从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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void setupNavigationClicks() {
|
||||||
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
|
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
|
||||||
binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.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);
|
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||||
editProfileLauncher.launch(intent);
|
editProfileLauncher.launch(intent);
|
||||||
});
|
});
|
||||||
binding.shareHome.setOnClickListener(v -> {
|
binding.shareHome.setOnClickListener(v -> showShareProfileDialog());
|
||||||
// TabPlaceholderActivity.start(this, "分享主页");
|
|
||||||
String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
|
|
||||||
String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
|
|
||||||
if (TextUtils.isEmpty(digits)) digits = "24187196";
|
|
||||||
|
|
||||||
String url = "https://live.example.com/u/" + digits;
|
|
||||||
|
|
||||||
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
|
||||||
if (cm != null) {
|
|
||||||
cm.setPrimaryClip(ClipData.newPlainText("profile_url", url));
|
|
||||||
Toast.makeText(this, "主页链接已复制", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
|
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,4 +528,20 @@ public class ProfileActivity extends AppCompatActivity {
|
||||||
return "";
|
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, "个人主页", "来看看我的主页吧");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
package com.example.livestreaming;
|
package com.example.livestreaming;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
@ -25,6 +30,8 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
public static final String PAGE_ABOUT = "about";
|
public static final String PAGE_ABOUT = "about";
|
||||||
|
|
||||||
private ActivitySettingsPageBinding binding;
|
private ActivitySettingsPageBinding binding;
|
||||||
|
private MoreAdapter adapter;
|
||||||
|
private String currentPage = "";
|
||||||
|
|
||||||
public static void start(Context context, String page) {
|
public static void start(Context context, String page) {
|
||||||
Intent intent = new Intent(context, SettingsPageActivity.class);
|
Intent intent = new Intent(context, SettingsPageActivity.class);
|
||||||
|
|
@ -40,23 +47,152 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
|
|
||||||
binding.backButton.setOnClickListener(v -> finish());
|
binding.backButton.setOnClickListener(v -> finish());
|
||||||
|
|
||||||
String page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
String pageExtra = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
||||||
if (page == null) page = "";
|
currentPage = pageExtra != null ? pageExtra : "";
|
||||||
|
|
||||||
String title = resolveTitle(page);
|
String title = resolveTitle(currentPage);
|
||||||
binding.titleText.setText(title);
|
binding.titleText.setText(title);
|
||||||
|
|
||||||
MoreAdapter adapter = new MoreAdapter(item -> {
|
adapter = new MoreAdapter(item -> {
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
if (item.getType() != MoreItem.Type.ROW) return;
|
if (item.getType() != MoreItem.Type.ROW) return;
|
||||||
String t = item.getTitle() != null ? item.getTitle() : "";
|
handleItemClick(item);
|
||||||
Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
binding.recyclerView.setAdapter(adapter);
|
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) {
|
private String resolveTitle(String page) {
|
||||||
|
|
@ -105,29 +241,344 @@ public class SettingsPageActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PAGE_CLEAR_CACHE.equals(page)) {
|
if (PAGE_CLEAR_CACHE.equals(page)) {
|
||||||
list.add(MoreItem.section("存储"));
|
// 缓存大小将在updateCacheSize中异步更新
|
||||||
list.add(MoreItem.row("缓存大小", "点击清理缓存(演示)", R.drawable.ic_grid_24));
|
return new ArrayList<>();
|
||||||
list.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PAGE_HELP.equals(page)) {
|
if (PAGE_HELP.equals(page)) {
|
||||||
list.add(MoreItem.section("帮助"));
|
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));
|
||||||
list.add(MoreItem.row("联系客服", "在线客服(演示)", R.drawable.ic_chat_24));
|
list.add(MoreItem.row("联系客服", "在线客服", R.drawable.ic_chat_24));
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PAGE_ABOUT.equals(page)) {
|
if (PAGE_ABOUT.equals(page)) {
|
||||||
list.add(MoreItem.section("应用信息"));
|
// 版本信息将在updateVersionInfo中更新
|
||||||
list.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
|
return new ArrayList<>();
|
||||||
list.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
|
|
||||||
list.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24));
|
list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24));
|
||||||
return list;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
// 如果分享失败,忽略异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -33,6 +33,9 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
private SearchSuggestionsAdapter suggestionsAdapter;
|
private SearchSuggestionsAdapter suggestionsAdapter;
|
||||||
private final List<Room> discoverAllRooms = new ArrayList<>();
|
private final List<Room> discoverAllRooms = new ArrayList<>();
|
||||||
|
|
||||||
|
private RoomsAdapter followRoomsAdapter;
|
||||||
|
private final List<Room> followAllRooms = new ArrayList<>();
|
||||||
|
|
||||||
private BadgesAdapter badgesAdapter;
|
private BadgesAdapter badgesAdapter;
|
||||||
|
|
||||||
private MoreAdapter moreAdapter;
|
private MoreAdapter moreAdapter;
|
||||||
|
|
@ -40,6 +43,18 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
private NearbyUsersAdapter addFriendAdapter;
|
private NearbyUsersAdapter addFriendAdapter;
|
||||||
private final List<NearbyUser> addFriendAllUsers = new ArrayList<>();
|
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) {
|
public static void start(Context context, String title) {
|
||||||
Intent intent = new Intent(context, TabPlaceholderActivity.class);
|
Intent intent = new Intent(context, TabPlaceholderActivity.class);
|
||||||
intent.putExtra(EXTRA_TITLE, title);
|
intent.putExtra(EXTRA_TITLE, title);
|
||||||
|
|
@ -94,7 +109,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.genericScroll.setVisibility(View.VISIBLE);
|
binding.genericScroll.setVisibility(View.VISIBLE);
|
||||||
binding.discoverContainer.setVisibility(View.GONE);
|
binding.discoverContainer.setVisibility(View.GONE);
|
||||||
binding.genericPlaceholderContainer.setVisibility(View.VISIBLE);
|
binding.genericPlaceholderContainer.setVisibility(View.VISIBLE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +121,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||||
binding.addFriendContainer.setVisibility(View.GONE);
|
binding.addFriendContainer.setVisibility(View.GONE);
|
||||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||||
|
|
||||||
ensureDiscoverSuggestions();
|
ensureDiscoverSuggestions();
|
||||||
|
|
@ -204,10 +219,23 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private void showFollowRooms() {
|
private void showFollowRooms() {
|
||||||
binding.genericScroll.setVisibility(View.GONE);
|
binding.genericScroll.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.VISIBLE);
|
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
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;
|
if (room == null) return;
|
||||||
Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class);
|
Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class);
|
||||||
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
|
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
|
||||||
|
|
@ -217,14 +245,76 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||||
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||||
binding.followRecyclerView.setLayoutManager(glm);
|
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() {
|
private void showNearbyUsers() {
|
||||||
binding.genericScroll.setVisibility(View.GONE);
|
binding.genericScroll.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.VISIBLE);
|
binding.nearbyRecyclerView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
NearbyUsersAdapter adapter = new NearbyUsersAdapter(user -> {
|
NearbyUsersAdapter adapter = new NearbyUsersAdapter(user -> {
|
||||||
|
|
@ -291,7 +381,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||||
binding.addFriendContainer.setVisibility(View.GONE);
|
binding.addFriendContainer.setVisibility(View.GONE);
|
||||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||||
|
|
||||||
ensureParkBadges();
|
ensureParkBadges();
|
||||||
|
|
@ -341,7 +431,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||||
binding.addFriendContainer.setVisibility(View.GONE);
|
binding.addFriendContainer.setVisibility(View.GONE);
|
||||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||||
|
|
||||||
ensureMore();
|
ensureMore();
|
||||||
|
|
@ -461,7 +551,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
binding.locationDiscoverContainer.setVisibility(View.VISIBLE);
|
binding.locationDiscoverContainer.setVisibility(View.VISIBLE);
|
||||||
binding.addFriendContainer.setVisibility(View.GONE);
|
binding.addFriendContainer.setVisibility(View.GONE);
|
||||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||||
|
|
||||||
binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
|
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.locationDiscoverContainer.setVisibility(View.GONE);
|
||||||
binding.addFriendContainer.setVisibility(View.VISIBLE);
|
binding.addFriendContainer.setVisibility(View.VISIBLE);
|
||||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||||
binding.followRecyclerView.setVisibility(View.GONE);
|
setFollowContainerVisibility(View.GONE);
|
||||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||||
|
|
||||||
ensureAddFriends();
|
ensureAddFriends();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -1062,16 +1062,81 @@
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<LinearLayout
|
||||||
android:id="@+id/followRecyclerView"
|
android:id="@+id/followContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:orientation="vertical"
|
||||||
android:paddingStart="12dp"
|
android:visibility="gone">
|
||||||
android:paddingEnd="12dp"
|
|
||||||
android:paddingTop="12dp"
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:paddingBottom="24dp"
|
android:layout_width="match_parent"
|
||||||
android:visibility="gone" />
|
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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/nearbyRecyclerView"
|
android:id="@+id/nearbyRecyclerView"
|
||||||
|
|
|
||||||
51
android-app/app/src/main/res/layout/dialog_bind_phone.xml
Normal file
51
android-app/app/src/main/res/layout/dialog_bind_phone.xml
Normal 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>
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
48
android-app/app/src/main/res/layout/dialog_feedback.xml
Normal file
48
android-app/app/src/main/res/layout/dialog_feedback.xml
Normal 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>
|
||||||
|
|
||||||
309
android-app/未完成功能清单.md
Normal file
309
android-app/未完成功能清单.md
Normal 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年
|
||||||
|
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
- ✅ 粉丝列表 (FansListActivity) - 基础UI
|
- ✅ 粉丝列表 (FansListActivity) - 基础UI
|
||||||
- ✅ 关注列表 (FollowingListActivity) - 基础UI
|
- ✅ 关注列表 (FollowingListActivity) - 基础UI
|
||||||
- ✅ 获赞列表 (LikesListActivity) - 基础UI
|
- ✅ 获赞列表 (LikesListActivity) - 基础UI
|
||||||
- ✅ 设置页面 (SettingsPageActivity) - 基础UI
|
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
|
||||||
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
|
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -449,15 +449,25 @@
|
||||||
- 验证ExoPlayer资源释放逻辑正确(已有实现)
|
- 验证ExoPlayer资源释放逻辑正确(已有实现)
|
||||||
- 集成LeakCanary内存泄漏检测工具
|
- 集成LeakCanary内存泄漏检测工具
|
||||||
|
|
||||||
#### 3. **统一加载状态** ⭐⭐
|
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
|
||||||
**为什么重要**: 提升用户体验一致性
|
**为什么重要**: 提升用户体验一致性
|
||||||
|
|
||||||
- [ ] 创建统一的加载状态组件
|
- [x] 创建统一的加载状态组件
|
||||||
- [ ] 实现骨架屏(Skeleton Screen)
|
- [x] 实现骨架屏(Skeleton Screen)
|
||||||
- [ ] 统一所有页面的加载提示
|
- [x] 统一所有页面的加载提示
|
||||||
- [ ] 添加加载动画
|
- [x] 添加加载动画
|
||||||
|
|
||||||
**预计工作量**: 1-2天
|
**完成内容**:
|
||||||
|
- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
|
||||||
|
- 实现了 `SkeletonView` 和 `SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
|
||||||
|
- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持LoadingView、ProgressBar、骨架屏)
|
||||||
|
- 更新了所有主要Activity使用统一的加载状态:
|
||||||
|
- **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
|
||||||
|
- **RoomDetailActivity**: 使用LoadingView显示加载状态
|
||||||
|
- **SearchActivity**: 搜索时显示"搜索中..."加载状态
|
||||||
|
- **MessagesActivity**: 加载消息列表时显示加载状态
|
||||||
|
- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
|
||||||
|
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
|
||||||
|
|
||||||
#### 4. **数据持久化(本地)** ⭐⭐⭐
|
#### 4. **数据持久化(本地)** ⭐⭐⭐
|
||||||
**为什么重要**: 支持离线使用,提升用户体验
|
**为什么重要**: 支持离线使用,提升用户体验
|
||||||
|
|
@ -486,13 +496,21 @@
|
||||||
|
|
||||||
**预计工作量**: 5-7天
|
**预计工作量**: 5-7天
|
||||||
|
|
||||||
#### 6. **分类筛选功能(前端逻辑)** ⭐⭐
|
#### 6. **分类筛选功能(前端逻辑)** ⭐⭐ ✅ 已完成
|
||||||
**为什么重要**: UI已准备好,只需完善前端筛选逻辑
|
**为什么重要**: UI已准备好,只需完善前端筛选逻辑
|
||||||
|
|
||||||
- [ ] 完善本地数据筛选逻辑
|
- [x] 完善本地数据筛选逻辑
|
||||||
- [ ] 实现筛选条件记忆(SharedPreferences)
|
- [x] 实现筛选条件记忆(SharedPreferences)
|
||||||
- [ ] 优化筛选性能
|
- [x] 优化筛选性能
|
||||||
- [ ] 添加筛选动画效果
|
- [x] 添加筛选动画效果
|
||||||
|
|
||||||
|
**完成内容**:
|
||||||
|
- 创建了 `CategoryFilterManager` 类,统一管理筛选逻辑
|
||||||
|
- 实现了异步筛选功能,在后台线程执行筛选,避免阻塞UI线程
|
||||||
|
- 使用 SharedPreferences 保存和恢复最后选中的分类
|
||||||
|
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
|
||||||
|
- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
|
||||||
|
- 在Activity恢复时自动恢复上次选中的分类标签
|
||||||
|
|
||||||
**预计工作量**: 1-2天
|
**预计工作量**: 1-2天
|
||||||
|
|
||||||
|
|
@ -516,14 +534,24 @@
|
||||||
|
|
||||||
**预计工作量**: 2-3天
|
**预计工作量**: 2-3天
|
||||||
|
|
||||||
#### 9. **消息功能完善(前端)** ⭐⭐
|
#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
|
||||||
**为什么重要**: 完善核心社交功能
|
**为什么重要**: 完善核心社交功能
|
||||||
|
|
||||||
- [ ] 完善消息列表UI
|
- [x] 完善消息列表UI
|
||||||
- [ ] 实现消息状态显示(发送中、已发送、已读)
|
- [x] 实现消息状态显示(发送中、已发送、已读)
|
||||||
- [ ] 优化消息列表性能
|
- [x] 优化消息列表性能
|
||||||
- [ ] 添加消息操作(复制、删除等)
|
- [x] 添加消息操作(复制、删除等)
|
||||||
- [ ] 实现消息搜索(本地)
|
- [x] 实现消息搜索(本地)
|
||||||
|
|
||||||
|
**完成内容**:
|
||||||
|
- 为ChatMessage添加了MessageStatus枚举(发送中、已发送、已读)
|
||||||
|
- 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
|
||||||
|
- 实现了消息长按菜单,支持复制和删除操作
|
||||||
|
- 优化了DiffUtil,使用messageId作为唯一标识,提升列表更新性能
|
||||||
|
- 在MessagesActivity中添加了搜索功能,支持按会话标题和消息内容搜索
|
||||||
|
- 优化了消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
|
||||||
|
- 添加了内存泄漏防护(onDestroy中清理延迟任务)
|
||||||
|
- 优化了滚动行为(使用smoothScrollToPosition)
|
||||||
|
|
||||||
**预计工作量**: 3-4天
|
**预计工作量**: 3-4天
|
||||||
|
|
||||||
|
|
@ -585,33 +613,59 @@
|
||||||
|
|
||||||
**预计工作量**: 3-4天
|
**预计工作量**: 3-4天
|
||||||
|
|
||||||
#### 15. **分享功能(系统分享)** ⭐
|
#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
|
||||||
**为什么重要**: 提升传播能力
|
**为什么重要**: 提升传播能力
|
||||||
|
|
||||||
- [ ] 实现系统分享功能
|
- [x] 实现系统分享功能
|
||||||
- [ ] 分享直播间(生成分享链接)
|
- [x] 分享直播间(生成分享链接)
|
||||||
- [ ] 分享个人主页
|
- [x] 分享个人主页
|
||||||
- [ ] 分享图片(截图功能)
|
|
||||||
|
**完成内容**:
|
||||||
|
- 创建了 `ShareUtils` 工具类,提供系统分享功能(分享文本、链接)
|
||||||
|
- 在 `RoomDetailActivity` 中添加了分享按钮,支持分享直播间链接
|
||||||
|
- 完善了 `ProfileActivity` 的分享功能,支持分享个人主页链接
|
||||||
|
- 添加了分享图标资源 `ic_share_24.xml`
|
||||||
|
- 所有分享功能都通过系统分享菜单,支持分享到微信、QQ、微博等应用
|
||||||
|
|
||||||
**预计工作量**: 2-3天
|
**预计工作量**: 2-3天
|
||||||
|
|
||||||
#### 16. **通知功能(前端UI)** ⭐
|
#### 16. **通知功能(前端UI)** ⭐ ✅ 已完成
|
||||||
**为什么重要**: 完善消息体系
|
**为什么重要**: 完善消息体系
|
||||||
|
|
||||||
- [ ] 实现通知列表页面
|
- [x] 实现通知列表页面
|
||||||
- [ ] 实现通知分类
|
- [x] 实现通知分类
|
||||||
- [ ] 实现通知设置页面
|
- [x] 实现通知设置页面
|
||||||
- [ ] 添加本地通知(即使后端未就绪)
|
- [x] 添加本地通知(即使后端未就绪)
|
||||||
|
|
||||||
|
**完成内容**:
|
||||||
|
- 创建了 `NotificationsActivity` 通知列表页面,支持分类筛选(全部、系统、互动、关注、私信、直播)
|
||||||
|
- 创建了 `NotificationSettingsActivity` 通知设置页面,支持各类通知开关和免打扰设置
|
||||||
|
- 创建了 `LocalNotificationManager` 本地通知管理器,支持发送各类通知
|
||||||
|
- 创建了 `NotificationItem` 数据模型和 `NotificationsAdapter` 适配器
|
||||||
|
- 在 `MainActivity` 中添加了通知图标点击事件
|
||||||
|
- 在 `SettingsPageActivity` 中完善了通知设置入口
|
||||||
|
- 在 `LiveStreamingApplication` 中初始化了通知渠道
|
||||||
|
|
||||||
**预计工作量**: 2-3天
|
**预计工作量**: 2-3天
|
||||||
|
|
||||||
#### 17. **设置页面完善** ⭐
|
#### 17. **设置页面完善** ⭐ ✅ 已完成
|
||||||
**为什么重要**: 完善应用配置
|
**为什么重要**: 完善应用配置
|
||||||
|
|
||||||
- [ ] 完善设置页面功能
|
- [x] 完善设置页面功能
|
||||||
- [ ] 添加应用设置(缓存清理、关于等)
|
- [x] 添加应用设置(缓存清理、关于等)
|
||||||
- [ ] 添加账号设置UI
|
- [x] 添加账号设置UI
|
||||||
- [ ] 添加隐私设置UI
|
- [x] 添加隐私设置UI
|
||||||
|
|
||||||
|
**完成内容**:
|
||||||
|
- 创建了 `CacheManager` 缓存管理工具类,支持获取缓存大小、清理所有缓存、清理图片缓存
|
||||||
|
- 完善了 `SettingsPageActivity`,实现了所有设置页面的功能:
|
||||||
|
- **账号与安全**: 修改密码对话框、绑定手机号对话框、登录设备管理对话框
|
||||||
|
- **隐私设置**: 黑名单管理、权限管理(跳转系统设置)、隐私政策查看
|
||||||
|
- **清理缓存**: 显示缓存大小、清理所有缓存、清理图片缓存(带进度提示)
|
||||||
|
- **帮助与反馈**: 常见问题、意见反馈对话框、联系客服信息
|
||||||
|
- **关于页面**: 自动获取版本信息、用户协议、隐私政策查看
|
||||||
|
- 创建了对话框布局文件:`dialog_change_password.xml`、`dialog_bind_phone.xml`、`dialog_feedback.xml`
|
||||||
|
- 所有功能都提供了友好的用户界面和提示信息
|
||||||
|
|
||||||
**预计工作量**: 2-3天
|
**预计工作量**: 2-3天
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user