1:添加cp进场动画效果
2:修改cp礼物弹框 3:添加组成cp后进入心动空间跳转
This commit is contained in:
@@ -4534,8 +4534,7 @@ public class RetrofitClient {
|
||||
});
|
||||
}
|
||||
|
||||
public void roomUserCharmList(String room_id, String
|
||||
user_id, BaseObserver<List<RoomUserCharmListBean>> observer) {
|
||||
public void roomUserCharmList(String room_id, String user_id, BaseObserver<List<RoomUserCharmListBean>> observer) {
|
||||
sApiServer.roomUserCharmList(room_id, user_id).enqueue(new Callback<BaseModel<List<RoomUserCharmListBean>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<BaseModel<List<RoomUserCharmListBean>>> call, Response<BaseModel<List<RoomUserCharmListBean>>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> 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<AnimationTask> animationQueue = new LinkedList<>();
|
||||
private boolean isAnimationRunning = false;
|
||||
private final Object animationLock = new Object();
|
||||
|
||||
// 下载任务缓存
|
||||
private final Queue<DownloadTask> 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<OnPlaybackCompleteListener> 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<String, Void, String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Runnable>(),
|
||||
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() {
|
||||
|
||||
11
BaseModule/src/main/res/anim/up_down_animation.xml
Normal file
11
BaseModule/src/main/res/anim/up_down_animation.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shareInterpolator="false">
|
||||
<translate
|
||||
android:duration="2000"
|
||||
android:fromYDelta="0"
|
||||
android:toYDelta="-20"
|
||||
android:repeatCount="infinite"
|
||||
android:repeatMode="reverse"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
|
||||
</set>
|
||||
103
BaseModule/src/main/res/layout/room_cp_vip_view.xml
Normal file
103
BaseModule/src/main/res/layout/room_cp_vip_view.xml
Normal file
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
|
||||
<com.tencent.qgame.animplayer.AnimView
|
||||
android:id="@+id/anim_cp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.58" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_head"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/guideline">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.makeramen.roundedimageview.RoundedImageView
|
||||
android:id="@+id/room_cp_head1"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@mipmap/ic_launcher_app"
|
||||
app:riv_oval="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/room_cp_name1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:enabled="true"
|
||||
android:maxEms="5"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/color_999999"
|
||||
android:textSize="8sp"
|
||||
tools:text="" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="@dimen/dp_40"
|
||||
android:layout_height="0dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.makeramen.roundedimageview.RoundedImageView
|
||||
android:id="@+id/room_cp_head2"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@mipmap/ic_launcher_app"
|
||||
app:riv_oval="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/room_cp_name2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:enabled="true"
|
||||
android:maxEms="5"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/color_999999"
|
||||
android:textSize="8sp"
|
||||
tools:text="" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
Reference in New Issue
Block a user