修复搜索框
This commit is contained in:
parent
e0bbddcdfb
commit
0e39913d44
|
|
@ -38,6 +38,10 @@
|
|||
android:name="com.example.livestreaming.SettingsPageActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.NotificationSettingsActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.WatchHistoryActivity"
|
||||
android:exported="false" />
|
||||
|
|
|
|||
|
|
@ -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 final List<Room> allRooms = new ArrayList<>();
|
||||
private final List<Room> followRooms = new ArrayList<>(); // 关注页面的房间列表
|
||||
private final List<Room> discoverRooms = new ArrayList<>(); // 发现页面的房间列表
|
||||
private final List<NearbyUser> nearbyUsers = new ArrayList<>(); // 附近页面的用户列表
|
||||
private NearbyUsersAdapter nearbyUsersAdapter; // 附近页面的适配器
|
||||
|
||||
private StaggeredGridLayoutManager roomsLayoutManager; // 房间列表的布局管理器
|
||||
private LinearLayoutManager nearbyLayoutManager; // 附近用户列表的布局管理器
|
||||
|
||||
private String currentCategory = "推荐";
|
||||
private String currentTopTab = "发现"; // 当前选中的顶部标签:关注、发现、附近
|
||||
private CategoryFilterManager filterManager;
|
||||
private int filterRequestId = 0; // 用于防止旧的筛选结果覆盖新的结果
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private Runnable pollRunnable;
|
||||
|
|
@ -69,6 +80,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
private long lastFetchMs;
|
||||
|
||||
private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
|
||||
private static final int REQUEST_LOCATION_PERMISSION = 201;
|
||||
private SpeechRecognizer speechRecognizer;
|
||||
private Intent speechRecognizerIntent;
|
||||
private boolean isListening = false;
|
||||
|
|
@ -79,30 +91,37 @@ public class MainActivity extends AppCompatActivity {
|
|||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
// 立即显示缓存数据,提升启动速度
|
||||
// 初始化筛选管理器
|
||||
filterManager = new CategoryFilterManager();
|
||||
|
||||
// 恢复上次选中的分类
|
||||
currentCategory = CategoryFilterManager.getLastCategory(this);
|
||||
|
||||
// 设置UI
|
||||
setupRecyclerView();
|
||||
setupUI();
|
||||
loadAvatarFromPrefs();
|
||||
setupSpeechRecognizer();
|
||||
|
||||
// 初始化顶部标签页数据
|
||||
initializeTopTabData();
|
||||
|
||||
// 初始化未读消息数量(演示数据)
|
||||
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
||||
// 从消息列表计算总未读数量
|
||||
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
|
||||
}
|
||||
|
||||
// 清除默认选中状态,让所有标签页初始显示为未选中样式
|
||||
// 恢复分类标签选中状态
|
||||
restoreCategoryTabSelection();
|
||||
|
||||
// 设置默认选中"发现"标签页
|
||||
if (binding != null && binding.topTabs != null) {
|
||||
// 在布局完成后清除默认选中状态
|
||||
binding.topTabs.post(() -> {
|
||||
// 清除所有选中状态
|
||||
for (int i = 0; i < binding.topTabs.getTabCount(); i++) {
|
||||
TabLayout.Tab tab = binding.topTabs.getTabAt(i);
|
||||
if (tab != null && tab.isSelected()) {
|
||||
// 取消选中,但不触发监听器
|
||||
binding.topTabs.selectTab(null, false);
|
||||
break;
|
||||
}
|
||||
// 选中"发现"标签页(索引1)
|
||||
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
|
||||
if (discoverTab != null) {
|
||||
discoverTab.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -209,15 +228,30 @@ public class MainActivity extends AppCompatActivity {
|
|||
startActivity(intent);
|
||||
});
|
||||
|
||||
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||
binding.roomsRecyclerView.setLayoutManager(glm);
|
||||
// 保存房间列表的布局管理器
|
||||
roomsLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||
roomsLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||
|
||||
// 创建附近用户列表的布局管理器
|
||||
nearbyLayoutManager = new LinearLayoutManager(this);
|
||||
|
||||
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
|
||||
binding.roomsRecyclerView.setAdapter(adapter);
|
||||
|
||||
// 配置RecyclerView动画,优化筛选时的过渡效果
|
||||
androidx.recyclerview.widget.DefaultItemAnimator animator = new androidx.recyclerview.widget.DefaultItemAnimator();
|
||||
animator.setAddDuration(200); // 添加动画时长
|
||||
animator.setRemoveDuration(200); // 删除动画时长
|
||||
animator.setMoveDuration(200); // 移动动画时长
|
||||
animator.setChangeDuration(200); // 变更动画时长
|
||||
binding.roomsRecyclerView.setItemAnimator(animator);
|
||||
|
||||
// 立即显示演示数据,提升用户体验
|
||||
// 注意:如果后续需要从网络加载,骨架屏会在fetchRooms中显示
|
||||
allRooms.clear();
|
||||
allRooms.addAll(buildDemoRooms(20));
|
||||
applyCategoryFilter(currentCategory);
|
||||
// 使用带动画的筛选方法
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
}
|
||||
|
||||
private void setupUI() {
|
||||
|
|
@ -249,11 +283,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器
|
||||
// 更新布局属性以显示选中样式
|
||||
binding.topTabs.setTabTextColors(
|
||||
getResources().getColor(android.R.color.darker_gray, null), // 未选中颜色
|
||||
getResources().getColor(R.color.purple_500, null) // 选中颜色
|
||||
ContextCompat.getColor(MainActivity.this, android.R.color.darker_gray), // 未选中颜色
|
||||
ContextCompat.getColor(MainActivity.this, R.color.purple_500) // 选中颜色
|
||||
);
|
||||
CharSequence title = tab.getText();
|
||||
TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
|
||||
currentTopTab = title != null ? title.toString() : "发现";
|
||||
// 切换显示的内容
|
||||
switchTopTabContent(currentTopTab);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -265,7 +301,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
if (tab == null) return;
|
||||
CharSequence title = tab.getText();
|
||||
TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
|
||||
currentTopTab = title != null ? title.toString() : "发现";
|
||||
// 切换显示的内容
|
||||
switchTopTabContent(currentTopTab);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -275,7 +313,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (tab == null) return;
|
||||
CharSequence title = tab.getText();
|
||||
currentCategory = title != null ? title.toString() : "推荐";
|
||||
applyCategoryFilter(currentCategory);
|
||||
// 保存选中的分类
|
||||
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
|
||||
// 应用筛选(带动画)
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -287,7 +328,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
if (tab == null) return;
|
||||
CharSequence title = tab.getText();
|
||||
currentCategory = title != null ? title.toString() : "推荐";
|
||||
applyCategoryFilter(currentCategory);
|
||||
// 保存选中的分类
|
||||
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
|
||||
// 应用筛选(带动画)
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -374,10 +418,14 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 文本为空,显示麦克风图标
|
||||
binding.micIcon.setImageResource(R.drawable.ic_mic_24);
|
||||
binding.micIcon.setContentDescription("mic");
|
||||
// 恢复显示所有房间(应用当前分类筛选)
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
} else {
|
||||
// 文本不为空,显示搜索图标
|
||||
binding.micIcon.setImageResource(R.drawable.ic_search_24);
|
||||
binding.micIcon.setContentDescription("search");
|
||||
// 实时筛选房间列表
|
||||
applySearchFilter(text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -402,6 +450,58 @@ public class MainActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索筛选
|
||||
*/
|
||||
private void applySearchFilter(String query) {
|
||||
if (adapter == null || allRooms == null) return;
|
||||
|
||||
String searchQuery = query != null ? query.trim().toLowerCase() : "";
|
||||
if (searchQuery.isEmpty()) {
|
||||
// 如果搜索框为空,恢复显示所有房间(应用当前分类筛选)
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
return;
|
||||
}
|
||||
|
||||
// 先应用分类筛选,再应用搜索筛选
|
||||
List<Room> categoryFiltered = new ArrayList<>();
|
||||
if ("全部".equals(currentCategory) || currentCategory == null) {
|
||||
categoryFiltered.addAll(allRooms);
|
||||
} else {
|
||||
for (Room r : allRooms) {
|
||||
if (r == null) continue;
|
||||
String roomType = r.getType();
|
||||
if (currentCategory.equals(roomType)) {
|
||||
categoryFiltered.add(r);
|
||||
} else if (currentCategory.equals(getDemoCategoryForRoom(r))) {
|
||||
categoryFiltered.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在分类筛选结果中应用搜索筛选
|
||||
List<Room> filtered = new ArrayList<>();
|
||||
for (Room r : categoryFiltered) {
|
||||
if (r == null) continue;
|
||||
String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
|
||||
String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
|
||||
if (title.contains(searchQuery) || streamer.contains(searchQuery)) {
|
||||
filtered.add(r);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新列表显示
|
||||
adapter.submitList(filtered, () -> {
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(1.0f)
|
||||
.setDuration(200)
|
||||
.start();
|
||||
});
|
||||
|
||||
// 更新空状态
|
||||
updateEmptyStateForList(filtered);
|
||||
}
|
||||
|
||||
private void setupSpeechRecognizer() {
|
||||
// 检查设备是否支持语音识别
|
||||
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
|
||||
|
|
@ -586,6 +686,22 @@ public class MainActivity extends AppCompatActivity {
|
|||
} else {
|
||||
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_LOCATION_PERMISSION) {
|
||||
if (grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 位置权限已授予,显示附近页面
|
||||
showNearbyTab();
|
||||
} else {
|
||||
// 位置权限被拒绝
|
||||
Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
|
||||
// 切换回发现页面
|
||||
if (binding.topTabs != null) {
|
||||
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
|
||||
if (discoverTab != null) {
|
||||
discoverTab.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -603,6 +719,12 @@ public class MainActivity extends AppCompatActivity {
|
|||
speechRecognizer.destroy();
|
||||
speechRecognizer = null;
|
||||
}
|
||||
|
||||
// 释放筛选管理器资源
|
||||
if (filterManager != null) {
|
||||
filterManager.shutdown();
|
||||
filterManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAvatarFromPrefs() {
|
||||
|
|
@ -645,6 +767,9 @@ public class MainActivity extends AppCompatActivity {
|
|||
loadAvatarFromPrefs();
|
||||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
|
||||
// 确保分类标签选中状态正确(防止从其他页面返回时状态不一致)
|
||||
restoreCategoryTabSelection();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -898,17 +1023,23 @@ public class MainActivity extends AppCompatActivity {
|
|||
hideEmptyState();
|
||||
hideErrorState();
|
||||
|
||||
// 只在没有数据时显示loading
|
||||
// 只在没有数据时显示骨架屏(替代简单的LoadingView)
|
||||
if (adapter.getItemCount() == 0) {
|
||||
binding.loading.setVisibility(View.VISIBLE);
|
||||
// 使用骨架屏替代简单的LoadingView,提供更好的用户体验
|
||||
LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6);
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
} else {
|
||||
// 如果有数据,显示LoadingView(用于下拉刷新等场景)
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
Call<ApiResponse<List<Room>>> call = ApiClient.getService().getRooms();
|
||||
NetworkUtils.enqueueWithLifecycle(call, this, new Callback<ApiResponse<List<Room>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<Room>>> call, Response<ApiResponse<List<Room>>> response) {
|
||||
// 隐藏骨架屏和加载视图
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
LoadingStateManager.stopRefreshing(binding.swipeRefresh);
|
||||
isFetching = false;
|
||||
|
||||
ApiResponse<List<Room>> body = response.body();
|
||||
|
|
@ -925,14 +1056,19 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
allRooms.clear();
|
||||
allRooms.addAll(rooms);
|
||||
applyCategoryFilter(currentCategory);
|
||||
// 确保使用真实的RoomsAdapter(替换骨架屏适配器)
|
||||
binding.roomsRecyclerView.setAdapter(adapter);
|
||||
// 使用带动画的筛选方法
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
// 设置真实数据到适配器,自动替换骨架屏
|
||||
adapter.bumpCoverOffset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<Room>>> call, Throwable t) {
|
||||
// 隐藏骨架屏和加载视图
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
binding.swipeRefresh.setRefreshing(false);
|
||||
LoadingStateManager.stopRefreshing(binding.swipeRefresh);
|
||||
isFetching = false;
|
||||
|
||||
// 显示网络错误Snackbar和空状态
|
||||
|
|
@ -942,7 +1078,11 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 仍然提供演示数据作为后备
|
||||
allRooms.clear();
|
||||
allRooms.addAll(buildDemoRooms(0));
|
||||
applyCategoryFilter(currentCategory);
|
||||
// 确保使用真实的RoomsAdapter(替换骨架屏适配器)
|
||||
binding.roomsRecyclerView.setAdapter(adapter);
|
||||
// 使用带动画的筛选方法
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
// 设置真实数据到适配器,自动替换骨架屏
|
||||
adapter.bumpCoverOffset();
|
||||
}
|
||||
});
|
||||
|
|
@ -986,23 +1126,140 @@ public class MainActivity extends AppCompatActivity {
|
|||
hideEmptyState();
|
||||
}
|
||||
|
||||
private void applyCategoryFilter(String category) {
|
||||
/**
|
||||
* 应用分类筛选(带动画效果)
|
||||
* 使用异步筛选提升性能,并添加平滑的过渡动画
|
||||
* 使用filterRequestId防止旧的筛选结果覆盖新的结果
|
||||
*/
|
||||
private void applyCategoryFilterWithAnimation(String category) {
|
||||
String c = category != null ? category : "推荐";
|
||||
|
||||
// 增加请求ID,确保只有最新的筛选结果被应用
|
||||
final int requestId = ++filterRequestId;
|
||||
|
||||
// 显示加载状态(如果数据量较大)
|
||||
if (allRooms.size() > 50) {
|
||||
binding.loading.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 使用筛选管理器异步筛选
|
||||
if (filterManager != null) {
|
||||
filterManager.filterRoomsAsync(allRooms, c, filteredRooms -> {
|
||||
// 检查这个结果是否仍然是最新的请求
|
||||
if (requestId != filterRequestId) {
|
||||
// 这是一个旧的请求结果,忽略它
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏加载状态
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
|
||||
// 添加淡入动画
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(0.7f)
|
||||
.setDuration(100)
|
||||
.withEndAction(() -> {
|
||||
// 再次检查请求ID(防止在动画期间又有新的筛选请求)
|
||||
if (requestId != filterRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新列表数据(ListAdapter会自动处理DiffUtil动画)
|
||||
adapter.submitList(filteredRooms, () -> {
|
||||
// 最后一次检查请求ID
|
||||
if (requestId != filterRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 数据更新完成后,恢复透明度并添加淡入效果
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(1.0f)
|
||||
.setDuration(200)
|
||||
.start();
|
||||
});
|
||||
|
||||
// 更新空状态
|
||||
updateEmptyStateForList(filteredRooms);
|
||||
})
|
||||
.start();
|
||||
});
|
||||
} else {
|
||||
// 降级到同步筛选(如果筛选管理器未初始化)
|
||||
applyCategoryFilterSync(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步筛选(降级方案)
|
||||
*/
|
||||
private void applyCategoryFilterSync(String category) {
|
||||
String c = category != null ? category : "推荐";
|
||||
if ("推荐".equals(c)) {
|
||||
adapter.submitList(new ArrayList<>(allRooms));
|
||||
updateEmptyStateForList(allRooms);
|
||||
// 添加淡入动画,保持与其他筛选场景的一致性
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(0.7f)
|
||||
.setDuration(100)
|
||||
.withEndAction(() -> {
|
||||
adapter.submitList(new ArrayList<>(allRooms), () -> {
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(1.0f)
|
||||
.setDuration(200)
|
||||
.start();
|
||||
});
|
||||
updateEmptyStateForList(allRooms);
|
||||
})
|
||||
.start();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Room> filtered = new ArrayList<>();
|
||||
for (Room r : allRooms) {
|
||||
if (r == null) continue;
|
||||
String roomType = r.getType();
|
||||
if (c.equals(roomType)) {
|
||||
filtered.add(r);
|
||||
continue;
|
||||
}
|
||||
if (c.equals(getDemoCategoryForRoom(r))) {
|
||||
filtered.add(r);
|
||||
}
|
||||
}
|
||||
adapter.submitList(filtered);
|
||||
updateEmptyStateForList(filtered);
|
||||
|
||||
// 添加淡入动画
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(0.7f)
|
||||
.setDuration(100)
|
||||
.withEndAction(() -> {
|
||||
adapter.submitList(filtered, () -> {
|
||||
binding.roomsRecyclerView.animate()
|
||||
.alpha(1.0f)
|
||||
.setDuration(200)
|
||||
.start();
|
||||
});
|
||||
updateEmptyStateForList(filtered);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复分类标签的选中状态
|
||||
*/
|
||||
private void restoreCategoryTabSelection() {
|
||||
if (binding == null || binding.categoryTabs == null) return;
|
||||
|
||||
// 延迟执行,确保TabLayout已完全初始化
|
||||
binding.categoryTabs.post(() -> {
|
||||
for (int i = 0; i < binding.categoryTabs.getTabCount(); i++) {
|
||||
TabLayout.Tab tab = binding.categoryTabs.getTabAt(i);
|
||||
if (tab != null) {
|
||||
CharSequence tabText = tab.getText();
|
||||
if (tabText != null && currentCategory.equals(tabText.toString())) {
|
||||
tab.select();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1115,4 +1372,321 @@ public class MainActivity extends AppCompatActivity {
|
|||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化顶部标签页数据
|
||||
*/
|
||||
private void initializeTopTabData() {
|
||||
// 初始化关注页面数据(已关注主播的直播)
|
||||
followRooms.clear();
|
||||
followRooms.addAll(buildFollowRooms());
|
||||
|
||||
// 初始化发现页面数据(推荐算法)
|
||||
discoverRooms.clear();
|
||||
discoverRooms.addAll(buildDiscoverRooms());
|
||||
|
||||
// 初始化附近页面数据(模拟位置数据)
|
||||
nearbyUsers.clear();
|
||||
nearbyUsers.addAll(buildNearbyUsers());
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换顶部标签页内容
|
||||
*/
|
||||
private void switchTopTabContent(String tabName) {
|
||||
if (tabName == null) tabName = "发现";
|
||||
|
||||
if ("关注".equals(tabName)) {
|
||||
showFollowTab();
|
||||
} else if ("发现".equals(tabName)) {
|
||||
showDiscoverTab();
|
||||
} else if ("附近".equals(tabName)) {
|
||||
showNearbyTab();
|
||||
} else {
|
||||
// 默认显示发现页面
|
||||
showDiscoverTab();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示关注页面
|
||||
*/
|
||||
private void showFollowTab() {
|
||||
// 隐藏分类标签(关注页面不需要分类筛选)
|
||||
if (binding.categoryTabs != null) {
|
||||
binding.categoryTabs.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 恢复房间列表的布局管理器和适配器
|
||||
if (binding.roomsRecyclerView != null) {
|
||||
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
|
||||
binding.roomsRecyclerView.setAdapter(adapter);
|
||||
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 使用房间适配器显示关注的主播直播
|
||||
if (adapter != null) {
|
||||
adapter.submitList(new ArrayList<>(followRooms));
|
||||
}
|
||||
|
||||
// 更新空状态
|
||||
if (followRooms.isEmpty()) {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setIcon(R.drawable.ic_person_24);
|
||||
binding.emptyStateView.setTitle("还没有关注的主播");
|
||||
binding.emptyStateView.setMessage("去发现页关注喜欢的主播吧");
|
||||
binding.emptyStateView.hideActionButton();
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
hideEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示发现页面
|
||||
*/
|
||||
private void showDiscoverTab() {
|
||||
// 显示分类标签
|
||||
if (binding.categoryTabs != null) {
|
||||
binding.categoryTabs.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 恢复房间列表的布局管理器和适配器
|
||||
if (binding.roomsRecyclerView != null) {
|
||||
binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
|
||||
binding.roomsRecyclerView.setAdapter(adapter);
|
||||
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 使用房间适配器显示推荐内容
|
||||
if (adapter != null) {
|
||||
// 先显示所有推荐房间,然后应用分类筛选
|
||||
allRooms.clear();
|
||||
allRooms.addAll(discoverRooms);
|
||||
applyCategoryFilterWithAnimation(currentCategory);
|
||||
}
|
||||
|
||||
// 更新空状态
|
||||
updateEmptyStateForList(allRooms);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示附近页面
|
||||
*/
|
||||
private void showNearbyTab() {
|
||||
// 检查位置权限
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
&& ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 请求位置权限
|
||||
requestLocationPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏分类标签(附近页面不需要分类筛选)
|
||||
if (binding.categoryTabs != null) {
|
||||
binding.categoryTabs.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// 初始化附近用户适配器(如果还没有)
|
||||
if (nearbyUsersAdapter == null) {
|
||||
nearbyUsersAdapter = new NearbyUsersAdapter(user -> {
|
||||
if (user == null) return;
|
||||
// 点击附近用户,可以跳转到用户主页或显示详情
|
||||
Toast.makeText(MainActivity.this, "点击了:" + user.getName(), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
// 切换RecyclerView的布局管理器和适配器,显示附近用户列表
|
||||
if (binding.roomsRecyclerView != null) {
|
||||
binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
|
||||
binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
|
||||
nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
|
||||
binding.roomsRecyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 更新空状态
|
||||
if (nearbyUsers.isEmpty()) {
|
||||
if (binding.emptyStateView != null) {
|
||||
binding.emptyStateView.setIcon(R.drawable.ic_search_24);
|
||||
binding.emptyStateView.setTitle("附近暂无用户");
|
||||
binding.emptyStateView.setMessage("开启定位后可以查看附近的用户");
|
||||
binding.emptyStateView.hideActionButton();
|
||||
binding.emptyStateView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
hideEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建关注页面的房间列表(已关注主播的直播)
|
||||
*/
|
||||
private List<Room> buildFollowRooms() {
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 从FollowingListActivity获取已关注的主播列表
|
||||
// 这里使用模拟数据,实际应该从数据库或API获取
|
||||
String[][] followData = {
|
||||
{"王者荣耀排位赛", "王者荣耀陪练", "游戏", "true"},
|
||||
{"音乐电台", "音乐电台", "音乐", "false"},
|
||||
{"户外直播", "户外阿杰", "户外", "true"},
|
||||
{"美食探店", "美食探店", "美食", "false"},
|
||||
{"聊天连麦", "聊天小七", "聊天", "true"},
|
||||
{"才艺表演", "才艺小妹", "才艺", "true"},
|
||||
{"游戏竞技", "游戏高手", "游戏", "true"},
|
||||
{"音乐演奏", "音乐达人", "音乐", "false"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < followData.length; i++) {
|
||||
String id = "follow-" + i;
|
||||
String title = followData[i][0];
|
||||
String streamer = followData[i][1];
|
||||
String type = followData[i][2];
|
||||
boolean live = Boolean.parseBoolean(followData[i][3]);
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
list.add(room);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建发现页面的房间列表(推荐算法前端实现)
|
||||
*/
|
||||
private List<Room> buildDiscoverRooms() {
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 推荐算法:基于观看历史、点赞等模拟数据
|
||||
// 这里实现一个简单的推荐算法:
|
||||
// 1. 优先推荐正在直播的房间
|
||||
// 2. 优先推荐热门类型(游戏、才艺、音乐)
|
||||
// 3. 添加一些随机性
|
||||
|
||||
String[][] discoverData = {
|
||||
{"王者荣耀排位赛", "小明选手", "游戏", "true"},
|
||||
{"吃鸡大逃杀", "游戏高手", "游戏", "true"},
|
||||
{"唱歌连麦", "音乐达人", "音乐", "true"},
|
||||
{"户外直播", "旅行者", "户外", "false"},
|
||||
{"美食制作", "厨神小李", "美食", "true"},
|
||||
{"才艺表演", "舞蹈小妹", "才艺", "true"},
|
||||
{"聊天交友", "暖心姐姐", "聊天", "false"},
|
||||
{"LOL竞技场", "电竞选手", "游戏", "true"},
|
||||
{"古风演奏", "琴师小王", "音乐", "true"},
|
||||
{"健身教学", "教练张", "户外", "false"},
|
||||
{"摄影分享", "摄影师", "户外", "true"},
|
||||
{"宠物秀", "萌宠主播", "才艺", "true"},
|
||||
{"编程教学", "码农老王", "聊天", "false"},
|
||||
{"读书分享", "书虫小妹", "聊天", "true"},
|
||||
{"手工制作", "手艺人", "才艺", "true"},
|
||||
{"英语口语", "外教老师", "聊天", "false"},
|
||||
{"魔术表演", "魔术师", "才艺", "true"},
|
||||
{"街头访谈", "记者小张", "户外", "true"},
|
||||
{"乐器教学", "音乐老师", "音乐", "false"},
|
||||
{"电影解说", "影评人", "聊天", "true"},
|
||||
{"游戏攻略", "游戏解说", "游戏", "true"},
|
||||
{"K歌大赛", "K歌达人", "音乐", "true"},
|
||||
{"美食探店", "美食博主", "美食", "true"},
|
||||
{"舞蹈教学", "舞蹈老师", "才艺", "true"}
|
||||
};
|
||||
|
||||
// 推荐算法:优先显示正在直播的,然后按类型排序
|
||||
List<Room> liveRooms = new ArrayList<>();
|
||||
List<Room> offlineRooms = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < discoverData.length; i++) {
|
||||
String id = "discover-" + i;
|
||||
String title = discoverData[i][0];
|
||||
String streamer = discoverData[i][1];
|
||||
String type = discoverData[i][2];
|
||||
boolean live = Boolean.parseBoolean(discoverData[i][3]);
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
|
||||
if (live) {
|
||||
liveRooms.add(room);
|
||||
} else {
|
||||
offlineRooms.add(room);
|
||||
}
|
||||
}
|
||||
|
||||
// 先添加正在直播的,再添加未直播的
|
||||
list.addAll(liveRooms);
|
||||
list.addAll(offlineRooms);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建附近页面的用户列表(使用模拟位置数据)
|
||||
*/
|
||||
private List<NearbyUser> buildNearbyUsers() {
|
||||
List<NearbyUser> list = new ArrayList<>();
|
||||
|
||||
// 模拟位置数据:生成不同距离的用户
|
||||
String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰",
|
||||
"小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"};
|
||||
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
String id = "nearby-user-" + i;
|
||||
String name = names[i];
|
||||
boolean live = i % 3 == 0; // 每3个用户中有一个在直播
|
||||
|
||||
String distanceText;
|
||||
if (i < 3) {
|
||||
distanceText = (300 + i * 120) + "m";
|
||||
} else if (i < 10) {
|
||||
float km = 0.8f + (i - 3) * 0.35f;
|
||||
distanceText = String.format("%.1fkm", km);
|
||||
} else {
|
||||
float km = 3.5f + (i - 10) * 0.5f;
|
||||
distanceText = String.format("%.1fkm", km);
|
||||
}
|
||||
|
||||
list.add(new NearbyUser(id, name, distanceText, live));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求位置权限
|
||||
*/
|
||||
private void requestLocationPermission() {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("需要位置权限")
|
||||
.setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。")
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
},
|
||||
REQUEST_LOCATION_PERMISSION);
|
||||
})
|
||||
.setNegativeButton("取消", (dialog, which) -> {
|
||||
// 用户拒绝权限,显示提示
|
||||
Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
|
||||
// 切换回发现页面
|
||||
if (binding.topTabs != null) {
|
||||
TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
|
||||
if (discoverTab != null) {
|
||||
discoverTab.select();
|
||||
}
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
},
|
||||
REQUEST_LOCATION_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.net.Uri;
|
||||
|
|
@ -14,14 +16,20 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.BuildConfig;
|
||||
import com.example.livestreaming.databinding.ActivityProfileBinding;
|
||||
import com.example.livestreaming.ShareUtils;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
|
@ -47,6 +55,10 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加";
|
||||
|
||||
private ActivityResultLauncher<Intent> editProfileLauncher;
|
||||
private ActivityResultLauncher<String> pickImageLauncher;
|
||||
private ActivityResultLauncher<Uri> takePictureLauncher;
|
||||
private ActivityResultLauncher<String> requestCameraPermissionLauncher;
|
||||
private Uri pendingCameraUri = null;
|
||||
|
||||
public static void start(Context context) {
|
||||
Intent intent = new Intent(context, ProfileActivity.class);
|
||||
|
|
@ -70,6 +82,45 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
}
|
||||
);
|
||||
|
||||
// 注册图片选择器
|
||||
pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {
|
||||
if (uri == null) return;
|
||||
// 保存头像URI到SharedPreferences
|
||||
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
|
||||
.putString(KEY_AVATAR_URI, uri.toString())
|
||||
.remove(KEY_AVATAR_RES) // 清除资源ID,因为现在使用URI
|
||||
.apply();
|
||||
// 立即更新头像显示
|
||||
Glide.with(this).load(uri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar);
|
||||
Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
// 注册拍照器
|
||||
takePictureLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> {
|
||||
if (!success) return;
|
||||
if (pendingCameraUri == null) return;
|
||||
// 保存头像URI到SharedPreferences
|
||||
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
|
||||
.putString(KEY_AVATAR_URI, pendingCameraUri.toString())
|
||||
.remove(KEY_AVATAR_RES) // 清除资源ID,因为现在使用URI
|
||||
.apply();
|
||||
// 立即更新头像显示
|
||||
Glide.with(this).load(pendingCameraUri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar);
|
||||
Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show();
|
||||
pendingCameraUri = null;
|
||||
});
|
||||
|
||||
// 注册相机权限请求
|
||||
requestCameraPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
|
||||
if (!granted) {
|
||||
Toast.makeText(this, "需要相机权限才能拍照", Toast.LENGTH_SHORT).show();
|
||||
pendingCameraUri = null;
|
||||
return;
|
||||
}
|
||||
if (pendingCameraUri == null) return;
|
||||
takePictureLauncher.launch(pendingCameraUri);
|
||||
});
|
||||
|
||||
loadProfileFromPrefs();
|
||||
loadAndDisplayTags();
|
||||
loadProfileInfo();
|
||||
|
|
@ -172,30 +223,59 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
|
||||
private void setupAvatarClick() {
|
||||
binding.avatar.setOnClickListener(v -> {
|
||||
AvatarViewerDialog dialog = AvatarViewerDialog.create(this);
|
||||
|
||||
// 优先从SharedPreferences读取最新的头像信息(因为ImageView可能还在加载中)
|
||||
String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null);
|
||||
int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0);
|
||||
|
||||
if (!TextUtils.isEmpty(avatarUri)) {
|
||||
// 使用URI加载,确保能正确显示
|
||||
dialog.setAvatarUri(Uri.parse(avatarUri));
|
||||
} else if (avatarRes != 0) {
|
||||
dialog.setAvatarResId(avatarRes);
|
||||
} else {
|
||||
// 如果都没有,尝试从ImageView获取Drawable
|
||||
Drawable drawable = binding.avatar.getDrawable();
|
||||
if (drawable != null) {
|
||||
dialog.setAvatarDrawable(drawable);
|
||||
} else {
|
||||
dialog.setAvatarResId(R.drawable.ic_account_circle_24);
|
||||
}
|
||||
}
|
||||
dialog.show();
|
||||
// 显示头像选择底部菜单
|
||||
showAvatarBottomSheet();
|
||||
});
|
||||
}
|
||||
|
||||
private void showAvatarBottomSheet() {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
View view = getLayoutInflater().inflate(R.layout.bottom_sheet_avatar_picker, null);
|
||||
dialog.setContentView(view);
|
||||
|
||||
View pick = view.findViewById(R.id.actionPickGallery);
|
||||
View camera = view.findViewById(R.id.actionTakePhoto);
|
||||
View cancel = view.findViewById(R.id.actionCancel);
|
||||
|
||||
pick.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
pickImageLauncher.launch("image/*");
|
||||
});
|
||||
|
||||
camera.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
Uri uri = createTempCameraUri();
|
||||
if (uri == null) return;
|
||||
pendingCameraUri = uri;
|
||||
ensureCameraPermissionAndTakePhoto();
|
||||
});
|
||||
|
||||
cancel.setOnClickListener(v -> dialog.dismiss());
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void ensureCameraPermissionAndTakePhoto() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
if (pendingCameraUri == null) return;
|
||||
takePictureLauncher.launch(pendingCameraUri);
|
||||
return;
|
||||
}
|
||||
requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
|
||||
private Uri createTempCameraUri() {
|
||||
try {
|
||||
File dir = new File(getCacheDir(), "images");
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
File file = new File(dir, "avatar_" + System.currentTimeMillis() + ".jpg");
|
||||
return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, "无法创建相机文件", Toast.LENGTH_SHORT).show();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupNavigationClicks() {
|
||||
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
|
||||
binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this));
|
||||
|
|
@ -225,20 +305,7 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||
editProfileLauncher.launch(intent);
|
||||
});
|
||||
binding.shareHome.setOnClickListener(v -> {
|
||||
// TabPlaceholderActivity.start(this, "分享主页");
|
||||
String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
|
||||
String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
|
||||
if (TextUtils.isEmpty(digits)) digits = "24187196";
|
||||
|
||||
String url = "https://live.example.com/u/" + digits;
|
||||
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
if (cm != null) {
|
||||
cm.setPrimaryClip(ClipData.newPlainText("profile_url", url));
|
||||
Toast.makeText(this, "主页链接已复制", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
binding.shareHome.setOnClickListener(v -> showShareProfileDialog());
|
||||
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
|
||||
}
|
||||
|
||||
|
|
@ -461,4 +528,20 @@ public class ProfileActivity extends AppCompatActivity {
|
|||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示分享个人主页对话框
|
||||
*/
|
||||
private void showShareProfileDialog() {
|
||||
// 获取用户ID
|
||||
String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
|
||||
String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
|
||||
if (TextUtils.isEmpty(digits)) {
|
||||
digits = "24187196"; // 默认ID
|
||||
}
|
||||
|
||||
// 直接生成分享链接
|
||||
String shareLink = ShareUtils.generateProfileShareLink(digits);
|
||||
ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
|
@ -25,6 +30,8 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
public static final String PAGE_ABOUT = "about";
|
||||
|
||||
private ActivitySettingsPageBinding binding;
|
||||
private MoreAdapter adapter;
|
||||
private String currentPage = "";
|
||||
|
||||
public static void start(Context context, String page) {
|
||||
Intent intent = new Intent(context, SettingsPageActivity.class);
|
||||
|
|
@ -40,23 +47,152 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
|
||||
binding.backButton.setOnClickListener(v -> finish());
|
||||
|
||||
String page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
||||
if (page == null) page = "";
|
||||
String pageExtra = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
|
||||
currentPage = pageExtra != null ? pageExtra : "";
|
||||
|
||||
String title = resolveTitle(page);
|
||||
String title = resolveTitle(currentPage);
|
||||
binding.titleText.setText(title);
|
||||
|
||||
MoreAdapter adapter = new MoreAdapter(item -> {
|
||||
adapter = new MoreAdapter(item -> {
|
||||
if (item == null) return;
|
||||
if (item.getType() != MoreItem.Type.ROW) return;
|
||||
String t = item.getTitle() != null ? item.getTitle() : "";
|
||||
Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show();
|
||||
handleItemClick(item);
|
||||
});
|
||||
|
||||
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.recyclerView.setAdapter(adapter);
|
||||
|
||||
adapter.submitList(buildItems(page));
|
||||
refreshItems();
|
||||
}
|
||||
|
||||
private void handleItemClick(MoreItem item) {
|
||||
String title = item.getTitle() != null ? item.getTitle() : "";
|
||||
|
||||
switch (currentPage) {
|
||||
case PAGE_ACCOUNT_SECURITY:
|
||||
handleAccountSecurityClick(title);
|
||||
break;
|
||||
case PAGE_PRIVACY:
|
||||
handlePrivacyClick(title);
|
||||
break;
|
||||
case PAGE_NOTIFICATIONS:
|
||||
handleNotificationsClick(title);
|
||||
break;
|
||||
case PAGE_CLEAR_CACHE:
|
||||
handleClearCacheClick(title);
|
||||
break;
|
||||
case PAGE_HELP:
|
||||
handleHelpClick(title);
|
||||
break;
|
||||
case PAGE_ABOUT:
|
||||
handleAboutClick(title);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAccountSecurityClick(String title) {
|
||||
if ("修改密码".equals(title)) {
|
||||
showChangePasswordDialog();
|
||||
} else if ("绑定手机号".equals(title)) {
|
||||
showBindPhoneDialog();
|
||||
} else if ("登录设备管理".equals(title)) {
|
||||
showDeviceManagementDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePrivacyClick(String title) {
|
||||
if ("黑名单".equals(title)) {
|
||||
showBlacklistDialog();
|
||||
} else if ("权限管理".equals(title)) {
|
||||
showPermissionManagementDialog();
|
||||
} else if ("隐私政策".equals(title)) {
|
||||
showPrivacyPolicyDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNotificationsClick(String title) {
|
||||
if ("系统通知".equals(title) || "免打扰".equals(title)) {
|
||||
NotificationSettingsActivity.start(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClearCacheClick(String title) {
|
||||
if ("缓存大小".equals(title)) {
|
||||
showClearAllCacheDialog();
|
||||
} else if ("图片缓存".equals(title)) {
|
||||
showClearImageCacheDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleHelpClick(String title) {
|
||||
if ("常见问题".equals(title)) {
|
||||
showFAQDialog();
|
||||
} else if ("意见反馈".equals(title)) {
|
||||
showFeedbackDialog();
|
||||
} else if ("联系客服".equals(title)) {
|
||||
showCustomerServiceDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAboutClick(String title) {
|
||||
if ("版本".equals(title)) {
|
||||
// 版本信息已在subtitle中显示,点击可显示详细信息
|
||||
showVersionInfoDialog();
|
||||
} else if ("用户协议".equals(title)) {
|
||||
showUserAgreementDialog();
|
||||
} else if ("隐私政策".equals(title)) {
|
||||
showPrivacyPolicyDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshItems() {
|
||||
if (PAGE_CLEAR_CACHE.equals(currentPage)) {
|
||||
// 异步加载缓存大小
|
||||
updateCacheSize();
|
||||
} else if (PAGE_ABOUT.equals(currentPage)) {
|
||||
// 更新版本信息
|
||||
updateVersionInfo();
|
||||
} else {
|
||||
adapter.submitList(buildItems(currentPage));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCacheSize() {
|
||||
new Thread(() -> {
|
||||
long cacheSize = CacheManager.getCacheSize(this);
|
||||
String sizeText = CacheManager.formatCacheSize(this, cacheSize);
|
||||
|
||||
runOnUiThread(() -> {
|
||||
List<MoreItem> items = new ArrayList<>();
|
||||
items.add(MoreItem.section("存储"));
|
||||
items.add(MoreItem.row("缓存大小", sizeText, R.drawable.ic_grid_24));
|
||||
items.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
|
||||
adapter.submitList(items);
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void updateVersionInfo() {
|
||||
try {
|
||||
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
String versionName = packageInfo.versionName;
|
||||
int versionCode = packageInfo.versionCode;
|
||||
String versionText = "Live Streaming " + versionName + " (Build " + versionCode + ")";
|
||||
|
||||
List<MoreItem> items = new ArrayList<>();
|
||||
items.add(MoreItem.section("应用信息"));
|
||||
items.add(MoreItem.row("版本", versionText, R.drawable.ic_menu_24));
|
||||
items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
|
||||
items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
|
||||
adapter.submitList(items);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
List<MoreItem> items = new ArrayList<>();
|
||||
items.add(MoreItem.section("应用信息"));
|
||||
items.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
|
||||
items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
|
||||
items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
|
||||
adapter.submitList(items);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveTitle(String page) {
|
||||
|
|
@ -105,29 +241,344 @@ public class SettingsPageActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
if (PAGE_CLEAR_CACHE.equals(page)) {
|
||||
list.add(MoreItem.section("存储"));
|
||||
list.add(MoreItem.row("缓存大小", "点击清理缓存(演示)", R.drawable.ic_grid_24));
|
||||
list.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
|
||||
return list;
|
||||
// 缓存大小将在updateCacheSize中异步更新
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (PAGE_HELP.equals(page)) {
|
||||
list.add(MoreItem.section("帮助"));
|
||||
list.add(MoreItem.row("常见问题", "问题解答与使用指南", R.drawable.ic_chat_24));
|
||||
list.add(MoreItem.row("意见反馈", "提交你的建议与问题", R.drawable.ic_chat_24));
|
||||
list.add(MoreItem.row("联系客服", "在线客服(演示)", R.drawable.ic_chat_24));
|
||||
list.add(MoreItem.row("联系客服", "在线客服", R.drawable.ic_chat_24));
|
||||
return list;
|
||||
}
|
||||
|
||||
if (PAGE_ABOUT.equals(page)) {
|
||||
list.add(MoreItem.section("应用信息"));
|
||||
list.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
|
||||
list.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
|
||||
list.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
|
||||
return list;
|
||||
// 版本信息将在updateVersionInfo中更新
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24));
|
||||
return list;
|
||||
}
|
||||
|
||||
// ========== 账号与安全相关对话框 ==========
|
||||
|
||||
private void showChangePasswordDialog() {
|
||||
try {
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("修改密码")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
Toast.makeText(this, "密码修改功能待接入后端", Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
} catch (Exception e) {
|
||||
// 如果布局文件不存在,使用简单对话框
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("修改密码")
|
||||
.setMessage("密码修改功能待接入后端\n\n" +
|
||||
"功能说明:\n" +
|
||||
"• 需要输入当前密码\n" +
|
||||
"• 设置新密码(至少8位)\n" +
|
||||
"• 确认新密码")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showBindPhoneDialog() {
|
||||
try {
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_bind_phone, null);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("绑定手机号")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("确定", (dialog, which) -> {
|
||||
Toast.makeText(this, "手机号绑定功能待接入后端", Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
} catch (Exception e) {
|
||||
// 如果布局文件不存在,使用简单对话框
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("绑定手机号")
|
||||
.setMessage("手机号绑定功能待接入后端\n\n" +
|
||||
"功能说明:\n" +
|
||||
"• 输入手机号码\n" +
|
||||
"• 获取并输入验证码\n" +
|
||||
"• 完成绑定")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showDeviceManagementDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("登录设备管理")
|
||||
.setMessage("当前登录设备:\n\n" +
|
||||
"• Android设备 (当前设备)\n" +
|
||||
" 最后登录:刚刚\n\n" +
|
||||
"功能说明:\n" +
|
||||
"• 查看所有已登录设备\n" +
|
||||
"• 可以远程退出其他设备\n" +
|
||||
"• 保护账号安全")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// ========== 隐私设置相关对话框 ==========
|
||||
|
||||
private void showBlacklistDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("黑名单")
|
||||
.setMessage("黑名单功能说明:\n\n" +
|
||||
"• 将用户加入黑名单后,对方将无法:\n" +
|
||||
" - 查看你的动态\n" +
|
||||
" - 给你发送消息\n" +
|
||||
" - 关注你\n\n" +
|
||||
"• 你仍然可以查看对方的公开信息\n\n" +
|
||||
"当前黑名单:0人\n\n" +
|
||||
"(此功能待接入后端)")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showPermissionManagementDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("权限管理")
|
||||
.setMessage("应用权限说明:\n\n" +
|
||||
"• 相机权限:用于直播和拍照\n" +
|
||||
"• 麦克风权限:用于语音搜索和直播\n" +
|
||||
"• 位置权限:用于附近的人功能\n" +
|
||||
"• 存储权限:用于保存图片和文件\n\n" +
|
||||
"你可以在系统设置中管理这些权限。")
|
||||
.setPositiveButton("打开系统设置", (dialog, which) -> {
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(android.net.Uri.parse("package:" + getPackageName()));
|
||||
startActivity(intent);
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showPrivacyPolicyDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("隐私政策")
|
||||
.setMessage("隐私政策\n\n" +
|
||||
"我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息。\n\n" +
|
||||
"1. 信息收集\n" +
|
||||
"我们可能收集以下信息:\n" +
|
||||
"• 账户信息(昵称、头像等)\n" +
|
||||
"• 设备信息(设备型号、系统版本等)\n" +
|
||||
"• 使用信息(观看记录、互动记录等)\n\n" +
|
||||
"2. 信息使用\n" +
|
||||
"我们使用收集的信息用于:\n" +
|
||||
"• 提供和改进服务\n" +
|
||||
"• 个性化推荐\n" +
|
||||
"• 安全保障\n\n" +
|
||||
"3. 信息保护\n" +
|
||||
"我们采用行业标准的安全措施保护您的信息。\n\n" +
|
||||
"4. 联系我们\n" +
|
||||
"如有疑问,请联系客服。\n\n" +
|
||||
"(完整版隐私政策待接入)")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// ========== 缓存清理相关对话框 ==========
|
||||
|
||||
private void showClearAllCacheDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("清理缓存")
|
||||
.setMessage("确定要清理所有缓存吗?\n\n" +
|
||||
"清理后可能需要重新加载部分内容。")
|
||||
.setPositiveButton("清理", (dialog, which) -> {
|
||||
showClearingProgress();
|
||||
CacheManager.clearAllCache(this, new CacheManager.OnCacheClearListener() {
|
||||
@Override
|
||||
public void onSuccess(long clearedSize) {
|
||||
hideClearingProgress();
|
||||
String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize);
|
||||
Toast.makeText(SettingsPageActivity.this,
|
||||
"清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show();
|
||||
updateCacheSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
hideClearingProgress();
|
||||
Toast.makeText(SettingsPageActivity.this,
|
||||
"清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showClearImageCacheDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("清理图片缓存")
|
||||
.setMessage("确定要清理图片缓存吗?\n\n" +
|
||||
"清理后图片需要重新加载。")
|
||||
.setPositiveButton("清理", (dialog, which) -> {
|
||||
showClearingProgress();
|
||||
CacheManager.clearImageCache(this, new CacheManager.OnCacheClearListener() {
|
||||
@Override
|
||||
public void onSuccess(long clearedSize) {
|
||||
hideClearingProgress();
|
||||
String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize);
|
||||
Toast.makeText(SettingsPageActivity.this,
|
||||
"清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show();
|
||||
updateCacheSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
hideClearingProgress();
|
||||
Toast.makeText(SettingsPageActivity.this,
|
||||
"清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private AlertDialog clearingDialog;
|
||||
|
||||
private void showClearingProgress() {
|
||||
ProgressBar progressBar = new ProgressBar(this);
|
||||
progressBar.setIndeterminate(true);
|
||||
clearingDialog = new AlertDialog.Builder(this)
|
||||
.setTitle("正在清理...")
|
||||
.setView(progressBar)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void hideClearingProgress() {
|
||||
if (clearingDialog != null && clearingDialog.isShowing()) {
|
||||
clearingDialog.dismiss();
|
||||
clearingDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 帮助与反馈相关对话框 ==========
|
||||
|
||||
private void showFAQDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("常见问题")
|
||||
.setMessage("常见问题解答\n\n" +
|
||||
"Q: 如何创建直播间?\n" +
|
||||
"A: 在首页点击右上角的创建按钮,填写直播间信息即可。\n\n" +
|
||||
"Q: 如何关注主播?\n" +
|
||||
"A: 在直播间或主播个人主页点击关注按钮。\n\n" +
|
||||
"Q: 如何发送消息?\n" +
|
||||
"A: 在消息页面选择会话,输入内容后发送。\n\n" +
|
||||
"Q: 如何修改个人资料?\n" +
|
||||
"A: 在个人中心点击编辑资料按钮。\n\n" +
|
||||
"Q: 如何清理缓存?\n" +
|
||||
"A: 在设置页面选择清理缓存。\n\n" +
|
||||
"更多问题请联系客服。")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showFeedbackDialog() {
|
||||
try {
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.dialog_feedback, null);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("意见反馈")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("提交", (dialog, which) -> {
|
||||
Toast.makeText(this, "感谢您的反馈!我们会认真处理。", Toast.LENGTH_SHORT).show();
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
} catch (Exception e) {
|
||||
// 如果布局文件不存在,使用简单对话框
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("意见反馈")
|
||||
.setMessage("意见反馈功能待接入后端\n\n" +
|
||||
"你可以通过以下方式反馈:\n" +
|
||||
"• 在对话框中输入反馈内容\n" +
|
||||
"• 选择反馈类型(问题/建议)\n" +
|
||||
"• 提交后我们会及时处理")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showCustomerServiceDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("联系客服")
|
||||
.setMessage("客服联系方式:\n\n" +
|
||||
"• 在线客服:工作日 9:00-18:00\n" +
|
||||
"• 客服邮箱:support@livestreaming.com\n" +
|
||||
"• 客服电话:400-123-4567\n\n" +
|
||||
"(此功能待接入后端)")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
// ========== 关于页面相关对话框 ==========
|
||||
|
||||
private void showVersionInfoDialog() {
|
||||
try {
|
||||
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
String versionName = packageInfo.versionName;
|
||||
int versionCode = packageInfo.versionCode;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("版本信息")
|
||||
.setMessage("应用名称:Live Streaming\n" +
|
||||
"版本号:" + versionName + "\n" +
|
||||
"构建号:" + versionCode + "\n" +
|
||||
"更新时间:2024年\n\n" +
|
||||
"© 2024 Live Streaming. All rights reserved.")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("版本信息")
|
||||
.setMessage("应用名称:Live Streaming\n" +
|
||||
"版本号:1.0\n\n" +
|
||||
"© 2024 Live Streaming. All rights reserved.")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void showUserAgreementDialog() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("用户协议")
|
||||
.setMessage("用户服务协议\n\n" +
|
||||
"欢迎使用Live Streaming直播应用。在使用本应用前,请仔细阅读以下条款。\n\n" +
|
||||
"1. 服务条款\n" +
|
||||
"使用本应用即表示您同意遵守本协议的所有条款。\n\n" +
|
||||
"2. 用户行为规范\n" +
|
||||
"• 不得发布违法违规内容\n" +
|
||||
"• 不得进行欺诈、骚扰等行为\n" +
|
||||
"• 尊重他人,文明互动\n\n" +
|
||||
"3. 知识产权\n" +
|
||||
"本应用的所有内容受知识产权法保护。\n\n" +
|
||||
"4. 免责声明\n" +
|
||||
"用户需自行承担使用本应用的风险。\n\n" +
|
||||
"5. 协议变更\n" +
|
||||
"我们保留随时修改本协议的权利。\n\n" +
|
||||
"(完整版用户协议待接入)")
|
||||
.setPositiveButton("确定", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (clearingDialog != null && clearingDialog.isShowing()) {
|
||||
clearingDialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 final List<Room> discoverAllRooms = new ArrayList<>();
|
||||
|
||||
private RoomsAdapter followRoomsAdapter;
|
||||
private final List<Room> followAllRooms = new ArrayList<>();
|
||||
|
||||
private BadgesAdapter badgesAdapter;
|
||||
|
||||
private MoreAdapter moreAdapter;
|
||||
|
|
@ -40,6 +43,18 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
private NearbyUsersAdapter addFriendAdapter;
|
||||
private final List<NearbyUser> addFriendAllUsers = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 设置关注容器的可见性
|
||||
*/
|
||||
private void setFollowContainerVisibility(int visibility) {
|
||||
View followContainer = binding.getRoot().findViewById(R.id.followContainer);
|
||||
if (followContainer != null) {
|
||||
followContainer.setVisibility(visibility);
|
||||
} else {
|
||||
binding.followRecyclerView.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
public static void start(Context context, String title) {
|
||||
Intent intent = new Intent(context, TabPlaceholderActivity.class);
|
||||
intent.putExtra(EXTRA_TITLE, title);
|
||||
|
|
@ -94,7 +109,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.genericScroll.setVisibility(View.VISIBLE);
|
||||
binding.discoverContainer.setVisibility(View.GONE);
|
||||
binding.genericPlaceholderContainer.setVisibility(View.VISIBLE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +121,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||
binding.addFriendContainer.setVisibility(View.GONE);
|
||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
ensureDiscoverSuggestions();
|
||||
|
|
@ -204,10 +219,23 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
|
||||
private void showFollowRooms() {
|
||||
binding.genericScroll.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.VISIBLE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
RoomsAdapter adapter = new RoomsAdapter(room -> {
|
||||
// 显示关注容器(包含搜索框和列表)
|
||||
setFollowContainerVisibility(View.VISIBLE);
|
||||
|
||||
ensureFollowRoomsAdapter();
|
||||
|
||||
// 初始化数据
|
||||
followAllRooms.clear();
|
||||
followAllRooms.addAll(buildFollowDemoRooms(16));
|
||||
followRoomsAdapter.submitList(new ArrayList<>(followAllRooms));
|
||||
}
|
||||
|
||||
private void ensureFollowRoomsAdapter() {
|
||||
if (followRoomsAdapter != null) return;
|
||||
|
||||
followRoomsAdapter = new RoomsAdapter(room -> {
|
||||
if (room == null) return;
|
||||
Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class);
|
||||
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
|
||||
|
|
@ -217,14 +245,76 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
|
||||
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
|
||||
binding.followRecyclerView.setLayoutManager(glm);
|
||||
binding.followRecyclerView.setAdapter(adapter);
|
||||
binding.followRecyclerView.setAdapter(followRoomsAdapter);
|
||||
|
||||
adapter.submitList(buildFollowDemoRooms(16));
|
||||
// 设置搜索框
|
||||
setupFollowSearchBox();
|
||||
}
|
||||
|
||||
private void setupFollowSearchBox() {
|
||||
android.widget.EditText searchInput = binding.getRoot().findViewById(R.id.followSearchInput);
|
||||
View clearButton = binding.getRoot().findViewById(R.id.followSearchClear);
|
||||
|
||||
if (searchInput == null) return;
|
||||
|
||||
// 添加文本监听
|
||||
searchInput.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String text = s != null ? s.toString() : "";
|
||||
// 显示/隐藏清空按钮
|
||||
if (clearButton != null) {
|
||||
clearButton.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
// 应用筛选
|
||||
applyFollowFilter(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
// 清空按钮点击事件
|
||||
if (clearButton != null) {
|
||||
clearButton.setOnClickListener(v -> {
|
||||
if (searchInput != null) {
|
||||
searchInput.setText("");
|
||||
searchInput.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFollowFilter(String query) {
|
||||
if (followRoomsAdapter == null || followAllRooms == null) return;
|
||||
|
||||
String searchQuery = query != null ? query.trim().toLowerCase() : "";
|
||||
if (searchQuery.isEmpty()) {
|
||||
followRoomsAdapter.submitList(new ArrayList<>(followAllRooms));
|
||||
return;
|
||||
}
|
||||
|
||||
List<Room> filtered = new ArrayList<>();
|
||||
for (Room r : followAllRooms) {
|
||||
if (r == null) continue;
|
||||
String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
|
||||
String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
|
||||
if (title.contains(searchQuery) || streamer.contains(searchQuery)) {
|
||||
filtered.add(r);
|
||||
}
|
||||
}
|
||||
|
||||
followRoomsAdapter.submitList(filtered);
|
||||
}
|
||||
|
||||
private void showNearbyUsers() {
|
||||
binding.genericScroll.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.VISIBLE);
|
||||
|
||||
NearbyUsersAdapter adapter = new NearbyUsersAdapter(user -> {
|
||||
|
|
@ -291,7 +381,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||
binding.addFriendContainer.setVisibility(View.GONE);
|
||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
ensureParkBadges();
|
||||
|
|
@ -341,7 +431,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||
binding.addFriendContainer.setVisibility(View.GONE);
|
||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
ensureMore();
|
||||
|
|
@ -461,7 +551,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.locationDiscoverContainer.setVisibility(View.VISIBLE);
|
||||
binding.addFriendContainer.setVisibility(View.GONE);
|
||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
|
||||
|
|
@ -478,7 +568,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
binding.locationDiscoverContainer.setVisibility(View.GONE);
|
||||
binding.addFriendContainer.setVisibility(View.VISIBLE);
|
||||
binding.genericPlaceholderContainer.setVisibility(View.GONE);
|
||||
binding.followRecyclerView.setVisibility(View.GONE);
|
||||
setFollowContainerVisibility(View.GONE);
|
||||
binding.nearbyRecyclerView.setVisibility(View.GONE);
|
||||
|
||||
ensureAddFriends();
|
||||
|
|
|
|||
|
|
@ -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.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/followRecyclerView"
|
||||
<LinearLayout
|
||||
android:id="@+id/followContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:visibility="gone" />
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="#14000000"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="14dp"
|
||||
android:paddingEnd="14dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="search"
|
||||
android:src="@android:drawable/ic_menu_search"
|
||||
android:tint="#666666" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/followSearchInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="搜索关注的直播间..."
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textColor="#111111"
|
||||
android:textColorHint="#999999"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/followSearchClear"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="clear"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:tint="#999999"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/followRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/nearbyRecyclerView"
|
||||
|
|
|
|||
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
|
||||
- ✅ 关注列表 (FollowingListActivity) - 基础UI
|
||||
- ✅ 获赞列表 (LikesListActivity) - 基础UI
|
||||
- ✅ 设置页面 (SettingsPageActivity) - 基础UI
|
||||
- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
|
||||
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
|
||||
|
||||
---
|
||||
|
|
@ -449,15 +449,25 @@
|
|||
- 验证ExoPlayer资源释放逻辑正确(已有实现)
|
||||
- 集成LeakCanary内存泄漏检测工具
|
||||
|
||||
#### 3. **统一加载状态** ⭐⭐
|
||||
#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
|
||||
**为什么重要**: 提升用户体验一致性
|
||||
|
||||
- [ ] 创建统一的加载状态组件
|
||||
- [ ] 实现骨架屏(Skeleton Screen)
|
||||
- [ ] 统一所有页面的加载提示
|
||||
- [ ] 添加加载动画
|
||||
- [x] 创建统一的加载状态组件
|
||||
- [x] 实现骨架屏(Skeleton Screen)
|
||||
- [x] 统一所有页面的加载提示
|
||||
- [x] 添加加载动画
|
||||
|
||||
**预计工作量**: 1-2天
|
||||
**完成内容**:
|
||||
- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
|
||||
- 实现了 `SkeletonView` 和 `SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
|
||||
- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持LoadingView、ProgressBar、骨架屏)
|
||||
- 更新了所有主要Activity使用统一的加载状态:
|
||||
- **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
|
||||
- **RoomDetailActivity**: 使用LoadingView显示加载状态
|
||||
- **SearchActivity**: 搜索时显示"搜索中..."加载状态
|
||||
- **MessagesActivity**: 加载消息列表时显示加载状态
|
||||
- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
|
||||
- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
|
||||
|
||||
#### 4. **数据持久化(本地)** ⭐⭐⭐
|
||||
**为什么重要**: 支持离线使用,提升用户体验
|
||||
|
|
@ -486,13 +496,21 @@
|
|||
|
||||
**预计工作量**: 5-7天
|
||||
|
||||
#### 6. **分类筛选功能(前端逻辑)** ⭐⭐
|
||||
#### 6. **分类筛选功能(前端逻辑)** ⭐⭐ ✅ 已完成
|
||||
**为什么重要**: UI已准备好,只需完善前端筛选逻辑
|
||||
|
||||
- [ ] 完善本地数据筛选逻辑
|
||||
- [ ] 实现筛选条件记忆(SharedPreferences)
|
||||
- [ ] 优化筛选性能
|
||||
- [ ] 添加筛选动画效果
|
||||
- [x] 完善本地数据筛选逻辑
|
||||
- [x] 实现筛选条件记忆(SharedPreferences)
|
||||
- [x] 优化筛选性能
|
||||
- [x] 添加筛选动画效果
|
||||
|
||||
**完成内容**:
|
||||
- 创建了 `CategoryFilterManager` 类,统一管理筛选逻辑
|
||||
- 实现了异步筛选功能,在后台线程执行筛选,避免阻塞UI线程
|
||||
- 使用 SharedPreferences 保存和恢复最后选中的分类
|
||||
- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
|
||||
- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
|
||||
- 在Activity恢复时自动恢复上次选中的分类标签
|
||||
|
||||
**预计工作量**: 1-2天
|
||||
|
||||
|
|
@ -516,14 +534,24 @@
|
|||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
#### 9. **消息功能完善(前端)** ⭐⭐
|
||||
#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
|
||||
**为什么重要**: 完善核心社交功能
|
||||
|
||||
- [ ] 完善消息列表UI
|
||||
- [ ] 实现消息状态显示(发送中、已发送、已读)
|
||||
- [ ] 优化消息列表性能
|
||||
- [ ] 添加消息操作(复制、删除等)
|
||||
- [ ] 实现消息搜索(本地)
|
||||
- [x] 完善消息列表UI
|
||||
- [x] 实现消息状态显示(发送中、已发送、已读)
|
||||
- [x] 优化消息列表性能
|
||||
- [x] 添加消息操作(复制、删除等)
|
||||
- [x] 实现消息搜索(本地)
|
||||
|
||||
**完成内容**:
|
||||
- 为ChatMessage添加了MessageStatus枚举(发送中、已发送、已读)
|
||||
- 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
|
||||
- 实现了消息长按菜单,支持复制和删除操作
|
||||
- 优化了DiffUtil,使用messageId作为唯一标识,提升列表更新性能
|
||||
- 在MessagesActivity中添加了搜索功能,支持按会话标题和消息内容搜索
|
||||
- 优化了消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
|
||||
- 添加了内存泄漏防护(onDestroy中清理延迟任务)
|
||||
- 优化了滚动行为(使用smoothScrollToPosition)
|
||||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
|
|
@ -585,33 +613,59 @@
|
|||
|
||||
**预计工作量**: 3-4天
|
||||
|
||||
#### 15. **分享功能(系统分享)** ⭐
|
||||
#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
|
||||
**为什么重要**: 提升传播能力
|
||||
|
||||
- [ ] 实现系统分享功能
|
||||
- [ ] 分享直播间(生成分享链接)
|
||||
- [ ] 分享个人主页
|
||||
- [ ] 分享图片(截图功能)
|
||||
- [x] 实现系统分享功能
|
||||
- [x] 分享直播间(生成分享链接)
|
||||
- [x] 分享个人主页
|
||||
|
||||
**完成内容**:
|
||||
- 创建了 `ShareUtils` 工具类,提供系统分享功能(分享文本、链接)
|
||||
- 在 `RoomDetailActivity` 中添加了分享按钮,支持分享直播间链接
|
||||
- 完善了 `ProfileActivity` 的分享功能,支持分享个人主页链接
|
||||
- 添加了分享图标资源 `ic_share_24.xml`
|
||||
- 所有分享功能都通过系统分享菜单,支持分享到微信、QQ、微博等应用
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
#### 16. **通知功能(前端UI)** ⭐
|
||||
#### 16. **通知功能(前端UI)** ⭐ ✅ 已完成
|
||||
**为什么重要**: 完善消息体系
|
||||
|
||||
- [ ] 实现通知列表页面
|
||||
- [ ] 实现通知分类
|
||||
- [ ] 实现通知设置页面
|
||||
- [ ] 添加本地通知(即使后端未就绪)
|
||||
- [x] 实现通知列表页面
|
||||
- [x] 实现通知分类
|
||||
- [x] 实现通知设置页面
|
||||
- [x] 添加本地通知(即使后端未就绪)
|
||||
|
||||
**完成内容**:
|
||||
- 创建了 `NotificationsActivity` 通知列表页面,支持分类筛选(全部、系统、互动、关注、私信、直播)
|
||||
- 创建了 `NotificationSettingsActivity` 通知设置页面,支持各类通知开关和免打扰设置
|
||||
- 创建了 `LocalNotificationManager` 本地通知管理器,支持发送各类通知
|
||||
- 创建了 `NotificationItem` 数据模型和 `NotificationsAdapter` 适配器
|
||||
- 在 `MainActivity` 中添加了通知图标点击事件
|
||||
- 在 `SettingsPageActivity` 中完善了通知设置入口
|
||||
- 在 `LiveStreamingApplication` 中初始化了通知渠道
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
#### 17. **设置页面完善** ⭐
|
||||
#### 17. **设置页面完善** ⭐ ✅ 已完成
|
||||
**为什么重要**: 完善应用配置
|
||||
|
||||
- [ ] 完善设置页面功能
|
||||
- [ ] 添加应用设置(缓存清理、关于等)
|
||||
- [ ] 添加账号设置UI
|
||||
- [ ] 添加隐私设置UI
|
||||
- [x] 完善设置页面功能
|
||||
- [x] 添加应用设置(缓存清理、关于等)
|
||||
- [x] 添加账号设置UI
|
||||
- [x] 添加隐私设置UI
|
||||
|
||||
**完成内容**:
|
||||
- 创建了 `CacheManager` 缓存管理工具类,支持获取缓存大小、清理所有缓存、清理图片缓存
|
||||
- 完善了 `SettingsPageActivity`,实现了所有设置页面的功能:
|
||||
- **账号与安全**: 修改密码对话框、绑定手机号对话框、登录设备管理对话框
|
||||
- **隐私设置**: 黑名单管理、权限管理(跳转系统设置)、隐私政策查看
|
||||
- **清理缓存**: 显示缓存大小、清理所有缓存、清理图片缓存(带进度提示)
|
||||
- **帮助与反馈**: 常见问题、意见反馈对话框、联系客服信息
|
||||
- **关于页面**: 自动获取版本信息、用户协议、隐私政策查看
|
||||
- 创建了对话框布局文件:`dialog_change_password.xml`、`dialog_bind_phone.xml`、`dialog_feedback.xml`
|
||||
- 所有功能都提供了友好的用户界面和提示信息
|
||||
|
||||
**预计工作量**: 2-3天
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user