import React, { useEffect, useRef, useState, useCallback, useContext } from "react";
import { Async } from "@/utils/utils";
// 创建共享的 PIXI.Application 上下文
const PixiAppContext = React.createContext<{
app: PIXI.Application | null;
registerContainer: (id: string, container: PIXI.Container) => void;
unregisterContainer: (id: string) => void;
}>({
app: null,
registerContainer: () => { },
unregisterContainer: () => { },
});
interface PixiAppProviderProps {
children: React.ReactNode;
containerRef: React.RefObject<HTMLDivElement>; // 新增:容器引用
zIndex?: number
}
export const PixiAppProvider: React.FC<PixiAppProviderProps> = ({ children, containerRef, zIndex = 100 }) => {
const pixiAppRef = useRef<PIXI.Application | null>(null);
const [app, setApp] = useState<PIXI.Application | null>(null);
const containersRef = useRef<Map<string, PIXI.Container>>(new Map());
const registerContainer = useCallback((id: string, container: PIXI.Container) => {
containersRef.current.set(id, container);
}, []);
const unregisterContainer = useCallback((id: string) => {
containersRef.current.delete(id);
}, []);
useEffect(() => {
const initPixiApp = async () => {
const PIXI = (await import("pixi.js")).default || (await import("pixi.js"));
if (!pixiAppRef.current && containerRef.current) {
const containerElement = containerRef.current;
const containerRect = containerElement.getBoundingClientRect();
const newApp = new PIXI.Application({
width: containerRect.width,
height: containerRect.height,
transparent: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
});
// 设置 canvas 样式 - 相对于容器定位
newApp.view.style.position = 'absolute';
newApp.view.style.top = '0';
newApp.view.style.left = '0';
newApp.view.style.width = '100%';
newApp.view.style.height = '100%';
newApp.view.style.pointerEvents = 'none';
newApp.view.style.zIndex = containerElement.style.zIndex;
pixiAppRef.current = newApp;
setApp(newApp);
// 将 canvas 添加到容器
containerElement.appendChild(newApp.view);
// 监听容器尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
newApp.renderer.resize(width, height);
}
});
resizeObserver.observe(containerElement);
return () => {
resizeObserver.disconnect();
};
}
};
initPixiApp();
return () => {
if (pixiAppRef.current) {
// 从容器移除 canvas
if (containerRef.current && containerRef.current.contains(pixiAppRef.current.view)) {
containerRef.current.removeChild(pixiAppRef.current.view);
}
pixiAppRef.current.destroy(true);
pixiAppRef.current = null;
}
};
}, [containerRef]);
return (
<PixiAppContext.Provider value={{ app, registerContainer, unregisterContainer }}>
{children}
</PixiAppContext.Provider>
);
};
interface AnimatedProps {
spritesheet: PIXI.Spritesheet | null;
animationKey: string;
animationSpeed?: number;
scale: number;
loop: boolean;
onComplete?: () => void;
onFirstFrame?: (e: boolean) => void,
// 新增:用于定位的 props
targetElement: HTMLDivElement | null; // 目标元素,动画将相对于此元素居中
// const fireContainerRef = useRef<HTMLDivElement>(null);
}
const CommonDragonAnimation: React.FC<AnimatedProps> = ({
spritesheet,
animationKey,
animationSpeed = 0.2,
scale = 1,
loop = true,
onComplete,
onFirstFrame,
targetElement = null,
}) => {
const containerRef = useRef<PIXI.Container | null>(null);
const spriteRef = useRef<PIXI.AnimatedSprite | null>(null);
const [error, setError] = useState<string | null>(null);
const mountedRef = useRef(false);
const animationIdRef = useRef<string>(Math.random().toString(36).substr(2, 9));
const { app, registerContainer, unregisterContainer } = useContext(PixiAppContext);
// 计算相对于容器的位置
const calculateRelativePosition = useCallback(() => {
if (!containerRef.current || !app || !targetElement) return { x: 0, y: 0 };
// 获取容器的位置
const appContainer = app.view.parentElement;
if (!appContainer) return { x: 0, y: 0 };
const containerRect = appContainer.getBoundingClientRect();
const targetRect = targetElement.getBoundingClientRect();
// 计算相对于容器的位置
const relativeX = targetRect.left - containerRect.left + targetRect.width / 2;
const relativeY = targetRect.top - containerRect.top + targetRect.height / 2;
return {
x: relativeX,
y: relativeY
};
}, [app, targetElement]);
// 更新容器位置
const updateContainerPosition = useCallback(() => {
if (!containerRef.current || !app) return;
const position = calculateRelativePosition();
containerRef.current.x = position.x;
containerRef.current.y = position.y;
}, [app, calculateRelativePosition]);
// 创建动画精灵
const createAnimatedSprite = useCallback(
(textures: PIXI.Texture[]) => {
if (!app || textures.length === 0) {
setError("无法创建动画精灵:无有效纹理或 PIXI 应用未初始化");
return;
}
// 创建容器(如果不存在)
if (!containerRef.current) {
containerRef.current = new PIXI.Container();
containerRef.current.pivot.set(containerRef.current.width / 2, containerRef.current.height / 2);
app.stage.addChild(containerRef.current);
registerContainer(animationIdRef.current, containerRef.current);
}
// 更新容器位置
updateContainerPosition();
if (spriteRef.current) {
spriteRef.current.scale.set(scale);
spriteRef.current.textures = textures;
spriteRef.current.loop = loop;
spriteRef.current.animationSpeed = animationSpeed;
if (onComplete) {
spriteRef.current.onComplete = onComplete;
}
spriteRef.current.gotoAndPlay(0);
} else {
const animatedSprite = new PIXI.AnimatedSprite(textures);
animatedSprite.animationSpeed = animationSpeed;
animatedSprite.loop = loop;
animatedSprite.scale.set(scale);
animatedSprite.anchor.set(0.5, 0.5);
animatedSprite.x = 0;
animatedSprite.y = 0;
animatedSprite.play();
if (onFirstFrame && textures.length > 0) {
onFirstFrame(true);
}
if (onFirstFrame) {
const handleFrameChange = (currentFrame: number) => {
if (currentFrame === 0) {
onFirstFrame(true);
animatedSprite.off('frameChange', handleFrameChange);
}
};
animatedSprite.on('frameChange', handleFrameChange);
}
if (onComplete) {
animatedSprite.onComplete = onComplete;
}
containerRef.current.addChild(animatedSprite);
spriteRef.current = animatedSprite;
}
},
[app, animationSpeed, loop, onComplete, scale, registerContainer, updateContainerPosition, onFirstFrame]
);
// 获取动画纹理(保持不变)
const getTextures = useCallback(
(animationKey: string) => {
if (!spritesheet) {
setError("精灵表未加载");
return [];
}
const frames = spritesheet.data.animations[animationKey];
if (!frames) {
setError(`未找到动画: ${animationKey}`);
return [];
}
const textures: PIXI.Texture[] = [];
frames.forEach((frameKey: string) => {
const texture = spritesheet.textures[frameKey];
if (texture) {
textures.push(texture);
} else {
console.warn(`未找到纹理: ${frameKey}`);
}
});
return textures;
},
[spritesheet]
);
// 初始化动画
useEffect(() => {
mountedRef.current = true;
if (!app) {
setError("PIXI 应用未初始化");
return;
}
if (!spritesheet) {
setError("精灵表尚未加载");
return;
}
Async(async () => {
if (!mountedRef.current) return;
const textures = getTextures(animationKey);
if (textures.length > 0) {
createAnimatedSprite(textures);
} else {
setError("没有有效的帧纹理可用于动画");
}
});
return () => {
mountedRef.current = false;
};
}, [spritesheet, animationKey, app, getTextures, createAnimatedSprite]);
// 清理资源
useEffect(() => {
return () => {
mountedRef.current = false;
if (spriteRef.current) {
spriteRef.current.stop();
spriteRef.current.destroy();
spriteRef.current = null;
}
if (containerRef.current) {
if (app && app.stage) {
app.stage.removeChild(containerRef.current);
}
containerRef.current.destroy();
containerRef.current = null;
}
unregisterContainer(animationIdRef.current);
};
}, [app, unregisterContainer]);
if (error) {
return <div style={{ color: "red" }}>错误: {error}</div>;
}
return null;
};
export default React.memo(CommonDragonAnimation);
import { useEffect, useRef, useState, useMemo } from "react";
import { Async } from "@/utils/utils";
interface SpritesheetHookResult {
spritesheet: PIXI.Spritesheet | null;
error: string | null;
}
// 全局缓存清理标记
let globalCacheCleanupDone = false;
/**
* 清理 PIXI 全局缓存中的特定资源
*/
function cleanupPIXICache(resourceId: string, imagePath: string) {
if (!(window as any).PIXI) return;
const PIXI = (window as any).PIXI;
// 清理特定资源的缓存
if (PIXI.utils.TextureCache && PIXI.utils.TextureCache[resourceId]) {
PIXI.utils.TextureCache[resourceId].destroy(true);
delete PIXI.utils.TextureCache[resourceId];
}
if (PIXI.utils.BaseTextureCache && PIXI.utils.BaseTextureCache[resourceId]) {
PIXI.utils.BaseTextureCache[resourceId].destroy();
delete PIXI.utils.BaseTextureCache[resourceId];
}
// 清理带路径的缓存项(错误信息中显示的这种格式)
const pathKeys = Object.keys(PIXI.utils.BaseTextureCache || {}).filter(key =>
key.includes(resourceId) || key.includes(imagePath)
);
pathKeys.forEach(key => {
PIXI.utils.BaseTextureCache[key]?.destroy();
delete PIXI.utils.BaseTextureCache[key];
PIXI.utils.TextureCache[key]?.destroy(true);
delete PIXI.utils.TextureCache[key];
});
}
/**
* 执行一次性全局缓存清理
*/
function performGlobalCacheCleanup() {
if (globalCacheCleanupDone || !(window as any).PIXI) return;
const PIXI = (window as any).PIXI;
// 清理所有纹理缓存
if (PIXI.utils.TextureCache) {
Object.keys(PIXI.utils.TextureCache).forEach(key => {
if (!key.startsWith('global_')) { // 保留一些全局纹理
PIXI.utils.TextureCache[key]?.destroy(true);
delete PIXI.utils.TextureCache[key];
}
});
}
if (PIXI.utils.BaseTextureCache) {
Object.keys(PIXI.utils.BaseTextureCache).forEach(key => {
if (!key.startsWith('global_')) {
PIXI.utils.BaseTextureCache[key]?.destroy();
delete PIXI.utils.BaseTextureCache[key];
}
});
}
globalCacheCleanupDone = true;
}
export function useSpriteSheetJson(imagePath: string, spriteSheetData: any): SpritesheetHookResult {
const [spritesheet, setSpritesheet] = useState<PIXI.Spritesheet | null>(null);
const [error, setError] = useState<string | null>(null);
const loaderRef = useRef<PIXI.Loader | null>(null);
const mountedRef = useRef(true);
// 生成唯一资源 ID - 添加时间戳确保唯一性
const resourceId = useMemo(() => {
const fileName = imagePath.split("/").pop()?.replace(/\.[^/.]+$/, "") || "default";
return `${fileName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}, [imagePath]);
// 修改 spriteSheetData,为帧名称和动画名称添加唯一前缀
const modifiedSpriteSheetData = useMemo(() => {
const newData = JSON.parse(JSON.stringify(spriteSheetData));
const newFrames: Record<string, any> = {};
Object.keys(newData.frames).forEach((key) => {
newFrames[`${resourceId}_${key}`] = newData.frames[key];
});
newData.frames = newFrames;
Object.keys(newData.animations).forEach((animKey) => {
newData.animations[animKey] = newData.animations[animKey].map(
(frame: string) => `${resourceId}_${frame}`
);
});
return newData;
}, [spriteSheetData, resourceId]);
useEffect(() => {
// 在组件挂载时执行全局缓存清理
performGlobalCacheCleanup();
Async(async () => {
const PIXI = (await import("pixi.js")).default || (await import("pixi.js"));
if (!(window as any).PIXI) (window as any).PIXI = PIXI;
// 清理旧资源 - 先清理缓存
cleanupPIXICache(resourceId, imagePath);
if (loaderRef.current) {
loaderRef.current.reset();
loaderRef.current.destroy();
loaderRef.current = null;
}
if (spritesheet) {
// 安全地销毁精灵表
try {
spritesheet.destroy(true);
} catch (e) {
console.warn('销毁精灵表时出错:', e);
}
setSpritesheet(null);
}
// 创建新的加载器实例
loaderRef.current = new PIXI.Loader();
// 设置加载器前缀避免冲突
loaderRef.current.baseUrl = '';
loaderRef.current.add(resourceId, imagePath).load((loader, resources) => {
if (!mountedRef.current) return;
const baseTexture: any = resources[resourceId]?.texture?.baseTexture;
if (!baseTexture) {
setError(`无法加载精灵表图片: ${imagePath}`);
return;
}
// 设置基纹理的缓存ID,避免冲突
baseTexture.textureCacheIds = [resourceId];
const spritesheetInstance = new PIXI.Spritesheet(baseTexture, modifiedSpriteSheetData);
// 解析精灵表
spritesheetInstance.parse(() => {
if (!mountedRef.current) return;
setSpritesheet(spritesheetInstance);
});
});
loaderRef.current.onError.add((err) => {
if (mountedRef.current) {
setError(`加载精灵表失败: ${err.message}`);
}
});
});
return () => {
mountedRef.current = false;
// 清理资源
if (loaderRef.current) {
try {
loaderRef.current.reset();
loaderRef.current.destroy();
} catch (e) {
console.warn('销毁加载器时出错:', e);
}
loaderRef.current = null;
}
if (spritesheet) {
try {
spritesheet.destroy(true);
} catch (e) {
console.warn('销毁精灵表时出错:', e);
}
setSpritesheet(null);
}
// 清理缓存
setTimeout(() => cleanupPIXICache(resourceId, imagePath), 100);
};
}, [imagePath, modifiedSpriteSheetData, resourceId]);
return { spritesheet, error };
}
以上代码为封装的动画组件,请分析并解决问题:
在ios与PC端设备中,一切正常,但在安卓手机设备中,动画位置显示全黑。经测试验证,排除了webGL支持问题、精灵表也能正常解析,动画播放和大小都正常,仅画面全黑。该如何修复?