Files
yusheng-android/BaseModule/src/main/java/com/xscm/moduleutil/widget/AvatarFrameView.java

572 lines
19 KiB
Java
Raw Normal View History

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;
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 "";
}
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)) {
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)) {
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() {
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;
// 关键在执行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;
// 检查是否已销毁
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;
}
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); // 播放一次
}
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);
}
// 增加空值检查
if (mBinding != null) {
2025-10-24 17:52:11 +08:00
mBinding.playView.stopPlay();
}
}
2026-01-22 15:28:44 +08:00
public void stopPlay(){
// 增加空值检查
if (mBinding != null && mBinding.playView.isRunning()) {
mBinding.playView.stopPlay();
}
}
2025-10-24 17:52:11 +08:00
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() {
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()) {
handleSVGAComplete(videoItem, url);
2025-10-24 17:52:11 +08:00
} else {
handleSVGAComplete(videoItem, url);
}
}
@Override
public void onError() {
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);
return;
2025-10-24 17:52:11 +08:00
} else {
// 加载新的SVGA
loadNewSVGA(url);
}
} catch (Exception e) {
LogUtils.e(TAG, "Error loading SVGA: " + e.getMessage());
isPlaying = false;
}
// 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();
// }
2025-10-20 10:16:44 +08:00
}
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-20 10:16:44 +08:00
2025-10-24 17:52:11 +08:00
// 停止播放器
if (mBinding != null && mBinding.playView != null) {
mBinding.playView.stopPlay();
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");
performUICleanup();
2025-10-24 17:52:11 +08:00
}
/**
* 在主线程执行 UI 相关的清理操作
*/
private void performUICleanup() {
try {
// 停止并清理播放器
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;
}
}
}