useAnimatedImageValue
This commit is contained in:
parent
f2f460850c
commit
aebbb13f87
|
|
@ -1,11 +1,11 @@
|
|||
import { Image, type ImageRef } from 'expo-image'
|
||||
import { memo, useEffect, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { type ViewStyle } from 'react-native'
|
||||
import { useSharedValue } from 'react-native-reanimated'
|
||||
import { useDerivedValue, useSharedValue } from 'react-native-reanimated'
|
||||
|
||||
import {
|
||||
Canvas,
|
||||
Image,
|
||||
Image as SkiaImage,
|
||||
useAnimatedImageValue,
|
||||
useImage,
|
||||
useVideo
|
||||
} from "@shopify/react-native-skia";
|
||||
|
|
@ -17,12 +17,13 @@ type Props = {
|
|||
style?: ViewStyle
|
||||
width?: number
|
||||
autoplay?: boolean // 控制动画播放,默认 true
|
||||
} & React.ComponentProps<typeof Video>
|
||||
}
|
||||
|
||||
// 默认宽度256半屏宽度
|
||||
const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ...videoProps }: Props) => {
|
||||
const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true }: Props) => {
|
||||
const [urlFinal, setUrlFinal] = useState('')
|
||||
const imageRef = useRef<ImageRef>(null)
|
||||
const pausedValue = useSharedValue(!autoplay)
|
||||
const looping = useSharedValue(true)
|
||||
|
||||
const createUrl = (url: string) => {
|
||||
return `https://modal-dev.bowong.cc/api/custom/video/converter/v2?media_url=${encodeURI(url)}&options=compression_level=3,quality=70,loop=true,resolution=${width}x${width},fps=24`
|
||||
|
|
@ -34,6 +35,15 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
|||
return lowerUrl?.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|svg)(\?.*)?$/i)
|
||||
}
|
||||
|
||||
// 检测是否为动画图片(GIF 或动画 WebP)
|
||||
const isAnimatedImage = (u?: string | null) => {
|
||||
if (!u) return false
|
||||
const lowerUrl = u.toLowerCase()
|
||||
// GIF 文件通常是动画的
|
||||
// WebP 可能是动画的,需要通过内容判断,这里先简单判断扩展名
|
||||
return lowerUrl.includes('.gif') || lowerUrl.includes('.webp')
|
||||
}
|
||||
|
||||
async function resolveRedirect(url: string) {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
|
@ -85,14 +95,8 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
|||
|
||||
// 控制动画播放/停止
|
||||
useEffect(() => {
|
||||
if (imageRef?.current) {
|
||||
if (autoplay) {
|
||||
imageRef.current?.startAnimating()
|
||||
} else {
|
||||
imageRef.current?.stopAnimating()
|
||||
}
|
||||
}
|
||||
}, [autoplay])
|
||||
pausedValue.value = !autoplay
|
||||
}, [autoplay, pausedValue])
|
||||
|
||||
if (!url) return null
|
||||
|
||||
|
|
@ -122,13 +126,25 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
|||
// 判断是否为图片(需要检查原始 URL 和最终 URL)
|
||||
const shouldUseImage = isImageFile || (displayUrl ? isImg(displayUrl) : false)
|
||||
|
||||
// 判断是否为动画图片
|
||||
const shouldUseAnimatedImage = shouldUseImage && isAnimatedImage(displayUrl)
|
||||
|
||||
// 判断是否为静态图片
|
||||
const shouldUseStaticImage = shouldUseImage && !shouldUseAnimatedImage
|
||||
|
||||
// 只有在明确是视频链接且不是图片时才调用 useVideo,避免触发 Skia.Video 原生异常
|
||||
const shouldUseVideo = !shouldUseImage && isVideoUrl(displayUrl)
|
||||
|
||||
// Hooks 必须在顶层调用,不能条件调用
|
||||
// 对于图片,使用 useImage;对于视频,使用 useVideo
|
||||
// 对于静态图片,使用 useImage
|
||||
// 对于动画图片,使用 useAnimatedImageValue(支持 autoplay 控制)
|
||||
// 对于视频,使用 useVideo
|
||||
// 如果 displayUrl 为 null,hooks 会返回 null
|
||||
const image = useImage(shouldUseImage ? displayUrl : null)
|
||||
const staticImage = useImage(shouldUseStaticImage ? displayUrl : null)
|
||||
const animatedImage = useAnimatedImageValue(
|
||||
shouldUseAnimatedImage ? displayUrl : null,
|
||||
shouldUseAnimatedImage ? pausedValue : undefined
|
||||
)
|
||||
const videoResult = useVideo(
|
||||
shouldUseVideo ? displayUrl : null,
|
||||
{
|
||||
|
|
@ -137,38 +153,67 @@ const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ..
|
|||
}
|
||||
)
|
||||
|
||||
// 使用 useDerivedValue 从 videoResult.currentFrame 获取当前帧
|
||||
// 如果 videoResult 为 null,videoFrame 也会是 null
|
||||
const videoFrame = useDerivedValue(() => {
|
||||
return videoResult?.currentFrame.value ?? null
|
||||
})
|
||||
|
||||
if (!url || !displayUrl) return null
|
||||
|
||||
const canvasStyle = style || {}
|
||||
const canvasWidth = (canvasStyle.width as number) || width
|
||||
const canvasHeight = (canvasStyle.height as number) || width
|
||||
|
||||
// 如果是图片且已加载
|
||||
if (shouldUseImage && image) {
|
||||
// 如果是静态图片且已加载
|
||||
if (shouldUseStaticImage && staticImage) {
|
||||
return (
|
||||
<Image
|
||||
cachePolicy="disk"
|
||||
source={{ uri: url }}
|
||||
style={style as any}
|
||||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||||
/>
|
||||
<Canvas style={[{ width: canvasWidth, height: canvasHeight }, canvasStyle]}>
|
||||
<SkiaImage
|
||||
image={staticImage}
|
||||
x={0}
|
||||
y={0}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
fit="cover"
|
||||
/>
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
// 移除 key 避免组件重建导致闪烁,使用 transition 实现平滑切换
|
||||
<Image
|
||||
ref={imageRef}
|
||||
// 只使用 disk 缓存,减少内存占用
|
||||
cachePolicy="disk"
|
||||
// 添加 recyclingKey 帮助内存回收
|
||||
recyclingKey={urlFinal}
|
||||
source={{ uri: urlFinal }}
|
||||
style={style as any}
|
||||
autoplay={autoplay}
|
||||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||||
/>
|
||||
)
|
||||
// 如果是动画图片(GIF 或动画 WebP)
|
||||
if (shouldUseAnimatedImage && animatedImage) {
|
||||
return (
|
||||
<Canvas style={[{ width: canvasWidth, height: canvasHeight }, canvasStyle]}>
|
||||
<SkiaImage
|
||||
image={animatedImage}
|
||||
x={0}
|
||||
y={0}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
fit="cover"
|
||||
/>
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
|
||||
// 如果是视频,使用 currentFrame SharedValue
|
||||
if (shouldUseVideo && videoResult && videoFrame) {
|
||||
return (
|
||||
<Canvas style={[{ width: canvasWidth, height: canvasHeight }, canvasStyle]}>
|
||||
<SkiaImage
|
||||
image={videoFrame}
|
||||
x={0}
|
||||
y={0}
|
||||
width={canvasWidth}
|
||||
height={canvasHeight}
|
||||
fit="cover"
|
||||
/>
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default memo(VideoBox)
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue