From 0dd7c367cb1f51ee23f0622b4f8b6497310c755a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E5=B0=8F=E6=B1=9F?= <461355754@qq.com> Date: Mon, 24 Nov 2025 18:48:14 +0800 Subject: [PATCH] =?UTF-8?q?1:=E6=B7=BB=E5=8A=A0cp=E8=BF=9B=E5=9C=BA?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E6=95=88=E6=9E=9C=202=EF=BC=9A=E4=BF=AE?= =?UTF-8?q?=E6=94=B9cp=E7=A4=BC=E7=89=A9=E5=BC=B9=E6=A1=86=203=EF=BC=9A?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BB=84=E6=88=90cp=E5=90=8E=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E5=BF=83=E5=8A=A8=E7=A9=BA=E9=97=B4=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xscm/moduleutil/http/RetrofitClient.java | 6 +- .../moduleutil/utils/roomview/RoomCPView.java | 919 ++++++++++++++++++ .../widget/QXGiftPlayerManager.java | 62 +- .../src/main/res/anim/up_down_animation.xml | 11 + .../src/main/res/layout/room_cp_vip_view.xml | 103 ++ .../com/xscm/modulemain/BaseMvpActivity.java | 8 +- .../activity/room/activity/RoomActivity.kt | 147 +-- .../activity/user/activity/HeartCpActivity.kt | 3 + .../activity/ui/main/BosomFriendFragment.kt | 8 +- .../src/main/res/layout/activity_room.xml | 21 +- .../main/res/layout/fragment_bosom_friend.xml | 406 ++++---- .../res/layout/fragment_room_user_info.xml | 2 +- .../src/main/res/layout/room_cp_vip_view.xml | 113 --- 13 files changed, 1334 insertions(+), 475 deletions(-) create mode 100644 BaseModule/src/main/java/com/xscm/moduleutil/utils/roomview/RoomCPView.java create mode 100644 BaseModule/src/main/res/anim/up_down_animation.xml create mode 100644 BaseModule/src/main/res/layout/room_cp_vip_view.xml delete mode 100644 MainModule/src/main/res/layout/room_cp_vip_view.xml diff --git a/BaseModule/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java b/BaseModule/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java index 0000bd0a..f296f0c8 100644 --- a/BaseModule/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java +++ b/BaseModule/src/main/java/com/xscm/moduleutil/http/RetrofitClient.java @@ -4534,8 +4534,7 @@ public class RetrofitClient { }); } - public void roomUserCharmList(String room_id, String - user_id, BaseObserver> observer) { + public void roomUserCharmList(String room_id, String user_id, BaseObserver> observer) { sApiServer.roomUserCharmList(room_id, user_id).enqueue(new Callback>>() { @Override public void onResponse(Call>> call, Response>> response) { @@ -4566,7 +4565,8 @@ public class RetrofitClient { if (baseModel.getCode() == 1) { observer.onNext(baseModel.getData()); } else if (baseModel.getCode() == 0) { - observer.onNext(null); + ToastUtils.showLong(baseModel.getMsg()); +// observer.onNext(null); } } } 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 new file mode 100644 index 00000000..1da3fc98 --- /dev/null +++ b/BaseModule/src/main/java/com/xscm/moduleutil/utils/roomview/RoomCPView.java @@ -0,0 +1,919 @@ +package com.xscm.moduleutil.utils.roomview; + +import android.content.Context; +import android.os.AsyncTask; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; +import android.view.animation.TranslateAnimation; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.blankj.utilcode.util.LogUtils; +import com.makeramen.roundedimageview.RoundedImageView; +import com.tencent.qgame.animplayer.AnimConfig; +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 java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantLock; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * 项目名称:羽声语音 + * 时间: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; + + private RoundedImageView room_cp_head1; + private RoundedImageView room_cp_head2; + 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 final Queue downloadQueue = new LinkedList<>(); + private boolean isDownloadRunning = false; + private final Object downloadLock = new Object(); + + private String currPlayPath = ""; + + public void setQueue(ExecutorService queue) { + this.queue = queue; + } + + public RoomCPView(@NonNull Context context) { + super(context); + this.mContext = context; + init(); + } + + public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + init(); + } + + public RoomCPView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.mContext = context; + init(); + } + + 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) { + anim_cp.setAnimListener(new IAnimListener() { + @Override + public void onVideoDestroy() { + LogUtils.e("onVideoDestroy"); + } + + @Override + public void onVideoComplete() { + LogUtils.e("onVideoComplete"); + // 确保所有UI操作在主线程中执行 + post(() -> { + if (anim_cp != null) { + // 停止头像动画 + stopAvatarAnimation(); + + // 隐藏动画视图和头像 + anim_cp.setVisibility(View.GONE); + avatarContainer1.setVisibility(View.GONE); + + if (isOnece) { + return; + } + // 通知播放完成 + notifyPlaybackComplete(); + loadStartAnimation(); + } + }); + } + + @Override + public void onVideoRender(int i, @Nullable AnimConfig animConfig) { + // LogUtils.e("onVideoRender", i, animConfig); + } + + @Override + public void onVideoStart() { + LogUtils.e("onVideoStart"); + // 确保所有UI操作在主线程中执行 + post(() -> { + // 动画开始,显示视图和头像 + setVisibility(View.VISIBLE); + anim_cp.setVisibility(View.VISIBLE); + avatarContainer1.setVisibility(View.VISIBLE); + + + + // 启动头像上下浮动动画 +// startAvatarFloatAnimation(); + }); + } + + @Override + public boolean onVideoConfigReady(@NonNull AnimConfig animConfig) { + LogUtils.e("onVideoConfigReady", animConfig); + return true; + } + + @Override + public void onFailed(int i, @Nullable String s) { + LogUtils.e("onFailed", s); + // 确保所有UI操作在主线程中执行 + post(() -> { + // 动画失败,隐藏视图 + setVisibility(View.GONE); + anim_cp.setVisibility(View.GONE); + // 继续播放下一个 + loadStartAnimation(); + }); + } + }); + } + } + + 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); + } + + 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(); + }); + } + } 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(); + + // 开始播放动画 + anim_cp.setLoop(1); + }); + } + + @Override + public void onError(String error) { + LogUtils.e("MP4下载或播放失败: " + error); + // 处理失败情况,继续播放下一个 + post(() -> { + lock.lock(); + try { + isLoadEffect = 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动画文件路径 + */ + public void displayEffectView(String room_head1, String room_head2, String room_name1, String room_name2, String mp4Path) { + // 确保视图已初始化 + reinitView(); + + // 设置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); + + // 确保视图可见 + setVisibility(View.VISIBLE); + + // 检查队列是否已初始化 + if (queue == null) { + 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 + if (!("svga".equals(pathExtension) || "mp4".equals(pathExtension))) { + return; + } + + // 锁住list + lock.lock(); + try { + // 添加动画数据进list + animationArray.add(playImage); + + // 如果没有在加载则开始加载 + if (!isLoadEffect) { + // 更改加载状态标记 + isLoadEffect = true; + loadStartAnimation(); + } + } finally { + // 解锁 + lock.unlock(); + } + } + }); + } + + + + + /** + * 原始的动画方法,现在只被内部调用 + */ + public void startAnimation(AnimationListener listener) { + // 设置进入动画 + AlphaAnimation fadeIn = new AlphaAnimation(0f, 1f); + fadeIn.setDuration(500); + fadeIn.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + // 保持显示1秒后开始退出动画 + postDelayed(() -> { + AlphaAnimation fadeOut = new AlphaAnimation(1f, 0f); + fadeOut.setDuration(500); + fadeOut.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + if (listener != null) { + listener.onAnimationEnd(); + } + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + startAnimation(fadeOut); + }, 1000); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + startAnimation(fadeIn); + } + + public interface AnimationListener { + void onAnimationEnd(); + } + + // 添加播放完成监听接口 + public interface OnPlaybackCompleteListener { + void onPlaybackComplete(); + } + + // 添加成员变量 + private List playbackCompleteListeners = new ArrayList<>(); + + // 添加监听器管理方法 + public void addOnPlaybackCompleteListener(OnPlaybackCompleteListener listener) { + if (listener != null && !playbackCompleteListeners.contains(listener)) { + playbackCompleteListeners.add(listener); + } + } + + public void removeOnPlaybackCompleteListener(OnPlaybackCompleteListener listener) { + if (listener != null) { + playbackCompleteListeners.remove(listener); + } + } + + public void clearPlaybackCompleteListeners() { + playbackCompleteListeners.clear(); + } + + // 触发播放完成回调的方法 + private void notifyPlaybackComplete() { + for (OnPlaybackCompleteListener listener : new ArrayList<>(playbackCompleteListeners)) { + if (listener != null) { + listener.onPlaybackComplete(); + } + } + } + + /** + * 动画任务类,用于存储动画所需的数据 + */ + public static class AnimationTask { + String room_head1; + String room_head2; + String room_cp_name1; + String room_cp_name2; + String mp4Path; // MP4动画文件路径 + AnimationListener listener; + + AnimationTask(String room_head1, String room_head2, String room_cp_name1, String room_cp_name2, String mp4Path, AnimationListener listener) { + this.room_head1 = room_head1; + this.room_head2 = room_head2; + this.room_cp_name1 = room_cp_name1; + this.room_cp_name2 = room_cp_name2; + this.mp4Path = mp4Path; + this.listener = listener; + } + } + + /** + * 处理MP4文件,如果是网络URL则下载到本地 + * @param path 文件路径或URL + * @param callback 下载回调 + */ + private void handleMP4File(String path, DownloadCallback callback) { + // 判断是否是网络URL + if (path != null && (path.startsWith("http://") || path.startsWith("https://"))) { + // 是网络URL,需要下载 + downloadFile(path, callback); + } else { + // 本地路径,直接使用 + if (callback != null) { + callback.onSuccess(path); + } + } + } + + /** + * 下载文件到本地 + * @param url 下载URL + * @param callback 下载回调 + */ + private void downloadFile(String url, DownloadCallback callback) { + synchronized (downloadLock) { + // 创建下载任务并加入队列 + DownloadTask task = new DownloadTask(url, callback); + downloadQueue.offer(task); + + // 如果当前没有下载任务在执行,则开始下载 + if (!isDownloadRunning) { + processNextDownload(); + } + } + } + + /** + * 处理下一个下载任务 + */ + private void processNextDownload() { + synchronized (downloadLock) { + if (downloadQueue.isEmpty()) { + isDownloadRunning = false; + return; + } + + isDownloadRunning = true; + DownloadTask currentTask = downloadQueue.poll(); + + // 执行下载任务 + new DownloadAsyncTask(currentTask.callback).execute(currentTask.url); + } + } + + /** + * 异步下载任务 + */ + private class DownloadAsyncTask extends AsyncTask { + private DownloadCallback callback; + private String errorMessage; + + public DownloadAsyncTask(DownloadCallback callback) { + this.callback = callback; + } + + @Override + protected String doInBackground(String... urls) { + String url = urls[0]; + try { + // 获取文件名 + int lastSlashIndex = url.lastIndexOf('/'); + String fileName = lastSlashIndex != -1 ? url.substring(lastSlashIndex + 1) : url; + int queryIndex = fileName.indexOf("?"); + if (queryIndex != -1) { + fileName = fileName.substring(0, queryIndex); + } + + // 创建本地文件 + File dir = new File(getContext().getCacheDir(), "animations"); + if (!dir.exists()) { + dir.mkdirs(); + } + File file = new File(dir, fileName); + + // 如果文件已存在,直接返回本地路径 + if (file.exists()) { + return file.getAbsolutePath(); + } + + // 下载文件 + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.connect(); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + errorMessage = "服务器返回HTTP错误代码: " + connection.getResponseCode(); + return null; + } + + InputStream input = connection.getInputStream(); + FileOutputStream output = new FileOutputStream(file); + + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = input.read(buffer)) != -1) { + output.write(buffer, 0, bytesRead); + } + + output.close(); + input.close(); + connection.disconnect(); + + return file.getAbsolutePath(); + } catch (IOException e) { + errorMessage = "下载失败: " + e.getMessage(); + return null; + } + } + + @Override + protected void onPostExecute(String result) { + synchronized (downloadLock) { + // 下载完成,处理下一个下载任务 + processNextDownload(); + } + + // 通知回调 + if (callback != null) { + if (result != null) { + callback.onSuccess(result); + } else { + callback.onError(errorMessage != null ? errorMessage : "未知错误"); + } + } + } + } + + /** + * 下载任务类 + */ + private static class DownloadTask { + String url; + DownloadCallback callback; + + DownloadTask(String url, DownloadCallback callback) { + this.url = url; + this.callback = callback; + } + } + + /** + * 下载回调接口 + */ + private interface DownloadCallback { + void onSuccess(String localPath); + void onError(String error); + } + + /** + * 获取文件扩展名 + * @param url 文件URL + * @return 文件扩展名 + */ + private String getFileExtension(String url) { + if (url != null && url.contains(".")) { + return url.substring(url.lastIndexOf(".") + 1); + } + return ""; + } + + /** + * 销毁特效视图 + */ + public void destroyEffectView() { + // 停止头像动画 + stopAvatarAnimation(); + + // 清理监听器 + clearPlaybackCompleteListeners(); + + // 停止动画视图但保留组件以便重用 + if (anim_cp != null) { + anim_cp.stopPlay(); + anim_cp.setVisibility(View.GONE); + } + + // 隐藏视图但不移除,以便再次使用 + setVisibility(View.GONE); + + // 清空动画队列 + lock.lock(); + try { + animationArray.clear(); + isLoadEffect = false; + } finally { + lock.unlock(); + } + } + + /** + * 从底部弹起动画 + */ + private void startBottomUpAnimation() { + // 获取父视图的高度,如果为0则使用屏幕高度 + float parentHeight = 0; + if (getParent() != null) { + parentHeight = ((View) getParent()).getHeight(); + } + if (parentHeight <= 0) { + parentHeight = getResources().getDisplayMetrics().heightPixels; + } + + // 设置初始位置在屏幕底部外 +// setTranslationY(parentHeight); + + // 创建从底部弹起的动画 + TranslateAnimation bottomUpAnimation = new TranslateAnimation( + Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 0, + Animation.RELATIVE_TO_PARENT, 1.0f, Animation.RELATIVE_TO_PARENT, 0.0f); + bottomUpAnimation.setDuration(2000); // 动画持续时间 + + // 创建弹性插值器 + Interpolator overshootInterpolator = new OvershootInterpolator(0.5f); + bottomUpAnimation.setInterpolator(overshootInterpolator); + + // 应用动画 + startAnimation(bottomUpAnimation); + + // 动画结束后重置位置 + bottomUpAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + setVisibility(View.VISIBLE); + anim_cp.setVisibility(View.VISIBLE); + avatarContainer1.setVisibility(VISIBLE); + + anim_cp.startPlay(new File(currPlayPath)); + } + + @Override + public void onAnimationEnd(Animation animation) { + startAvatarFloatAnimation(); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + avatarsParentContainer.startAnimation(bottomUpAnimation); + } + + /** + * 头像上下动画 + */ + private void startAvatarFloatAnimation() { + // 确保包含两个头像的父布局已初始化 + if (avatarsParentContainer == null) { + return; + } + + // 包含两个头像的父布局上下浮动动画 + AnimationSet avatarsAnimationSet = new AnimationSet(true); + // 使用适中的移动距离,确保头像框显示完整 + TranslateAnimation avatarsFloatUp = new TranslateAnimation( + Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f, + Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, -0.01f); + avatarsFloatUp.setDuration(900); // 增加动画持续时间,使动画更平滑 + avatarsFloatUp.setRepeatCount(Animation.INFINITE); + avatarsFloatUp.setRepeatMode(Animation.REVERSE); + + avatarsAnimationSet.addAnimation(avatarsFloatUp); + avatarsParentContainer.startAnimation(avatarsAnimationSet); + + } + + /** + * 停止头像动画 + */ + private void stopAvatarAnimation() { + if (avatarsParentContainer != null) { + avatarsParentContainer.clearAnimation(); + } + } + + /** + * 停止播放动画 + */ + public void stopPlay() { + stopAvatarAnimation(); + if (anim_cp != null) { + anim_cp.stopPlay(); + anim_cp.setVisibility(View.GONE); + } + setVisibility(View.GONE); + } + + /** + * 开启或关闭特效视图 + * @param isShow 是否显示 + */ + public void openOrCloseEffectViewWith(boolean isShow) { + this.isShow = isShow; + + if (anim_cp != null) { + anim_cp.stopPlay(); + anim_cp.setVisibility(View.GONE); + } + setVisibility(isShow ? View.VISIBLE : View.GONE); + } + + /** + * 下载并播放动画 + * @param context 上下文 + * @param playImage 动画URL + * @param callback 下载回调 + */ + public void downloadAndPlay(Context context, String playImage, DownloadCallback callback) { + String fileName = playImage.substring(playImage.lastIndexOf("/")); + String filePath = context.getCacheDir().getAbsolutePath() + fileName; + + LogUtils.e("@@@@@filePath: " + filePath.toString()); + + File file = new File(filePath); + + if (!file.exists()) { + LogUtils.e("无缓存"); + // 使用OkHttp进行下载 + OkHttpClient client = new OkHttpClient(); + Request request = new Request.Builder() + .url(playImage) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + LogUtils.e("MP4下载失败: " + e.toString()); + // 在主线程中回调失败 + post(() -> { + if (callback != null) { + callback.onError(e.getMessage()); + } + }); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful()) { + try (ResponseBody responseBody = response.body()) { + if (responseBody != null) { + File downloadedFile = new File(filePath); + FileOutputStream fos = new FileOutputStream(downloadedFile); + fos.write(responseBody.bytes()); + fos.close(); + + // 在主线程中回调成功 + post(() -> { + if (callback != null) { + callback.onSuccess(downloadedFile.getAbsolutePath()); + } + }); + } else { + // 在主线程中回调失败 + post(() -> { + if (callback != null) { + callback.onError("Response body is null"); + } + }); + } + } catch (Exception e) { + LogUtils.e("MP4文件保存失败: " + e.getMessage()); + // 在主线程中回调失败 + post(() -> { + if (callback != null) { + callback.onError(e.getMessage()); + } + }); + } + } else { + LogUtils.e("MP4下载响应失败"); + // 在主线程中回调失败 + post(() -> { + if (callback != null) { + callback.onError("Response not successful: " + response.code()); + } + }); + } + } + }); + } else { + // 文件已存在,直接回调成功 + if (callback != null) { + callback.onSuccess(file.getAbsolutePath()); + } + } + } + + /** + * 重新初始化视图,以便再次播放 + */ + public void reinitView() { + if (anim_cp == null) { + // 如果动画视图已被销毁,重新初始化 + initView(); + + // 重新设置动画监听器 + setupAnimListener(); + } + + // 确保线程池可用 + if (queue == null || queue.isShutdown()) { + queue = Executors.newSingleThreadExecutor(); + } + + // 重置状态 + isLoadEffect = false; + isShow = true; + } + + /** + * 完全销毁视图,释放所有资源 + * 当不再需要此视图时调用 + */ + public void completeDestroy() { + // 停止头像动画 + stopAvatarAnimation(); + + // 清理监听器 + clearPlaybackCompleteListeners(); + + // 停止并移除动画视图 + if (anim_cp != null) { + anim_cp.stopPlay(); + removeView(anim_cp); + anim_cp = null; + } + + // 停止线程池 + if (queue != null) { + queue.shutdown(); + queue = null; + } + + // 隐藏并移除整个视图 + setVisibility(View.GONE); + if (getParent() != null) { + ((ViewGroup) getParent()).removeView(this); + } + } + +} \ No newline at end of file diff --git a/BaseModule/src/main/java/com/xscm/moduleutil/widget/QXGiftPlayerManager.java b/BaseModule/src/main/java/com/xscm/moduleutil/widget/QXGiftPlayerManager.java index 8fbcbcc8..c30c6a52 100644 --- a/BaseModule/src/main/java/com/xscm/moduleutil/widget/QXGiftPlayerManager.java +++ b/BaseModule/src/main/java/com/xscm/moduleutil/widget/QXGiftPlayerManager.java @@ -6,6 +6,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import com.xscm.moduleutil.bean.GiftBean; +import com.xscm.moduleutil.utils.roomview.RoomCPView; import java.util.List; import java.util.concurrent.ExecutorService; @@ -20,6 +21,8 @@ public class QXGiftPlayerManager { private GiftAnimView fullEffectView; private GiftAnimView chatEffectView; + + private RoomCPView roomCPView; private Context context; private QXGiftPlayerManager(Context context) { @@ -39,6 +42,15 @@ public class QXGiftPlayerManager { return bgEffectView; } + public RoomCPView getRoomCPView(){ + if (roomCPView==null){ + initRoomCPView(); + } + return roomCPView; + } + + + public GiftAnimView getDefaultFullEffectView() { if (fullEffectView == null) { initFullEffectView(); @@ -62,10 +74,25 @@ public class QXGiftPlayerManager { bgEffectView.setBackgroundColor(0x00000000); bgEffectView.setClickable(false); // userInteractionEnabled = NO - // 添加全屏特效视图和聊天特效视图 + // 添加全屏特效视图、聊天特效视图和CP视图 ((ViewGroup) bgEffectView).addView(getDefaultFullEffectView()); ((ViewGroup) bgEffectView).addView(getDefaultChatEffectView()); + ((ViewGroup) bgEffectView).addView(getRoomCPView()); } + + private void initRoomCPView() { + roomCPView=new RoomCPView(context); + roomCPView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + // 创建专用线程池替代GCD队列 + ExecutorService queue = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + Executors.defaultThreadFactory()); + roomCPView.setQueue(queue); + } + public void displayFullEffectView(String gift) { getDefaultFullEffectView().displayEffectView(gift); } @@ -78,6 +105,31 @@ public class QXGiftPlayerManager { getDefaultChatEffectView().displayEffectView(gift); } + /** + * 显示CP动画 + * @param room_head1 头像1的URL + * @param room_head2 头像2的URL + * @param room_cp_name1 名称1 + * @param room_cp_name2 名称2 + * @param mp4Path MP4动画文件路径 + */ + public void displayCPView(String room_head1, String room_head2, String room_cp_name1, String room_cp_name2, String mp4Path) { + getRoomCPView().displayEffectView(room_head1, room_head2, room_cp_name1, room_cp_name2, mp4Path); + } + + /** + * 开启或关闭CP动画 + * @param isShow 是否显示 + */ + public void openOrCloseCPViewWith(boolean isShow) { + } + + /** + * 停止CP动画 + */ + public void stopCPPlay() { + } + public void openOrCloseEffectViewWith(boolean isShow) { @@ -101,6 +153,12 @@ public class QXGiftPlayerManager { } chatEffectView = null; } + + if (roomCPView != null) { + // 先调用destroyEffectView方法,它会自动从父视图中移除 + roomCPView.destroyEffectView(); + roomCPView = null; + } if (bgEffectView != null) { bgEffectView = null; @@ -114,6 +172,8 @@ public class QXGiftPlayerManager { if (chatEffectView != null) { chatEffectView.stopPlay(); } + if (roomCPView != null) { + } } private void initFullEffectView() { diff --git a/BaseModule/src/main/res/anim/up_down_animation.xml b/BaseModule/src/main/res/anim/up_down_animation.xml new file mode 100644 index 00000000..d5032e09 --- /dev/null +++ b/BaseModule/src/main/res/anim/up_down_animation.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/BaseModule/src/main/res/layout/room_cp_vip_view.xml b/BaseModule/src/main/res/layout/room_cp_vip_view.xml new file mode 100644 index 00000000..084af379 --- /dev/null +++ b/BaseModule/src/main/res/layout/room_cp_vip_view.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MainModule/src/main/java/com/xscm/modulemain/BaseMvpActivity.java b/MainModule/src/main/java/com/xscm/modulemain/BaseMvpActivity.java index 9eedc649..8f230d47 100644 --- a/MainModule/src/main/java/com/xscm/modulemain/BaseMvpActivity.java +++ b/MainModule/src/main/java/com/xscm/modulemain/BaseMvpActivity.java @@ -41,6 +41,7 @@ import com.tencent.imsdk.v2.V2TIMValueCallback; import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo; import com.xscm.modulemain.activity.WebViewActivity; import com.xscm.modulemain.activity.main.activity.MainActivity; +import com.xscm.modulemain.activity.user.activity.HeartCpActivity; import com.xscm.modulemain.manager.RoomManager; import com.xscm.moduleutil.BaseEvent; import com.xscm.moduleutil.R; @@ -217,7 +218,7 @@ public abstract class BaseMvpActivity

{ diff --git a/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt b/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt index 1bd41ab0..0a9e9565 100644 --- a/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt +++ b/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt @@ -345,7 +345,8 @@ class RoomActivity : BaseMvpActivity(), qxRedPacketManager!!.delegate = this; // 初始化礼物管理器 - GiftDisplayManager.getInstance().setupDisplayView(mBinding!!.giftContainer) + GiftDisplayManager.getInstance().setupDisplayView(mBinding?.giftContainer) + initPublicScreenFragment() } @@ -1489,153 +1490,13 @@ class RoomActivity : BaseMvpActivity(), roomFragment!!.handleRoomMessage(messageEvent) } }else if(msgType == EMMessageInfo.QXRoomMessageTypeCPText){ + LogUtils.e("CPText", messageEvent.text.rights_icon) if(messageEvent.text.rights_icon.isNotEmpty()){ - mBinding?.roomCpView?.fl!!.visibility = View.VISIBLE - ImageUtils.loadHead(messageEvent.text.fromUserInfo.avatar, mBinding?.roomCpView?.roomCpHead1) - mBinding?.roomCpView?.roomCpName1?.text = messageEvent.text.fromUserInfo.nickname - mBinding?.roomCpView?.roomCpName2?.text = messageEvent.text.toUserInfo.nickname - ImageUtils.loadHead(messageEvent.text.toUserInfo.avatar, mBinding?.roomCpView?.roomCpHead2) - playVap(messageEvent.text.rights_icon, mBinding?.roomCpView?.animCp!!, true) + QXGiftPlayerManager.getInstance(this).displayCPView(messageEvent.text.fromUserInfo.avatar,messageEvent.text.toUserInfo.avatar,messageEvent.text.fromUserInfo.nickname, messageEvent.text.toUserInfo.nickname,messageEvent.text.rights_icon) } } } - fun playVap(url: String, animView: AnimView, isTxk: Boolean) { - - if (!FileUtils.isFileExists(this.cacheDir.absolutePath + url.substring(url.lastIndexOf("/")))) { - LogUtils.e("无缓存") - - downloadAndPlay(this, url, object : GiftAnimView.DownloadCallback { - override fun onSuccess(file: File) { - post(Runnable { - animView.startPlay(file) - }) - } - - override fun onFailure(e: java.lang.Exception) { - LogUtils.e("MP4下载或播放失败: " + e.message) - } - }) - } else { - LogUtils.e("有缓存") - if (isTxk) { - animView.setLoop(20) - } - animView.startPlay( - File(this.cacheDir.absolutePath + url.substring(url.lastIndexOf("/"))) - ) - } - //情侣特效播放回调 - mBinding?.roomCpView?.animCp?.setAnimListener(object : IAnimListener { - override fun onFailed(errorType: Int, errorMsg: String?) { - } - - override fun onVideoComplete() { - runOnUiThread { - //播放结束后隐藏 - mBinding?.roomCpView?.fl.let { view -> - if (view?.visibility == View.VISIBLE) { - // 你的代码 - view?.visibility = GONE - } - } - - - } - } - - override fun onVideoDestroy() { - } - - override fun onVideoRender(frameIndex: Int, config: AnimConfig?) { - } - - override fun onVideoStart() { - } - - }) - } - fun downloadAndPlay( - context: Context, - playImage: String, - callback: GiftAnimView.DownloadCallback? - ) { - - val filePath = context.cacheDir.absolutePath + playImage.substring(playImage.lastIndexOf("/")) - val file = File(filePath) - - if (!FileUtils.isFileExists(this.cacheDir.absolutePath + playImage.substring(playImage.lastIndexOf("/")))) { - LogUtils.e("无缓存") - // 使用OkHttp进行下载 - val client = OkHttpClient() - val request = Request.Builder() - .url(playImage) - .build() - - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - LogUtils.e("MP4下载失败: " + e.toString()) - // 在主线程中回调失败 - post(Runnable { - if (callback != null) { - callback.onFailure(e) - } - }) - } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful()) { - try { - response.body().use { responseBody -> - if (responseBody != null) { - val downloadedFile = File(filePath) - val fos = FileOutputStream(downloadedFile) - fos.write(responseBody.bytes()) - fos.close() - - // 在主线程中回调成功 - post(Runnable { - if (callback != null) { - callback.onSuccess(downloadedFile) - } - }) - } else { - // 在主线程中回调失败 - post(Runnable { - if (callback != null) { - callback.onFailure(IOException("Response body is null")) - } - }) - } - } - } catch (e: java.lang.Exception) { - LogUtils.e("MP4文件保存失败: " + e.message) - // 在主线程中回调失败 - post(Runnable { - if (callback != null) { - callback.onFailure(e) - } - }) - } - } else { - LogUtils.e("MP4下载响应失败") - // 在主线程中回调失败 - post(Runnable { - if (callback != null) { - callback.onFailure(IOException("Response not successful: " + response.code())) - } - }) - } - } - }) - } else { - // 文件已存在,直接回调成功 - if (callback != null) { - callback.onSuccess(file) - } - } - } private var endTime: Long = 0 private fun xlhDjs(endTimeStr: String?) { diff --git a/MainModule/src/main/java/com/xscm/modulemain/activity/user/activity/HeartCpActivity.kt b/MainModule/src/main/java/com/xscm/modulemain/activity/user/activity/HeartCpActivity.kt index d4a3da6f..7d81e936 100644 --- a/MainModule/src/main/java/com/xscm/modulemain/activity/user/activity/HeartCpActivity.kt +++ b/MainModule/src/main/java/com/xscm/modulemain/activity/user/activity/HeartCpActivity.kt @@ -45,6 +45,9 @@ class HeartCpActivity : BaseMvpActivity - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -238,6 +39,207 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MainModule/src/main/res/layout/room_cp_vip_view.xml b/MainModule/src/main/res/layout/room_cp_vip_view.xml deleted file mode 100644 index a551f2fe..00000000 --- a/MainModule/src/main/res/layout/room_cp_vip_view.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -