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" />
-
+