From 23a07562b75dfbb01be5f2928e47bb22ee63cb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E5=B0=8F=E6=B1=9F?= <461355754@qq.com> Date: Thu, 4 Dec 2025 10:18:51 +0800 Subject: [PATCH] =?UTF-8?q?1:=E4=BF=AE=E6=94=B9CP=E7=89=B9=E6=95=88?= =?UTF-8?q?=E5=87=BA=E7=8E=B0=E5=A4=B4=E5=83=8F=E4=B8=8D=E8=A7=84=E5=88=99?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moduleutil/utils/roomview/RoomCPView.java | 378 ++++++++++++------ .../src/main/res/layout/room_cp_vip_view.xml | 4 +- .../main/res/layout/item_user_playlist.xml | 1 + 3 files changed, 266 insertions(+), 117 deletions(-) diff --git a/BaseModule/src/main/java/com/xscm/moduleutil/utils/roomview/RoomCPView.java b/BaseModule/src/main/java/com/xscm/moduleutil/utils/roomview/RoomCPView.java index 1da3fc98..c78c52b3 100644 --- a/BaseModule/src/main/java/com/xscm/moduleutil/utils/roomview/RoomCPView.java +++ b/BaseModule/src/main/java/com/xscm/moduleutil/utils/roomview/RoomCPView.java @@ -1,3 +1,4 @@ + package com.xscm.moduleutil.utils.roomview; import android.content.Context; @@ -28,6 +29,7 @@ import com.tencent.qgame.animplayer.AnimView; import com.tencent.qgame.animplayer.inter.IAnimListener; import com.xscm.moduleutil.R; import com.xscm.moduleutil.utils.ImageUtils; +import com.xscm.moduleutil.widget.CircularImage; import java.io.File; import java.io.FileOutputStream; @@ -51,79 +53,145 @@ import okhttp3.Response; import okhttp3.ResponseBody; /** - * 项目名称:羽声语音 + * 羽声语音房间CP特效视图 + * 用于显示CP(情侣)相关的动画特效,包括头像、名称和动画效果 + * 支持MP4和SVGA格式的动画文件 * 时间:2025/11/24 9:50 - * 用途: */ public class RoomCPView extends FrameLayout { - + // ==================== 视图组件 ==================== + /** 动画视图组件 */ private AnimView anim_cp; - private boolean isLoadEffect = false; - private boolean isShow = true; // 是否开启特效 - private ReentrantLock lock = new ReentrantLock(); - private List animationArray = new ArrayList<>(); - public ExecutorService queue = Executors.newSingleThreadExecutor(); - private Context mContext; - private boolean isOnece; + /** CP头像组件 */ + private CircularImage room_cp_head1, room_cp_head2; - private RoundedImageView room_cp_head1; - private RoundedImageView room_cp_head2; + /** CP名称组件 */ private TextView room_cp_name1, room_cp_name2; + + /** 头像容器 */ private LinearLayout avatarContainer1; + + /** 头像父容器 */ private ConstraintLayout avatarsParentContainer; - // 动画队列和锁机制 - private final Queue animationQueue = new LinkedList<>(); - private boolean isAnimationRunning = false; - private final Object animationLock = new Object(); + // ==================== 状态控制 ==================== + /** 是否正在加载特效 */ + private boolean isLoadEffect = false; - // 下载任务缓存 - private final Queue downloadQueue = new LinkedList<>(); - private boolean isDownloadRunning = false; - private final Object downloadLock = new Object(); + /** 是否开启特效显示 */ + private boolean isShow = true; + /** 是否仅播放一次 */ + private boolean isOnece; + + /** 当前播放的动画路径 */ private String currPlayPath = ""; + // ==================== 线程安全 ==================== + /** 动画数组锁 */ + private ReentrantLock lock = new ReentrantLock(); + + /** 动画队列锁 */ + private final Object animationLock = new Object(); + + /** 下载队列锁 */ + private final Object downloadLock = new Object(); + + // ==================== 数据存储 ==================== + /** 动画路径列表 */ + private List animationArray = new ArrayList<>(); + + /** 动画任务队列 */ + private final Queue animationQueue = new LinkedList<>(); + + /** 下载任务队列 */ + private final Queue downloadQueue = new LinkedList<>(); + + // ==================== 状态标志 ==================== + /** 是否有动画正在运行 */ + private boolean isAnimationRunning = false; + + /** 是否有下载任务正在运行 */ + private boolean isDownloadRunning = false; + + // ==================== 线程池 ==================== + /** 任务执行线程池 */ + public ExecutorService queue = Executors.newSingleThreadExecutor(); + + // ==================== 其他 ==================== + /** 上下文对象 */ + private Context mContext; + + /** 播放完成监听器列表 */ + private List playbackCompleteListeners = new ArrayList<>(); + + /** + * 设置线程池 + * @param queue 线程池 + */ public void setQueue(ExecutorService queue) { this.queue = queue; } + /** + * 基本构造函数 + * @param context 上下文 + */ public RoomCPView(@NonNull Context context) { super(context); this.mContext = context; init(); } + /** + * 带属性集的构造函数 + * @param context 上下文 + * @param attrs 属性集 + */ public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); this.mContext = context; init(); } + /** + * 带属性集和样式属性的构造函数 + * @param context 上下文 + * @param attrs 属性集 + * @param defStyleAttr 默认样式属性 + */ public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; init(); } + /** + * 完整构造函数 + * @param context 上下文 + * @param attrs 属性集 + * @param defStyleAttr 默认样式属性 + * @param defStyleRes 默认样式资源 + */ public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); this.mContext = context; init(); } + /** + * 初始化视图和状态 + */ private void init() { isLoadEffect = false; - initView(); - - // 设置动画监听器 setupAnimListener(); } /** * 设置动画监听器 + * 为动画视图设置各种状态回调,包括开始、完成、失败等事件 */ private void setupAnimListener() { if (anim_cp != null) { @@ -149,9 +217,28 @@ public class RoomCPView extends FrameLayout { if (isOnece) { return; } - // 通知播放完成 - notifyPlaybackComplete(); - loadStartAnimation(); + + // 添加延迟,确保当前动画完全结束 + postDelayed(() -> { + // 使用动画队列锁确保线程安全 + synchronized (animationLock) { + // 重置播放状态 + lock.lock(); + try { + isLoadEffect = false; + // 标记动画已停止运行 + isAnimationRunning = false; + } finally { + lock.unlock(); + } + + // 通知播放完成 + notifyPlaybackComplete(); + } + + // 继续播放下一个动画 + loadStartAnimation(); + }, 100); // 添加100毫秒的延迟 } }); } @@ -177,6 +264,7 @@ public class RoomCPView extends FrameLayout { // startAvatarFloatAnimation(); }); } +/**************** c361dac4fe23498d925fba2efe376ca8 ****************/ @Override public boolean onVideoConfigReady(@NonNull AnimConfig animConfig) { @@ -192,6 +280,20 @@ public class RoomCPView extends FrameLayout { // 动画失败,隐藏视图 setVisibility(View.GONE); anim_cp.setVisibility(View.GONE); + + // 使用动画队列锁确保线程安全 + synchronized (animationLock) { + // 重置播放状态 + lock.lock(); + try { + isLoadEffect = false; + // 标记动画已停止运行 + isAnimationRunning = false; + } finally { + lock.unlock(); + } + } + // 继续播放下一个 loadStartAnimation(); }); @@ -200,126 +302,169 @@ public class RoomCPView extends FrameLayout { } } + /** + * 初始化视图组件 + * 加载布局文件并获取各个视图组件的引用 + */ private void initView() { + // 加载布局文件 LayoutInflater.from(getContext()).inflate(R.layout.room_cp_vip_view, this, true); + + // 获取动画视图组件 anim_cp = findViewById(R.id.anim_cp); + + // 获取头像组件 room_cp_head1 = findViewById(R.id.room_cp_head1); room_cp_head2 = findViewById(R.id.room_cp_head2); + + // 获取名称组件 room_cp_name1 = findViewById(R.id.room_cp_name1); room_cp_name2 = findViewById(R.id.room_cp_name2); - // 获取头像的父布局 + // 获取头像容器 avatarContainer1 = findViewById(R.id.ll_head); - // 获取包含两个头像的父 LinearLayout + // 获取包含两个头像的父容器 avatarsParentContainer = (ConstraintLayout) avatarContainer1.getParent(); -// // 初始状态隐藏头像和名称 -// room_cp_head1.setVisibility(View.GONE); -// room_cp_head2.setVisibility(View.GONE); -// room_cp_name1.setVisibility(View.GONE); -// room_cp_name2.setVisibility(View.GONE); + // 注释:初始状态隐藏头像和名称(如需要可取消注释) + // room_cp_head1.setVisibility(View.GONE); + // room_cp_head2.setVisibility(View.GONE); + // room_cp_name1.setVisibility(View.GONE); + // room_cp_name2.setVisibility(View.GONE); } + /** + * 设置CP信息数据 + * @param room_head1 第一个用户头像URL + * @param room_head2 第二个用户头像URL + * @param room_cp_name1 第一个用户名称 + * @param room_cp_name2 第二个用户名称 + */ public void setCPTextData(String room_head1, String room_head2, String room_cp_name1, String room_cp_name2) { + // 加载用户头像 ImageUtils.loadHead(room_head1, room_cp_head1); ImageUtils.loadHead(room_head2, room_cp_head2); + + // 设置用户名称 this.room_cp_name1.setText(room_cp_name1); this.room_cp_name2.setText(room_cp_name2); } + /** + * 加载并开始播放动画 + * 从动画队列中取出下一个动画进行播放 + */ private void loadStartAnimation() { + // 检查是否开启特效 if (!isShow) { - // isshow 为是否开启特效 如果未开启则return return; } - String animationPath = null; - - // list加锁 - lock.lock(); - try { - // 如果list长度大于0 - if (!animationArray.isEmpty()) { - // 动画路径则赋值,取list中的第一个数据 - animationPath = animationArray.get(0); - // 移除list的第一条数据 - animationArray.remove(0); - isLoadEffect = true; - } else { - isLoadEffect = false; - // 队列为空,释放资源但不销毁视图 - post(() -> { - destroyEffectView(); - }); + // 使用动画队列锁确保线程安全 + synchronized (animationLock) { + // 如果已经有动画在运行,则直接返回,等待当前动画完成后再处理下一个 + if (isAnimationRunning) { + return; } - } finally { - // 解锁 - lock.unlock(); - } - if (isLoadEffect && animationPath != null && !TextUtils.isEmpty(animationPath)) { - String finalAnimationPath = animationPath; - post(new Runnable() { - @Override - public void run() { - // 处理MP4动画文件(可能是网络URL) - handleMP4File(finalAnimationPath, new DownloadCallback() { - @Override - public void onSuccess(String localPath) { - post(() -> { - // 设置MP4动画文件 - currPlayPath = localPath; - // 启动从底部弹起动画 - startBottomUpAnimation(); + String animationPath = null; - // 开始播放动画 - anim_cp.setLoop(1); - }); - } + // 对动画列表加锁 + lock.lock(); + try { + // 检查动画队列是否为空 + if (!animationArray.isEmpty()) { + // 获取队列中的第一个动画路径 + animationPath = animationArray.get(0); + // 从队列中移除已获取的动画 + animationArray.remove(0); - @Override - public void onError(String error) { - LogUtils.e("MP4下载或播放失败: " + error); - // 处理失败情况,继续播放下一个 - post(() -> { - lock.lock(); - try { - isLoadEffect = false; - } finally { - lock.unlock(); - } - loadStartAnimation(); - }); - } + // 设置状态标记 + isLoadEffect = true; + isAnimationRunning = true; + } else { + // 队列为空,重置状态并释放资源 + isLoadEffect = false; + post(() -> { + destroyEffectView(); }); } - }); + } finally { + // 解锁 + lock.unlock(); + } + + // 如果有动画需要播放 + if (isLoadEffect && animationPath != null && !TextUtils.isEmpty(animationPath)) { + String finalAnimationPath = animationPath; + post(new Runnable() { + @Override + public void run() { + // 处理MP4动画文件(可能是网络URL) + handleMP4File(finalAnimationPath, new DownloadCallback() { + @Override + public void onSuccess(String localPath) { + post(() -> { + // 设置当前播放路径 + currPlayPath = localPath; + // 启动从底部弹起动画 + startBottomUpAnimation(); + // 设置播放次数为1次 + anim_cp.setLoop(1); + }); + } + + @Override + public void onError(String error) { + LogUtils.e("MP4下载或播放失败: " + error); + // 处理失败情况,继续播放下一个 + post(() -> { + // 使用动画队列锁确保线程安全 + synchronized (animationLock) { + lock.lock(); + try { + // 重置状态标记 + isLoadEffect = false; + isAnimationRunning = false; + } finally { + lock.unlock(); + } + } + // 尝试播放下一个动画 + loadStartAnimation(); + }); + } + }); + } + }); + } } } /** - * CP特效进来 - * @param room_head1 头像1 - * @param room_head2 头像2 - * @param room_name1 名称1 - * @param room_name2 名称2 - * @param mp4Path MP4动画文件路径 + * 显示CP特效 + * 设置CP信息并添加动画到队列中播放 + * @param room_head1 第一个用户头像URL + * @param room_head2 第二个用户头像URL + * @param room_name1 第一个用户名称 + * @param room_name2 第二个用户名称 + * @param mp4Path 动画文件路径(支持mp4和svga格式) */ public void displayEffectView(String room_head1, String room_head2, String room_name1, String room_name2, String mp4Path) { // 确保视图已初始化 reinitView(); - // 设置CP数据,但不显示头像 + // 设置CP数据 setCPTextData(room_head1, room_head2, room_name1, room_name2); - // 确保头像初始为隐藏状态 -// room_cp_head1.setVisibility(View.GONE); -// room_cp_head2.setVisibility(View.GONE); -// room_cp_name1.setVisibility(View.GONE); -// room_cp_name2.setVisibility(View.GONE); + // 注释:确保头像初始为隐藏状态(如需要可取消注释) + // room_cp_head1.setVisibility(View.GONE); + // room_cp_head2.setVisibility(View.GONE); + // room_cp_name1.setVisibility(View.GONE); + // room_cp_name2.setVisibility(View.GONE); // 确保视图可见 setVisibility(View.VISIBLE); @@ -329,32 +474,32 @@ public class RoomCPView extends FrameLayout { queue = Executors.newSingleThreadExecutor(); } + // 在后台线程中处理动画 queue.execute(new Runnable() { @Override public void run() { - // 如果mp4Path不存在return + // 检查动画路径是否有效 if (mp4Path == null || mp4Path.isEmpty()) { return; } - // 将mp4Path链接转为小写 + // 获取文件扩展名并转为小写 String playImage = mp4Path; String pathExtension = getFileExtension(playImage).toLowerCase(); - // 判定礼物的后缀是否为svga或者mp4如果非这两种 则return + // 检查文件格式是否支持(仅支持svga和mp4) if (!("svga".equals(pathExtension) || "mp4".equals(pathExtension))) { return; } - // 锁住list + // 对动画列表加锁 lock.lock(); try { - // 添加动画数据进list + // 将动画路径添加到队列 animationArray.add(playImage); - // 如果没有在加载则开始加载 + // 如果当前没有动画在加载,则开始加载新动画 if (!isLoadEffect) { - // 更改加载状态标记 isLoadEffect = true; loadStartAnimation(); } @@ -420,7 +565,6 @@ public class RoomCPView extends FrameLayout { } // 添加成员变量 - private List playbackCompleteListeners = new ArrayList<>(); // 添加监听器管理方法 public void addOnPlaybackCompleteListener(OnPlaybackCompleteListener listener) { @@ -658,12 +802,16 @@ public class RoomCPView extends FrameLayout { setVisibility(View.GONE); // 清空动画队列 - lock.lock(); - try { - animationArray.clear(); - isLoadEffect = false; - } finally { - lock.unlock(); + synchronized (animationLock) { + lock.lock(); + try { + animationArray.clear(); + isLoadEffect = false; + // 重置动画运行状态 + isAnimationRunning = false; + } finally { + lock.unlock(); + } } } diff --git a/BaseModule/src/main/res/layout/room_cp_vip_view.xml b/BaseModule/src/main/res/layout/room_cp_vip_view.xml index 084af379..812a8d80 100644 --- a/BaseModule/src/main/res/layout/room_cp_vip_view.xml +++ b/BaseModule/src/main/res/layout/room_cp_vip_view.xml @@ -38,7 +38,7 @@ android:layout_height="wrap_content" android:orientation="vertical"> - -