2025-10-20 10:16:44 +08:00
|
|
|
|
package com.xscm.moduleutil.widget;
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context;
|
|
|
|
|
|
import android.os.Handler;
|
|
|
|
|
|
import android.os.Looper;
|
|
|
|
|
|
import android.util.AttributeSet;
|
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
|
import android.view.LayoutInflater;
|
|
|
|
|
|
import android.view.View;
|
|
|
|
|
|
import android.widget.FrameLayout;
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
|
|
import androidx.databinding.DataBindingUtil;
|
|
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
import com.blankj.utilcode.util.LogUtils;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
import com.opensource.svgaplayer.SVGACallback;
|
|
|
|
|
|
import com.opensource.svgaplayer.SVGADrawable;
|
|
|
|
|
|
import com.opensource.svgaplayer.SVGADynamicEntity;
|
|
|
|
|
|
import com.opensource.svgaplayer.SVGAImageView;
|
|
|
|
|
|
import com.opensource.svgaplayer.SVGAParser;
|
|
|
|
|
|
import com.opensource.svgaplayer.SVGAVideoEntity;
|
|
|
|
|
|
import com.xscm.moduleutil.R;
|
|
|
|
|
|
import com.xscm.moduleutil.databinding.RoomViewSvgaAnimBinding;
|
|
|
|
|
|
import com.xscm.moduleutil.utils.logger.Logger;
|
|
|
|
|
|
|
|
|
|
|
|
import java.io.File;
|
2025-10-24 17:52:11 +08:00
|
|
|
|
import java.io.FileOutputStream;
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
|
import java.lang.ref.WeakReference;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
import java.net.URL;
|
2025-10-24 17:52:11 +08:00
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
import java.util.concurrent.BlockingQueue;
|
|
|
|
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
import okhttp3.Call;
|
|
|
|
|
|
import okhttp3.Callback;
|
|
|
|
|
|
import okhttp3.OkHttpClient;
|
|
|
|
|
|
import okhttp3.Request;
|
|
|
|
|
|
import okhttp3.Response;
|
|
|
|
|
|
import okhttp3.ResponseBody;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
public class AvatarFrameView extends FrameLayout {
|
2025-12-02 19:35:18 +08:00
|
|
|
|
|
|
|
|
|
|
private boolean isMute = false;
|
2026-01-07 16:53:36 +08:00
|
|
|
|
|
2025-12-02 19:35:18 +08:00
|
|
|
|
public void setMute(boolean b) {
|
|
|
|
|
|
this.isMute = b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
public enum RenderType {SVGA, MP4}
|
|
|
|
|
|
|
|
|
|
|
|
private RenderType renderType;
|
|
|
|
|
|
private SVGAImageView svgaSurface;
|
|
|
|
|
|
private int mType;//1:循环播放 2:播放一次停止播放
|
2025-10-24 17:52:11 +08:00
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
private boolean isPlaying = false;
|
2025-10-24 17:52:11 +08:00
|
|
|
|
// 添加销毁标记
|
|
|
|
|
|
private boolean isDestroyed = false;
|
|
|
|
|
|
private static final String TAG = "AvatarFrameView";
|
|
|
|
|
|
private RoomViewSvgaAnimBinding mBinding;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
private static final int MAX_SVGA_CACHE_SIZE = 3;
|
|
|
|
|
|
private final Map<String, WeakReference<SVGAVideoEntity>> svgaCache = new LruCache<>(MAX_SVGA_CACHE_SIZE);
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public AvatarFrameView(@NonNull Context context) {
|
|
|
|
|
|
this(context, null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public AvatarFrameView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
|
|
|
|
|
this(context, attrs, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public AvatarFrameView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
|
|
|
|
super(context, attrs, defStyleAttr);
|
|
|
|
|
|
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.room_view_svga_anim, this, true);
|
|
|
|
|
|
initViews();
|
2025-10-24 17:52:11 +08:00
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void initViews() {
|
|
|
|
|
|
// 初始化 SVGA View
|
|
|
|
|
|
svgaSurface = new SVGAImageView(getContext());
|
|
|
|
|
|
svgaSurface.setVisibility(View.GONE);
|
|
|
|
|
|
addView(svgaSurface);
|
|
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
private String getFileExtension(String url) {
|
|
|
|
|
|
if (url == null || url.isEmpty()) return "";
|
|
|
|
|
|
int dotIndex = url.lastIndexOf(".");
|
|
|
|
|
|
if (dotIndex > 0 && dotIndex < url.length() - 1) {
|
|
|
|
|
|
return url.substring(dotIndex + 1).toLowerCase(); // 返回 "mp4", "svga" 等
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
2026-01-07 16:53:36 +08:00
|
|
|
|
public void setSource(String url, int type2) {
|
|
|
|
|
|
// 添加到播放队列
|
|
|
|
|
|
PlayItem item = new PlayItem(url, type2);
|
|
|
|
|
|
processPlayItem(item);
|
|
|
|
|
|
Logger.d("AvatarFrameView", "Added to queue, queue size: url: " + url);
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void processPlayItem(PlayItem item) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
String ext = getFileExtension(item.url);
|
|
|
|
|
|
if ("svga".equalsIgnoreCase(ext)) {
|
2026-01-07 16:53:36 +08:00
|
|
|
|
renderType = RenderType.SVGA;
|
|
|
|
|
|
mType = item.type;
|
|
|
|
|
|
if (mBinding != null) {
|
|
|
|
|
|
mBinding.playView.setVisibility(View.GONE);
|
|
|
|
|
|
}
|
|
|
|
|
|
loadSVGA(item.url);
|
2025-10-24 17:52:11 +08:00
|
|
|
|
} else if ("mp4".equalsIgnoreCase(ext)) {
|
2026-01-07 16:53:36 +08:00
|
|
|
|
renderType = RenderType.MP4;
|
|
|
|
|
|
mType = item.type;
|
|
|
|
|
|
if (mBinding != null) {
|
|
|
|
|
|
mBinding.playView.setVisibility(View.VISIBLE);
|
|
|
|
|
|
downloadAndPlayMp4(item.url);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mBinding = DataBindingUtil.inflate(LayoutInflater.from(getContext()), R.layout.room_view_svga_anim, this, true);
|
|
|
|
|
|
mBinding.playView.setVisibility(View.VISIBLE);
|
|
|
|
|
|
downloadAndPlayMp4(item.url);
|
|
|
|
|
|
}
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error processing play item: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public boolean isPlaying() {
|
2026-01-07 16:53:36 +08:00
|
|
|
|
if (mBinding != null) {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
return mBinding.playView.isRunning();
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
return true;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
boolean isTxk = false;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
public void downloadAndPlayMp4(String url) {
|
|
|
|
|
|
// 提取文件名
|
|
|
|
|
|
String fileName = url.substring(url.lastIndexOf("/"));
|
|
|
|
|
|
String filePath = getContext().getCacheDir().getAbsolutePath() + fileName;
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
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(url)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
|
|
client.newCall(request).enqueue(new Callback() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onFailure(Call call, IOException e) {
|
|
|
|
|
|
LogUtils.e("MP4下载失败: " + e.toString());
|
|
|
|
|
|
}
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
// 更简单的优化版本
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onResponse(Call call, Response response) throws IOException {
|
|
|
|
|
|
// 在异步回调中首先检查是否已销毁
|
|
|
|
|
|
if (isDestroyed) {
|
|
|
|
|
|
LogUtils.w(TAG, "View destroyed before download completed");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
LogUtils.d("@@@@", "onResponse" + Thread.currentThread().getName());
|
|
|
|
|
|
if (response.isSuccessful()) {
|
|
|
|
|
|
try (ResponseBody responseBody = response.body()) {
|
|
|
|
|
|
if (responseBody != null) {
|
|
|
|
|
|
// 在后台线程处理文件保存
|
|
|
|
|
|
|
|
|
|
|
|
String fileName = url.substring(url.lastIndexOf("/"));
|
|
|
|
|
|
String filePath = getContext().getCacheDir().getAbsolutePath() + fileName;
|
|
|
|
|
|
File downloadedFile = new File(filePath);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用流式传输避免大文件卡顿
|
|
|
|
|
|
try (InputStream inputStream = responseBody.byteStream();
|
|
|
|
|
|
FileOutputStream fos = new FileOutputStream(downloadedFile)) {
|
|
|
|
|
|
|
|
|
|
|
|
// 定义缓冲区大小(8KB)
|
|
|
|
|
|
byte[] buffer = new byte[1024 * 1024];
|
|
|
|
|
|
int bytesRead;
|
|
|
|
|
|
|
|
|
|
|
|
// 流式读取并写入文件
|
|
|
|
|
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
|
|
|
|
fos.write(buffer, 0, bytesRead);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fos.flush();
|
|
|
|
|
|
isTxk = true;
|
2026-01-07 16:53:36 +08:00
|
|
|
|
// 关键:在执行UI操作前再次检查是否已销毁
|
|
|
|
|
|
if (downloadedFile.exists()) {
|
|
|
|
|
|
LogUtils.d("@@@@Thread", Thread.currentThread().getName());
|
|
|
|
|
|
playMp4File(downloadedFile); // 使用正确的文件引用
|
|
|
|
|
|
} else {
|
|
|
|
|
|
LogUtils.w(TAG, "View destroyed or file not exist after download");
|
|
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
|
LogUtils.e("MP4文件保存失败: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e("MP4文件保存失败: " + e.getMessage());
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
LogUtils.e("MP4下载响应失败");
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
isTxk = true;
|
2026-01-07 16:53:36 +08:00
|
|
|
|
// 检查是否已销毁
|
|
|
|
|
|
if (file.exists()) {
|
|
|
|
|
|
LogUtils.e("有缓存:" + file.exists() + "====" + file.getAbsolutePath());
|
|
|
|
|
|
playMp4File(file);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
LogUtils.w(TAG, "有缓存2222222222222");
|
|
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void playMp4File(File file) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 双重检查确保组件未被销毁
|
|
|
|
|
|
if (isDestroyed) {
|
|
|
|
|
|
LogUtils.w(TAG, "Attempt to play MP4 file after view destroyed");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-07 16:53:36 +08:00
|
|
|
|
if (mBinding == null) {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
LogUtils.w(TAG, "PlayView is null");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (file != null && file.exists()) {
|
|
|
|
|
|
// 设置循环次数(根据mType决定)
|
|
|
|
|
|
if (mType == 1 || mType == 3) {
|
|
|
|
|
|
mBinding.playView.setLoop(Integer.max(1, 999999999)); // 无限循环
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mBinding.playView.setLoop(1); // 播放一次
|
|
|
|
|
|
}
|
2026-01-07 16:53:36 +08:00
|
|
|
|
mBinding.playView.setMute(isMute);
|
2025-10-24 17:52:11 +08:00
|
|
|
|
// 开始播放前检查视图状态
|
|
|
|
|
|
if (!isDestroyed && mBinding != null && mBinding.playView != null) {
|
|
|
|
|
|
mBinding.playView.startPlay(file);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
LogUtils.w(TAG, "View was destroyed before MP4 playback started");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
LogUtils.e("播放MP4文件出错: 文件不存在或已损坏");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e("播放MP4文件出错: " + e.getMessage());
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
private void handleSVGAComplete(SVGAVideoEntity videoItem, String url) {
|
|
|
|
|
|
if (svgaSurface == null) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 缓存实体(使用弱引用)
|
|
|
|
|
|
if (svgaCache.size() < MAX_SVGA_CACHE_SIZE) {
|
|
|
|
|
|
svgaCache.put(url, new WeakReference<>(videoItem));
|
|
|
|
|
|
}
|
|
|
|
|
|
SVGADrawable drawable = new SVGADrawable(videoItem, new SVGADynamicEntity());
|
|
|
|
|
|
svgaSurface.setImageDrawable(drawable);
|
|
|
|
|
|
svgaSurface.setCallback(new SVGACallback() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onStep(int i, double v) {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onRepeat() {
|
|
|
|
|
|
// 循环播放处理
|
|
|
|
|
|
if (mType != 1) { // 非循环播放
|
|
|
|
|
|
svgaSurface.stopAnimation();
|
|
|
|
|
|
svgaSurface.clearAnimation();
|
|
|
|
|
|
svgaSurface.setImageDrawable(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onPause() {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onFinished() {
|
|
|
|
|
|
if (mType == 1) { // 循环播放
|
|
|
|
|
|
// 继续循环播放
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// 设置循环次数
|
|
|
|
|
|
if (mType == 1 || mType == 3) {
|
|
|
|
|
|
svgaSurface.setLoops(0); // 无限循环
|
|
|
|
|
|
} else {
|
|
|
|
|
|
svgaSurface.setLoops(1); // 播放一次
|
|
|
|
|
|
}
|
|
|
|
|
|
svgaSurface.startAnimation();
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error handling SVGA completion: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void stopAll() {
|
|
|
|
|
|
if (svgaSurface != null) {
|
|
|
|
|
|
svgaSurface.stopAnimation();
|
|
|
|
|
|
svgaSurface.clearAnimation();
|
|
|
|
|
|
svgaSurface.setImageDrawable(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 增加空值检查
|
2026-01-07 16:53:36 +08:00
|
|
|
|
if (mBinding != null) {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
mBinding.playView.stopPlay();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void playCachedSVGA(SVGAVideoEntity videoItem) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
SVGADrawable drawable = new SVGADrawable(videoItem, new SVGADynamicEntity());
|
|
|
|
|
|
svgaSurface.setImageDrawable(drawable);
|
|
|
|
|
|
svgaSurface.setCallback(new SVGACallback() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onStep(int i, double v) {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onRepeat() {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onPause() {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onFinished() {
|
2026-01-07 16:53:36 +08:00
|
|
|
|
isPlaying = false;
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
// 设置循环次数
|
|
|
|
|
|
if (mType == 1 || mType == 3) {
|
|
|
|
|
|
svgaSurface.setLoops(0); // 无限循环
|
|
|
|
|
|
} else {
|
|
|
|
|
|
svgaSurface.setLoops(1); // 播放一次
|
|
|
|
|
|
}
|
|
|
|
|
|
svgaSurface.startAnimation();
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error playing cached SVGA: " + e.getMessage());
|
|
|
|
|
|
isPlaying = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void loadNewSVGA(String url) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
new SVGAParser(getContext()).parse(new URL(url), new SVGAParser.ParseCompletion() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onComplete(SVGAVideoEntity videoItem) {
|
|
|
|
|
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
2026-01-07 16:53:36 +08:00
|
|
|
|
handleSVGAComplete(videoItem, url);
|
2025-10-24 17:52:11 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
handleSVGAComplete(videoItem, url);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onError() {
|
2026-01-07 16:53:36 +08:00
|
|
|
|
isPlaying = false;
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error parsing SVGA: " + e.getMessage());
|
|
|
|
|
|
isPlaying = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
private void loadSVGA(String url) {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
svgaSurface.setVisibility(View.VISIBLE);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查缓存
|
|
|
|
|
|
WeakReference<SVGAVideoEntity> cachedRef = svgaCache.get(url);
|
|
|
|
|
|
SVGAVideoEntity cachedEntity = cachedRef != null ? cachedRef.get() : null;
|
|
|
|
|
|
|
|
|
|
|
|
if (cachedEntity != null) {
|
|
|
|
|
|
// 使用缓存的实体
|
|
|
|
|
|
playCachedSVGA(cachedEntity);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 加载新的SVGA
|
|
|
|
|
|
loadNewSVGA(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error loading SVGA: " + e.getMessage());
|
|
|
|
|
|
isPlaying = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
svgaSurface.setVisibility(View.VISIBLE);
|
|
|
|
|
|
try {
|
|
|
|
|
|
new SVGAParser(getContext()).parse(new URL(url), new SVGAParser.ParseCompletion() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onComplete(SVGAVideoEntity videoItem) {
|
|
|
|
|
|
SVGADrawable drawable = new SVGADrawable(videoItem, new SVGADynamicEntity());
|
|
|
|
|
|
svgaSurface.setImageDrawable(drawable);
|
|
|
|
|
|
svgaSurface.setCallback(new SVGACallback() {
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onStep(int i, double v) {
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onRepeat() {
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onPause() {
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onFinished() {
|
|
|
|
|
|
isPlaying = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
svgaSurface.startAnimation();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onError() {
|
|
|
|
|
|
isPlaying = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void clearPrevious() {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 停止并清理 SVGA 动画
|
|
|
|
|
|
if (svgaSurface != null && svgaSurface.getDrawable() instanceof SVGADrawable) {
|
|
|
|
|
|
SVGADrawable drawable = (SVGADrawable) svgaSurface.getDrawable();
|
|
|
|
|
|
if (drawable != null) {
|
|
|
|
|
|
drawable.stop();
|
|
|
|
|
|
// 清理 SVGADrawable 中的资源
|
|
|
|
|
|
svgaSurface.clearAnimation();
|
|
|
|
|
|
svgaSurface.setImageDrawable(null);
|
|
|
|
|
|
}
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
// 隐藏所有视图
|
|
|
|
|
|
if (svgaSurface != null) svgaSurface.setVisibility(View.GONE);
|
2025-10-25 18:07:21 +08:00
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
|
2025-10-24 17:52:11 +08:00
|
|
|
|
// 停止播放器
|
|
|
|
|
|
if (mBinding != null && mBinding.playView != null) {
|
|
|
|
|
|
mBinding.playView.stopPlay();
|
2025-10-25 18:07:21 +08:00
|
|
|
|
mBinding.playView.setVisibility(View.GONE);
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error in clearPrevious: " + e.getMessage());
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 简单的 LRU Cache 实现
|
|
|
|
|
|
private static class LruCache<K, V> extends LinkedHashMap<K, V> {
|
|
|
|
|
|
private final int maxSize;
|
|
|
|
|
|
|
|
|
|
|
|
public LruCache(int maxSize) {
|
|
|
|
|
|
super(16, 0.75f, true);
|
|
|
|
|
|
this.maxSize = maxSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
protected boolean removeEldestEntry(Entry<K, V> eldest) {
|
|
|
|
|
|
return size() > maxSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 释放所有资源
|
|
|
|
|
|
*/
|
|
|
|
|
|
private void releaseResources() {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
LogUtils.d(TAG, "Releasing all resources");
|
2026-01-07 16:53:36 +08:00
|
|
|
|
performUICleanup();
|
2025-10-24 17:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 在主线程执行 UI 相关的清理操作
|
|
|
|
|
|
*/
|
|
|
|
|
|
private void performUICleanup() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 停止并清理播放器
|
2026-01-07 16:53:36 +08:00
|
|
|
|
if (mBinding != null) {
|
2025-10-24 17:52:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
mBinding.playView.stopPlay();
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
Logger.e(TAG, "Error stopping playView: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 清理 SVGA 资源
|
|
|
|
|
|
if (svgaSurface != null) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
svgaSurface.stopAnimation(true);
|
|
|
|
|
|
svgaSurface.clear();
|
|
|
|
|
|
svgaSurface.clearAnimation();
|
|
|
|
|
|
svgaSurface.setImageDrawable(null);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
Logger.e(TAG, "Error releasing SVGA resources: " + e.getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
Logger.e(TAG, "Error in performUICleanup: " + e.getMessage());
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 公共释放方法,用于外部主动释放资源
|
|
|
|
|
|
*/
|
|
|
|
|
|
public void release() {
|
|
|
|
|
|
Logger.d("AvatarFrameView", "Public release called");
|
2025-10-24 17:52:11 +08:00
|
|
|
|
isDestroyed = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 清空播放队列
|
|
|
|
|
|
clearQueue();
|
|
|
|
|
|
// 释放所有资源
|
|
|
|
|
|
releaseResources();
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
|
LogUtils.e(TAG, "Error in AvatarFrameView release: " + e.getMessage());
|
2025-10-20 10:16:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-24 17:52:11 +08:00
|
|
|
|
|
2025-10-20 10:16:44 +08:00
|
|
|
|
public void clearQueue() {
|
|
|
|
|
|
isPlaying = false;
|
|
|
|
|
|
// 清理当前正在播放的内容
|
|
|
|
|
|
clearPrevious();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static class PlayItem {
|
|
|
|
|
|
String url;
|
|
|
|
|
|
int type;
|
|
|
|
|
|
|
|
|
|
|
|
PlayItem(String url, int type) {
|
|
|
|
|
|
this.url = url;
|
|
|
|
|
|
this.type = type;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|