diff --git a/android-app/app/src/main/assets/web/styles/yuanchi.css b/android-app/app/src/main/assets/web/styles/yuanchi.css index afa0f0e8..751f54d0 100644 --- a/android-app/app/src/main/assets/web/styles/yuanchi.css +++ b/android-app/app/src/main/assets/web/styles/yuanchi.css @@ -530,6 +530,7 @@ html, body { .sheet-content { display: none; padding: 0 16px 24px; + padding-bottom: 140px; /* 底部导航栏高度 + 额外空间 */ overflow-y: auto; max-height: calc(var(--sheet-expanded-height) - 50px); -webkit-overflow-scrolling: touch; diff --git a/android-app/app/src/main/assets/web/wishtree.html b/android-app/app/src/main/assets/web/wishtree.html index b5b0f889..d70bb89a 100644 --- a/android-app/app/src/main/assets/web/wishtree.html +++ b/android-app/app/src/main/assets/web/wishtree.html @@ -76,37 +76,37 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FeedAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/FeedAdapter.java index 95556929..6a369fb9 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/FeedAdapter.java +++ b/android-app/app/src/main/java/com/example/livestreaming/FeedAdapter.java @@ -194,7 +194,8 @@ public class FeedAdapter extends ListAdapter "title=" + work.getTitle() + ", userName=" + work.getUserName() + ", authorName=" + work.getAuthorName() + - ", likeCount=" + work.getLikeCount()); + ", likeCount=" + work.getLikeCount() + + ", content=" + (work.getContent() != null ? work.getContent().substring(0, Math.min(50, work.getContent().length())) : "null")); // 设置标题 String title = work.getTitle(); @@ -255,27 +256,83 @@ public class FeedAdapter extends ListAdapter toggleWorkLike(work, !currentLiked, binding); }); - // 加载封面图片 + // 判断是否为纯文字动态 String coverUrl = work.getCoverUrl(); - if (coverUrl == null || coverUrl.trim().isEmpty()) { - // 如果没有封面,尝试使用第一张图片 - if (work.getImageUrls() != null && !work.getImageUrls().isEmpty()) { - coverUrl = work.getImageUrls().get(0); + String videoUrl = work.getVideoUrl(); + boolean hasVideo = videoUrl != null && !videoUrl.trim().isEmpty(); + + // 检查是否有有效的图片列表 + boolean hasValidImageList = false; + if (work.getImageUrls() != null && !work.getImageUrls().isEmpty()) { + for (String imgUrl : work.getImageUrls()) { + if (imgUrl != null && !imgUrl.trim().isEmpty() && !isTextOnlyPlaceholder(imgUrl)) { + hasValidImageList = true; + break; + } + } + } + + // 获取文字内容 + String content = work.getContent(); + if (content == null || content.trim().isEmpty()) { + content = work.getDescription(); + } + boolean hasContent = content != null && !content.trim().isEmpty(); + + // 判断是否为纯文字动态: + // 核心逻辑:如果没有图片列表、没有视频,且有文字内容,就是纯文字动态 + // 不管coverUrl是什么,因为后端可能给纯文字动态设置了一个无效的封面URL + boolean isTextOnly = !hasValidImageList && !hasVideo && hasContent; + + // 检查封面是否为占位图(用于有图片的作品) + boolean isPlaceholderCover = isTextOnlyPlaceholder(coverUrl); + boolean hasCover = coverUrl != null && !coverUrl.trim().isEmpty() && !isPlaceholderCover; + + // 如果没有封面,尝试使用第一张有效图片作为封面 + String finalCoverUrl = coverUrl; + if (!hasCover && work.getImageUrls() != null) { + for (String imgUrl : work.getImageUrls()) { + if (imgUrl != null && !imgUrl.trim().isEmpty() && !isTextOnlyPlaceholder(imgUrl)) { + finalCoverUrl = imgUrl; + hasCover = true; + break; + } + } + } + + android.util.Log.d("FeedAdapter", "作品类型判断: id=" + work.getId() + + ", coverUrl=" + coverUrl + + ", hasValidImageList=" + hasValidImageList + + ", hasVideo=" + hasVideo + + ", hasContent=" + hasContent + + ", isTextOnly=" + isTextOnly); + + final String finalContent = content; + if (isTextOnly) { + // 纯文字动态:隐藏封面区域,显示文字内容区域 + binding.coverContainer.setVisibility(View.GONE); + binding.textContentContainer.setVisibility(View.VISIBLE); + binding.worksContentText.setText(finalContent); + + android.util.Log.d("FeedAdapter", "显示纯文字动态: " + finalContent.substring(0, Math.min(30, finalContent.length()))); + } else { + // 有封面的作品:显示封面区域,隐藏文字内容区域 + binding.coverContainer.setVisibility(View.VISIBLE); + binding.textContentContainer.setVisibility(View.GONE); + + if (hasCover && finalCoverUrl != null) { + Glide.with(binding.worksCoverImage) + .load(finalCoverUrl) + .placeholder(R.drawable.bg_cover_placeholder) + .centerCrop() + .into(binding.worksCoverImage); + } else { + binding.worksCoverImage.setImageResource(R.drawable.bg_cover_placeholder); } } - if (coverUrl != null && !coverUrl.trim().isEmpty()) { - Glide.with(binding.worksCoverImage) - .load(coverUrl) - .placeholder(R.drawable.bg_cover_placeholder) - .centerCrop() - .into(binding.worksCoverImage); - } else { - binding.worksCoverImage.setImageResource(R.drawable.bg_cover_placeholder); - } - // 显示作品类型图标(视频类型显示播放图标) - if ("VIDEO".equals(work.getType())) { + if ("VIDEO".equals(work.getType()) || hasVideo) { binding.worksTypeIcon.setVisibility(View.VISIBLE); } else { binding.worksTypeIcon.setVisibility(View.GONE); @@ -430,4 +487,46 @@ public class FeedAdapter extends ListAdapter } }); } + + /** + * 判断URL是否为纯文字动态的占位图 + * 后端为纯文字动态设置的特殊封面地址 + */ + private static boolean isTextOnlyPlaceholder(String url) { + if (url == null || url.trim().isEmpty()) { + return true; // 空URL也视为占位图 + } + + // 特殊占位图地址列表 + String[] placeholderPatterns = { + "TEXT_ONLY_DYNAMIC_PLACEHOLDER", // Android端发布纯文字动态时设置的标识 + "text_only_placeholder", // 纯文字动态占位图标识 + "placeholder/text", // 文字占位图路径 + "default_text_cover", // 默认文字封面 + "no_image_placeholder", // 无图片占位图 + "/placeholder.", // 通用占位图 + "crmebimage/public/content/", // 后端默认内容图片路径 + "default_cover", // 默认封面 + "empty_cover", // 空封面 + }; + + String lowerUrl = url.toLowerCase(); + for (String pattern : placeholderPatterns) { + if (lowerUrl.contains(pattern.toLowerCase())) { + return true; + } + } + + // 检查是否为空白图片或默认图片(根据文件名判断) + if (lowerUrl.endsWith("/default.png") || + lowerUrl.endsWith("/default.jpg") || + lowerUrl.endsWith("/placeholder.png") || + lowerUrl.endsWith("/placeholder.jpg") || + lowerUrl.endsWith("/empty.png") || + lowerUrl.endsWith("/empty.jpg")) { + return true; + } + + return false; + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java b/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java index 28c4f7f4..b2b1e58d 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java @@ -115,12 +115,13 @@ public class LikesListActivity extends AppCompatActivity { for (WorksResponse work : works) { Integer likeCount = work.getLikeCount(); if (likeCount != null && likeCount > 0) { - ConversationItem item = new ConversationItem(); - item.setId(String.valueOf(work.getId())); - item.setTitle(work.getTitle() != null ? work.getTitle() : "作品"); - item.setLastMessage(likeCount + "人点赞了这个作品"); - item.setAvatarUrl(work.getCoverImage()); - item.setUnreadCount(likeCount); + String id = String.valueOf(work.getId()); + String title = work.getTitle() != null ? work.getTitle() : "作品"; + String lastMessage = likeCount + "人点赞了这个作品"; + String timeText = ""; + + ConversationItem item = new ConversationItem(id, title, lastMessage, timeText, likeCount, false); + item.setAvatarUrl(work.getCoverUrl()); items.add(item); } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index 33731ff1..89574a7b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -550,8 +550,13 @@ public class MainActivity extends AppCompatActivity { // 加载热门作品 refreshHotWorks(); } else { - // 应用其他分类筛选(带动画) - applyCategoryFilterWithAnimation(currentCategory); + // 如果数据为空,先加载数据 + if (allFeedItems.isEmpty()) { + fetchDiscoverRooms(); + } else { + // 应用其他分类筛选(带动画) + applyCategoryFilterWithAnimation(currentCategory); + } } } @@ -572,8 +577,13 @@ public class MainActivity extends AppCompatActivity { // 刷新热门作品 refreshHotWorks(); } else { - // 应用其他分类筛选(带动画) - applyCategoryFilterWithAnimation(currentCategory); + // 如果数据为空,先加载数据;否则刷新 + if (allFeedItems.isEmpty()) { + fetchDiscoverRooms(); + } else { + // 重新加载数据 + fetchDiscoverRooms(); + } } } }); @@ -1883,62 +1893,95 @@ public class MainActivity extends AppCompatActivity { // 增加请求ID,确保只有最新的筛选结果被应用 final int requestId = ++filterRequestId; + // 如果是"推荐"或"全部",显示所有数据 + if ("推荐".equals(c) || "全部".equals(c)) { + binding.roomsRecyclerView.animate() + .alpha(0.7f) + .setDuration(100) + .withEndAction(() -> { + if (requestId != filterRequestId) return; + + adapter.submitList(new ArrayList<>(allFeedItems), () -> { + if (requestId != filterRequestId) return; + binding.roomsRecyclerView.animate() + .alpha(1.0f) + .setDuration(200) + .start(); + }); + + if (allFeedItems.isEmpty()) { + showEmptyState("暂无内容"); + } else { + hideEmptyState(); + } + }) + .start(); + return; + } + // 显示加载状态(如果数据量较大) - if (allRooms.size() > 50) { + if (allFeedItems.size() > 50) { binding.loading.setVisibility(View.VISIBLE); } - // 使用筛选管理器异步筛选 - if (filterManager != null) { - filterManager.filterRoomsAsync(allRooms, c, filteredRooms -> { - // 检查这个结果是否仍然是最新的请求 - if (requestId != filterRequestId) { - // 这是一个旧的请求结果,忽略它 - return; + // 筛选FeedItem列表 + List filtered = new ArrayList<>(); + for (FeedItem item : allFeedItems) { + if (item == null) continue; + + // 根据类型获取分类 + String itemCategory = null; + if (item.getType() == FeedItem.TYPE_ROOM && item.getRoom() != null) { + Room room = item.getRoom(); + itemCategory = room.getCategoryName(); + if (itemCategory == null || itemCategory.isEmpty()) { + itemCategory = room.getType(); } - - // 隐藏加载状态 - binding.loading.setVisibility(View.GONE); - - // 添加淡入动画 - binding.roomsRecyclerView.animate() - .alpha(0.7f) - .setDuration(100) - .withEndAction(() -> { - // 再次检查请求ID(防止在动画期间又有新的筛选请求) - if (requestId != filterRequestId) { - return; - } - - // 转换为FeedItem列表 - List feedItems = new ArrayList<>(); - for (Room room : filteredRooms) { - feedItems.add(FeedItem.fromRoom(room)); - } - - // 更新列表数据(ListAdapter会自动处理DiffUtil动画) - adapter.submitList(feedItems, () -> { - // 最后一次检查请求ID - if (requestId != filterRequestId) { - return; - } - - // 数据更新完成后,恢复透明度并添加淡入效果 - binding.roomsRecyclerView.animate() - .alpha(1.0f) - .setDuration(200) - .start(); - }); - - // 更新空状态 - updateEmptyStateForList(filteredRooms); - }) - .start(); - }); - } else { - // 降级到同步筛选(如果筛选管理器未初始化) - applyCategoryFilterSync(c); + } else if (item.getType() == FeedItem.TYPE_WORK && item.getWork() != null) { + // 作品按分类筛选 + WorksResponse work = item.getWork(); + itemCategory = work.getCategoryName(); + if (itemCategory == null || itemCategory.isEmpty()) { + itemCategory = work.getCategory(); + } + } else if (item.getType() == FeedItem.TYPE_CHATROOM) { + // 聊天室暂时不按分类筛选,全部显示 + filtered.add(item); + continue; + } + + // 如果分类匹配,添加到筛选结果 + if (c.equals(itemCategory)) { + filtered.add(item); + } } + + // 隐藏加载状态 + binding.loading.setVisibility(View.GONE); + + // 添加淡入动画 + final List finalFiltered = filtered; + binding.roomsRecyclerView.animate() + .alpha(0.7f) + .setDuration(100) + .withEndAction(() -> { + if (requestId != filterRequestId) return; + + adapter.submitList(finalFiltered, () -> { + if (requestId != filterRequestId) return; + binding.roomsRecyclerView.animate() + .alpha(1.0f) + .setDuration(200) + .start(); + }); + + if (finalFiltered.isEmpty()) { + showEmptyState("该分类暂无内容"); + } else { + hideEmptyState(); + } + }) + .start(); } /** @@ -2529,8 +2572,15 @@ public class MainActivity extends AppCompatActivity { Log.d(TAG, "checkAndDisplayFeed() 提交 " + allFeedItems.size() + " 项到适配器"); hideEmptyState(); if (binding.loading != null) binding.loading.setVisibility(View.GONE); - // 提交数据到适配器 - adapter.submitList(new ArrayList<>(allFeedItems)); + + // 根据当前分类筛选数据 + if ("热门".equals(currentCategory)) { + // 热门分类使用热门作品数据 + // 不做任何操作,热门数据由 refreshHotWorks 加载 + } else { + // 其他分类应用筛选 + applyCategoryFilterWithAnimation(currentCategory); + } } // 更新发现页面的空状态 @@ -2855,6 +2905,11 @@ public class MainActivity extends AppCompatActivity { binding.roomsRecyclerView.setVisibility(View.VISIBLE); } + // 预加载所有作品和直播间数据(用于其他分类筛选) + if (allFeedItems.isEmpty()) { + fetchDiscoverRooms(); + } + // 使用房间适配器显示推荐内容 if (adapter != null) { // 默认选中"热门"Tab并加载热门作品 @@ -4141,9 +4196,26 @@ public class MainActivity extends AppCompatActivity { /** * 从本地加载我的频道配置 + * 注意:如果本地配置的分类名称与后端不匹配,需要重置为默认配置 */ private void loadMyChannelsFromPrefs() { android.content.SharedPreferences prefs = getSharedPreferences("channel_prefs", MODE_PRIVATE); + + // 检查配置版本,如果版本不匹配则重置配置 + int configVersion = prefs.getInt("channel_config_version", 0); + int currentVersion = 2; // 版本2:分类名称与后端匹配(娱乐、游戏、音乐、户外) + + if (configVersion < currentVersion) { + // 版本不匹配,清除旧配置,使用新的默认配置 + Log.d(TAG, "频道配置版本过旧(" + configVersion + " < " + currentVersion + "),重置为默认配置"); + prefs.edit() + .remove("my_channels") + .putInt("channel_config_version", currentVersion) + .apply(); + initDefaultMyChannels(); + return; + } + String channelsJson = prefs.getString("my_channels", ""); if (!channelsJson.isEmpty()) { @@ -4180,14 +4252,17 @@ public class MainActivity extends AppCompatActivity { /** * 初始化默认的我的频道配置 + * 注意:分类名称必须与后端数据库 eb_live_room_category 表中的 name 字段一致 + * 后端分类:娱乐、游戏、音乐、户外、美食、体育、教育、科技 */ private void initDefaultMyChannels() { myChannels.clear(); myChannels.add(new ChannelTagAdapter.ChannelTag(0, "推荐")); - myChannels.add(new ChannelTagAdapter.ChannelTag(1, "直播")); - myChannels.add(new ChannelTagAdapter.ChannelTag(2, "视频")); + myChannels.add(new ChannelTagAdapter.ChannelTag(1, "娱乐")); + myChannels.add(new ChannelTagAdapter.ChannelTag(2, "游戏")); myChannels.add(new ChannelTagAdapter.ChannelTag(3, "音乐")); - Log.d(TAG, "使用默认我的频道配置"); + myChannels.add(new ChannelTagAdapter.ChannelTag(4, "户外")); + Log.d(TAG, "使用默认我的频道配置(与后端分类匹配)"); } /** diff --git a/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java index 40eea510..39612e3f 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/PublishCenterActivity.java @@ -253,7 +253,12 @@ public class PublishCenterActivity extends AppCompatActivity { request.setDescription(content); request.setType("IMAGE"); request.setImageUrls(new ArrayList<>()); - request.setCoverUrl(coverUrl != null ? coverUrl : ""); + // 如果没有封面,设置特殊标识URL,表示这是纯文字动态 + if (coverUrl == null || coverUrl.isEmpty()) { + request.setCoverUrl("TEXT_ONLY_DYNAMIC_PLACEHOLDER"); + } else { + request.setCoverUrl(coverUrl); + } ApiClient.getService(this).publishWork(request).enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { diff --git a/android-app/app/src/main/java/com/example/livestreaming/WishTagAnimator.java b/android-app/app/src/main/java/com/example/livestreaming/WishTagAnimator.java index 3b5c764f..9ad5c98b 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WishTagAnimator.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WishTagAnimator.java @@ -166,4 +166,148 @@ public class WishTagAnimator { animatorSet.start(); } + + /** + * 渐隐旧心愿并渐显新心愿的替换动画 + * @param wishTag 心愿牌View + * @param newText 新心愿文字 + * @param newBackground 新心愿背景资源 + * @param onComplete 动画完成回调 + */ + public static void animateFadeOutAndReplace(android.widget.TextView wishTag, String newText, + int newBackground, Runnable onComplete) { + // 第一阶段:旧心愿渐隐 + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(wishTag, "alpha", 1f, 0f); + fadeOut.setDuration(600); + fadeOut.setInterpolator(new AccelerateDecelerateInterpolator()); + + fadeOut.addListener(new android.animation.AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(android.animation.Animator animation) { + // 更新内容 + wishTag.setText(newText); + wishTag.setBackgroundResource(newBackground); + + // 第二阶段:新心愿渐显 + ObjectAnimator fadeIn = ObjectAnimator.ofFloat(wishTag, "alpha", 0f, 1f); + fadeIn.setDuration(600); + fadeIn.setInterpolator(new AccelerateDecelerateInterpolator()); + + // 添加轻微缩放效果 + ObjectAnimator scaleX = ObjectAnimator.ofFloat(wishTag, "scaleX", 0.8f, 1.05f, 1f); + scaleX.setDuration(600); + + ObjectAnimator scaleY = ObjectAnimator.ofFloat(wishTag, "scaleY", 0.8f, 1.05f, 1f); + scaleY.setDuration(600); + + AnimatorSet fadeInSet = new AnimatorSet(); + fadeInSet.playTogether(fadeIn, scaleX, scaleY); + + fadeInSet.addListener(new android.animation.AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(android.animation.Animator animation) { + // 开始摇摆动画 + startSwingAnimation(wishTag); + if (onComplete != null) { + onComplete.run(); + } + } + }); + + fadeInSet.start(); + } + }); + + fadeOut.start(); + } + + /** + * 替换动画:旧心愿渐隐消失,新心愿从底部飘上来 + * @param wishTag 心愿牌View + * @param newText 新心愿文字 + * @param newBackground 新心愿背景资源 + * @param startX 新心愿起始X坐标 + * @param startY 新心愿起始Y坐标 + * @param targetX 目标X坐标 + * @param targetY 目标Y坐标 + * @param onComplete 动画完成回调 + */ + public static void animateReplaceWithFlyIn(android.widget.TextView wishTag, String newText, + int newBackground, float startX, float startY, + float targetX, float targetY, Runnable onComplete) { + // 保存原始位置 + final float originalX = wishTag.getX(); + final float originalY = wishTag.getY(); + + // 第一阶段:旧心愿渐隐消失(800ms) + ObjectAnimator fadeOut = ObjectAnimator.ofFloat(wishTag, "alpha", 1f, 0f); + fadeOut.setDuration(800); + fadeOut.setInterpolator(new AccelerateDecelerateInterpolator()); + + fadeOut.addListener(new android.animation.AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(android.animation.Animator animation) { + // 更新内容和背景 + wishTag.setText(newText); + wishTag.setBackgroundResource(newBackground); + + // 设置到起始位置(屏幕底部) + wishTag.setX(startX); + wishTag.setY(startY); + wishTag.setAlpha(0f); + wishTag.setScaleX(0.5f); + wishTag.setScaleY(0.5f); + wishTag.setVisibility(View.VISIBLE); + + // 第二阶段:新心愿从底部飘上来(1200ms) + AnimatorSet flyInSet = new AnimatorSet(); + + // 移动到目标位置 + ObjectAnimator moveX = ObjectAnimator.ofFloat(wishTag, "x", startX, originalX); + moveX.setDuration(1200); + moveX.setInterpolator(new DecelerateInterpolator(1.5f)); + + ObjectAnimator moveY = ObjectAnimator.ofFloat(wishTag, "y", startY, originalY); + moveY.setDuration(1200); + moveY.setInterpolator(new DecelerateInterpolator(2f)); + + // 渐显 + ObjectAnimator fadeIn = ObjectAnimator.ofFloat(wishTag, "alpha", 0f, 1f); + fadeIn.setDuration(800); + + // 缩放 + ObjectAnimator scaleX = ObjectAnimator.ofFloat(wishTag, "scaleX", 0.5f, 1.1f, 1f); + scaleX.setDuration(1200); + scaleX.setInterpolator(new OvershootInterpolator(1.2f)); + + ObjectAnimator scaleY = ObjectAnimator.ofFloat(wishTag, "scaleY", 0.5f, 1.1f, 1f); + scaleY.setDuration(1200); + scaleY.setInterpolator(new OvershootInterpolator(1.2f)); + + // 轻微旋转 + ObjectAnimator rotate = ObjectAnimator.ofFloat(wishTag, "rotation", 0f, -5f, 5f, -3f, 3f, 0f); + rotate.setDuration(1500); + + flyInSet.playTogether(moveX, moveY, fadeIn, scaleX, scaleY, rotate); + + flyInSet.addListener(new android.animation.AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(android.animation.Animator animation) { + // 确保位置正确 + wishTag.setX(originalX); + wishTag.setY(originalY); + // 开始摇摆动画 + startSwingAnimation(wishTag); + if (onComplete != null) { + onComplete.run(); + } + } + }); + + flyInSet.start(); + } + }); + + fadeOut.start(); + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java index 7cb0e2fa..7b7720e3 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java @@ -33,8 +33,10 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Queue; import java.util.TimeZone; import retrofit2.Call; @@ -55,6 +57,9 @@ public class WishTreeActivity extends AppCompatActivity { // 心愿数据 private List myWishes = new ArrayList<>(); private WishtreeResponse.Festival currentFestival; + + // 心愿总数(从服务器获取) + private int totalWishCount = 0; // 心愿牌View数组 private TextView[] wishTags; @@ -72,6 +77,9 @@ public class WishTreeActivity extends AppCompatActivity { R.drawable.bg_wish_tag_blue, R.drawable.bg_wish_tag_pink }; + + // 心愿牌位置队列(先进先出,记录心愿牌索引的添加顺序) + private Queue wishTagQueue = new LinkedList<>(); public static void start(Context context) { context.startActivity(new Intent(context, WishTreeActivity.class)); @@ -182,17 +190,24 @@ public class WishTreeActivity extends AppCompatActivity { * 从服务端加载我的心愿 */ private void loadMyWishes() { + // 重新加载时重置队列 + resetWishTagQueue(); + apiService.getMyWishes(1, 10).enqueue(new Callback>() { @Override public void onResponse(@NonNull Call> call, @NonNull Response> response) { if (response.isSuccessful() && response.body() != null && response.body().getData() != null) { - myWishes = response.body().getData().list; + WishtreeResponse.WishPage page = response.body().getData(); + myWishes = page.list; if (myWishes == null) myWishes = new ArrayList<>(); + // 使用服务器返回的总数 + totalWishCount = page.total; updateWishTags(false); updateWishCount(); } else { myWishes = new ArrayList<>(); + totalWishCount = 0; updateWishTags(false); updateWishCount(); } @@ -210,6 +225,9 @@ public class WishTreeActivity extends AppCompatActivity { * @param animate 是否播放动画 */ private void updateWishTags(boolean animate) { + // 只在队列为空时初始化(首次加载或重新加载) + boolean needInitQueue = wishTagQueue.isEmpty(); + for (int i = 0; i < wishTags.length; i++) { if (wishTags[i] == null) continue; @@ -220,6 +238,11 @@ public class WishTreeActivity extends AppCompatActivity { wishTags[i].setText(verticalText); wishTags[i].setBackgroundResource(tagBackgrounds[i % tagBackgrounds.length]); + // 只在首次加载时按顺序加入队列 + if (needInitQueue) { + wishTagQueue.offer(i); + } + if (animate) { WishTagAnimator.animateAppear(wishTags[i], i * 150L); } else { @@ -232,6 +255,13 @@ public class WishTreeActivity extends AppCompatActivity { } } } + + /** + * 重新加载时重置队列 + */ + private void resetWishTagQueue() { + wishTagQueue.clear(); + } /** * 格式化文字为竖向显示 @@ -252,9 +282,9 @@ public class WishTreeActivity extends AppCompatActivity { * 更新祈愿值显示 */ private void updateWishCount() { - int count = myWishes != null ? myWishes.size() : 0; - binding.tvWishCount.setText("祈愿值:" + count + "/100"); - binding.progressWish.setProgress(count); + // 使用服务器返回的总数 + binding.tvWishCount.setText("祈愿值:" + totalWishCount + "/100"); + binding.progressWish.setProgress(Math.min(totalWishCount, 100)); } /** @@ -350,6 +380,9 @@ public class WishTreeActivity extends AppCompatActivity { if (newWish != null) { myWishes.add(0, newWish); + // 增加总数 + totalWishCount++; + // 播放飘动动画 animateNewWishTag(newWish); @@ -371,8 +404,88 @@ public class WishTreeActivity extends AppCompatActivity { /** * 播放新心愿飘到树上的动画 + * 如果心愿数量超过10个,会替换最早的心愿(旧心愿渐隐,新心愿从底部飘上来) */ private void animateNewWishTag(WishtreeResponse.Wish wish) { + FrameLayout container = findViewById(R.id.wishTagsContainer); + if (container == null) return; + + int currentCount = myWishes.size(); + + if (currentCount <= wishTags.length) { + // 心愿数量不超过10个,找一个空位置添加 + int targetIndex = findEmptyOrRandomSlot(); + if (targetIndex < 0 || targetIndex >= wishTags.length) return; + + TextView targetTag = wishTags[targetIndex]; + if (targetTag == null) return; + + // 设置心愿内容 + String verticalText = formatVerticalText(wish.content, 5); + targetTag.setText(verticalText); + targetTag.setBackgroundResource(tagBackgrounds[targetIndex % tagBackgrounds.length]); + + // 将新位置加入队列末尾 + wishTagQueue.offer(targetIndex); + + // 从屏幕底部中央飘到目标位置 + float startX = container.getWidth() / 2f - targetTag.getWidth() / 2f; + float startY = container.getHeight(); + float targetX = targetTag.getX(); + float targetY = targetTag.getY(); + + WishTagAnimator.animateWishToTree(targetTag, startX, startY, targetX, targetY, 1500, () -> { + // 动画完成后启动摇摆动画 + WishTagAnimator.startSwingAnimation(targetTag); + }); + } else { + // 心愿数量超过10个,从队列头部取出最早加入的心愿牌位置 + Integer oldestIndex = wishTagQueue.poll(); + if (oldestIndex == null) { + oldestIndex = 0; // 队列为空时默认第一个 + } + + TextView oldTag = wishTags[oldestIndex]; + if (oldTag == null) return; + + // 将这个位置重新加入队列末尾(因为新心愿会占用这个位置) + wishTagQueue.offer(oldestIndex); + + // 设置新心愿内容 + String verticalText = formatVerticalText(wish.content, 5); + int newBackground = tagBackgrounds[oldestIndex % tagBackgrounds.length]; + + // 获取目标位置 + float targetX = oldTag.getX(); + float targetY = oldTag.getY(); + float startX = container.getWidth() / 2f - oldTag.getWidth() / 2f; + float startY = container.getHeight(); + + // 旧心愿渐隐消失,同时新心愿从底部飘上来 + WishTagAnimator.animateReplaceWithFlyIn(oldTag, verticalText, newBackground, + startX, startY, targetX, targetY, () -> { + // 动画完成后启动摇摆动画 + WishTagAnimator.startSwingAnimation(oldTag); + }); + } + } + + /** + * 找一个空位置或随机位置 + */ + private int findEmptyOrRandomSlot() { + // 先找空位置 + for (int i = 0; i < wishTags.length; i++) { + if (wishTags[i] != null && wishTags[i].getVisibility() == View.GONE) { + return i; + } + } + // 没有空位置,随机选一个 + return (int) (Math.random() * wishTags.length); + } + + // 保留原来的方法签名用于兼容 + private void animateNewWishTagLegacy(WishtreeResponse.Wish wish) { // 找到第一个可用的心愿牌位置 int targetIndex = Math.min(myWishes.size() - 1, wishTags.length - 1); if (targetIndex < 0 || targetIndex >= wishTags.length) return; diff --git a/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java index fa120a80..4b3e0192 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/WorkDetailActivity.java @@ -112,11 +112,57 @@ public class WorkDetailActivity extends AppCompatActivity { // 设置作品描述 setupDescription(); - // 显示媒体内容 - if (workItem.getType() == WorkItem.WorkType.VIDEO) { - // 显示视频(暂时显示封面图,后续可以集成ExoPlayer) + // 判断内容类型 + boolean hasVideo = workItem.getType() == WorkItem.WorkType.VIDEO || + !TextUtils.isEmpty(workItem.getVideoUrl()); + + // 检查是否有有效的图片列表(不包括封面,因为封面可能是无效的占位图) + boolean hasValidImageList = false; + List imageUrls = workItem.getImageUrls(); + if (imageUrls != null && !imageUrls.isEmpty()) { + for (String url : imageUrls) { + if (url != null && !url.isEmpty() && !isTextOnlyPlaceholder(url)) { + hasValidImageList = true; + break; + } + } + } + + // 检查imageUris + List imageUris = workItem.getImageUris(); + if (!hasValidImageList && imageUris != null && !imageUris.isEmpty()) { + for (Uri uri : imageUris) { + if (uri != null && !isTextOnlyPlaceholder(uri.toString())) { + hasValidImageList = true; + break; + } + } + } + + String content = workItem.getDescription(); + boolean hasContent = !TextUtils.isEmpty(content); + + // 判断是否为纯文字动态: + // 核心逻辑:如果没有图片列表、没有视频,且有文字内容,就是纯文字动态 + boolean isTextOnly = !hasValidImageList && !hasVideo && hasContent; + + android.util.Log.d("WorkDetail", "内容类型判断: hasVideo=" + hasVideo + + ", hasValidImageList=" + hasValidImageList + ", hasContent=" + hasContent + + ", isTextOnly=" + isTextOnly); + + if (isTextOnly) { + // 纯文字动态:显示文字内容区域 + binding.imageViewPager.setVisibility(View.GONE); + binding.videoContainer.setVisibility(View.GONE); + binding.textContentContainer.setVisibility(View.VISIBLE); + binding.textContentText.setText(content); + + android.util.Log.d("WorkDetail", "显示纯文字动态: " + content); + } else if (hasVideo) { + // 显示视频 binding.imageViewPager.setVisibility(View.GONE); binding.videoContainer.setVisibility(View.VISIBLE); + binding.textContentContainer.setVisibility(View.GONE); // 使用封面URI或视频URI显示预览图 Uri videoUri = workItem.getVideoUri(); @@ -162,16 +208,18 @@ public class WorkDetailActivity extends AppCompatActivity { // 显示图片 binding.videoContainer.setVisibility(View.GONE); binding.imageViewPager.setVisibility(View.VISIBLE); + binding.textContentContainer.setVisibility(View.GONE); - List imageUris = workItem.getImageUris(); + // 重新获取imageUris(不重复定义变量) + List displayImageUris = workItem.getImageUris(); // 如果 imageUris 为空,尝试从 imageUrls 恢复 - if ((imageUris == null || imageUris.isEmpty()) && workItem.getImageUrls() != null && !workItem.getImageUrls().isEmpty()) { - imageUris = new ArrayList<>(); + if ((displayImageUris == null || displayImageUris.isEmpty()) && workItem.getImageUrls() != null && !workItem.getImageUrls().isEmpty()) { + displayImageUris = new ArrayList<>(); for (String url : workItem.getImageUrls()) { if (url != null && !url.isEmpty()) { try { Uri uri = Uri.parse(url); - imageUris.add(uri); + displayImageUris.add(uri); android.util.Log.d("WorkDetail", "恢复图片URI: " + uri); } catch (Exception e) { android.util.Log.e("WorkDetail", "解析图片URI失败: " + url, e); @@ -181,28 +229,35 @@ public class WorkDetailActivity extends AppCompatActivity { } // 如果还是没有图片,尝试使用封面 - if ((imageUris == null || imageUris.isEmpty()) && workItem.getCoverUri() != null) { - imageUris = new ArrayList<>(); - imageUris.add(workItem.getCoverUri()); + if ((displayImageUris == null || displayImageUris.isEmpty()) && workItem.getCoverUri() != null) { + displayImageUris = new ArrayList<>(); + displayImageUris.add(workItem.getCoverUri()); android.util.Log.d("WorkDetail", "使用封面作为图片: " + workItem.getCoverUri()); - } else if ((imageUris == null || imageUris.isEmpty()) && !TextUtils.isEmpty(workItem.getCoverUrl())) { + } else if ((displayImageUris == null || displayImageUris.isEmpty()) && !TextUtils.isEmpty(workItem.getCoverUrl())) { try { - imageUris = new ArrayList<>(); - imageUris.add(Uri.parse(workItem.getCoverUrl())); + displayImageUris = new ArrayList<>(); + displayImageUris.add(Uri.parse(workItem.getCoverUrl())); android.util.Log.d("WorkDetail", "使用封面URL作为图片: " + workItem.getCoverUrl()); } catch (Exception e) { android.util.Log.e("WorkDetail", "解析封面URL失败", e); } } - if (imageUris != null && !imageUris.isEmpty()) { - android.util.Log.d("WorkDetail", "显示图片数量: " + imageUris.size()); - imageAdapter = new ImagePagerAdapter(imageUris); + if (displayImageUris != null && !displayImageUris.isEmpty()) { + android.util.Log.d("WorkDetail", "显示图片数量: " + displayImageUris.size()); + imageAdapter = new ImagePagerAdapter(displayImageUris); binding.imageViewPager.setAdapter(imageAdapter); } else { android.util.Log.w("WorkDetail", "没有可显示的图片"); - binding.imageViewPager.setVisibility(View.GONE); - Toast.makeText(this, "图片加载失败", Toast.LENGTH_SHORT).show(); + // 如果没有图片但有内容,显示纯文字 + if (hasContent) { + binding.imageViewPager.setVisibility(View.GONE); + binding.textContentContainer.setVisibility(View.VISIBLE); + binding.textContentText.setText(content); + } else { + binding.imageViewPager.setVisibility(View.GONE); + Toast.makeText(this, "暂无内容", Toast.LENGTH_SHORT).show(); + } } } } @@ -1322,5 +1377,47 @@ public class WorkDetailActivity extends AppCompatActivity { } } } + + /** + * 判断URL是否为纯文字动态的占位图 + * 后端为纯文字动态设置的特殊封面地址 + */ + private boolean isTextOnlyPlaceholder(String url) { + if (url == null || url.trim().isEmpty()) { + return true; // 空URL也视为占位图 + } + + // 特殊占位图地址列表 + String[] placeholderPatterns = { + "TEXT_ONLY_DYNAMIC_PLACEHOLDER", // Android端发布纯文字动态时设置的标识 + "text_only_placeholder", // 纯文字动态占位图标识 + "placeholder/text", // 文字占位图路径 + "default_text_cover", // 默认文字封面 + "no_image_placeholder", // 无图片占位图 + "/placeholder.", // 通用占位图 + "crmebimage/public/content/", // 后端默认内容图片路径 + "default_cover", // 默认封面 + "empty_cover", // 空封面 + }; + + String lowerUrl = url.toLowerCase(); + for (String pattern : placeholderPatterns) { + if (lowerUrl.contains(pattern.toLowerCase())) { + return true; + } + } + + // 检查是否为空白图片或默认图片(根据文件名判断) + if (lowerUrl.endsWith("/default.png") || + lowerUrl.endsWith("/default.jpg") || + lowerUrl.endsWith("/placeholder.png") || + lowerUrl.endsWith("/placeholder.jpg") || + lowerUrl.endsWith("/empty.png") || + lowerUrl.endsWith("/empty.jpg")) { + return true; + } + + return false; + } } diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/WorksResponse.java b/android-app/app/src/main/java/com/example/livestreaming/net/WorksResponse.java index fd5643a0..b9671b4a 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/net/WorksResponse.java +++ b/android-app/app/src/main/java/com/example/livestreaming/net/WorksResponse.java @@ -9,6 +9,7 @@ public class WorksResponse { private Long id; // 作品ID private String title; // 作品标题 private String description; // 作品描述 + private String content; // 作品内容(纯文字动态) private String type; // 作品类型:IMAGE 或 VIDEO private String coverUrl; // 封面图片URL private String videoUrl; // 视频URL(视频作品) @@ -29,6 +30,8 @@ public class WorksResponse { private Boolean isOwner; // 是否是当前用户的作品 private Integer isHot; // 是否热门:1-是 0-否 private String hotTime; // 设置热门的时间 + private String category; // 分类编码 + private String categoryName; // 分类名称 public Long getId() { return id; @@ -47,12 +50,28 @@ public class WorksResponse { } public String getDescription() { - return description; + // 优先返回description,如果为空则返回content + if (description != null && !description.isEmpty()) { + return description; + } + return content; } public void setDescription(String description) { this.description = description; } + + public String getContent() { + // 优先返回content,如果为空则返回description + if (content != null && !content.isEmpty()) { + return content; + } + return description; + } + + public void setContent(String content) { + this.content = content; + } public String getType() { return type; @@ -223,4 +242,20 @@ public class WorksResponse { public void setHotTime(String hotTime) { this.hotTime = hotTime; } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } } diff --git a/android-app/app/src/main/res/color/bottom_nav_icon_glow_color.xml b/android-app/app/src/main/res/color/bottom_nav_icon_glow_color.xml index 3295dd10..1b56cdb4 100644 --- a/android-app/app/src/main/res/color/bottom_nav_icon_glow_color.xml +++ b/android-app/app/src/main/res/color/bottom_nav_icon_glow_color.xml @@ -1,6 +1,6 @@ - + - - + + diff --git a/android-app/app/src/main/res/drawable/bg_bottom_nav_deep.xml b/android-app/app/src/main/res/drawable/bg_bottom_nav_deep.xml index 6067cbf7..3cbad329 100644 --- a/android-app/app/src/main/res/drawable/bg_bottom_nav_deep.xml +++ b/android-app/app/src/main/res/drawable/bg_bottom_nav_deep.xml @@ -1,18 +1,18 @@ - + - + - + - + + android:width="0.5dp" + android:color="#E5E5E5" /> diff --git a/android-app/app/src/main/res/layout/activity_fish_pond_webview.xml b/android-app/app/src/main/res/layout/activity_fish_pond_webview.xml index 25afb86f..18767f97 100644 --- a/android-app/app/src/main/res/layout/activity_fish_pond_webview.xml +++ b/android-app/app/src/main/res/layout/activity_fish_pond_webview.xml @@ -30,15 +30,15 @@ + android:background="@color/white"> @@ -55,7 +55,7 @@ android:padding="6dp" android:contentDescription="添加" android:src="@drawable/ic_add_24" - app:tint="@color/white" + app:tint="@color/black" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/android-app/app/src/main/res/layout/activity_wish_tree.xml b/android-app/app/src/main/res/layout/activity_wish_tree.xml index e01442f6..ea1fda8d 100644 --- a/android-app/app/src/main/res/layout/activity_wish_tree.xml +++ b/android-app/app/src/main/res/layout/activity_wish_tree.xml @@ -25,12 +25,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + - + - + - + - + - + - + - + + + + + + + + + + + + - + - + - + - + @@ -106,12 +106,12 @@ app:layout_constraintBottom_toBottomOf="@id/lastMessage" app:layout_constraintEnd_toEndOf="parent" /> - +