微信小程序接入lottie动画

发布于:2024-04-25 ⋅ 阅读:(8) ⋅ 点赞:(0)

需求

需要把lottie动画在小程序的环境下进行展示

什么是lottie动画

Airbnb开发并开源。允许设计师将复杂的矢量动画导出为JSON文件,并通过lottie库在移动应用或者Web上无缝地渲染这些动画。这些动画可以在iOSAndroidWeb等多个平台上使用,并且可以以高性能和高质量进行呈现。

和传统的GIFCanvas有什么区别:

  • 矢量动画:lottie动画是基于矢量图形的,动画中的所有元素都是以数学形式描述的,可以无限缩放而不会失真。GIFCanvas动画通常是基于位图的,因此在缩放时可能会失去清晰度。
  • 文件大小:相同动画的产生的文件,lottie的更小
  • 可控制性:lottie有能够控制播放次数、播放快慢、播放开始和结束的监听等。
  • 跨平台:类似java jvm,不同的平台有专门的处理,使得lottie只要数据一致,动画就会一致。

小程序如何引入lottie

安装

使用lottie-miniprogram库,链接。这个库本身是对lottie-web的封装,源码也不多,而且这个库不一定能保证实时的跟上lottie-web的版本更新。有复杂需求的朋友建议直接看下源码怎么对lottie-web的处理,然后直接使用lottie-web

我这里使用的是Taro做的工程,以下的代码都是React的代码。

初始化Canvas载体

初始化一个canvas元素,并type置为2d

<Canvas
  type="2d"
  disableScroll
  id={DIVINATION_CANVAS_ID}
  canvasId={DIVINATION_CANVAS_ID}
></Canvas>

写一个函数用于在组件里面存储canvas实例:

const canvasRef = useRef<HTMLCanvasElement | null>(null)

const initCanvas = () => {
  const query = createSelectorQuery()
  // weixin的canvas完全实现了HTML上canvas的API,这里就直接用HTMLCanvasElement当做canvas的类型
  return new Promise<HTMLCanvasElement>(resolve => {
    if (canvasRef.current) {
      resolve(canvasRef.current)
      return
    }
    query
      .select(`#${DIVINATION_CANVAS_ID}`)
      .fields({ node: true, size: true })
      .exec(res => {
        const canvas = res[0].node
        canvasRef.current = canvas
        resolve(canvas)
      })
  })
}

初始化lottie动画

在页面onReady或者canvas元素onReady的时候,使用lottie加载动画,我这里因为是React,所以直接使用useEffect,又因为我的需求上可能有几个动画需要同时load

 
 const DIVINATION_STATUS_JSON: { [key in DIVINATION_STATUS_ENUM]: string } = {
  [DIVINATION_STATUS_ENUM.X1]:'https://test.com/x1.json',
  [DIVINATION_STATUS_ENUM.X2]:'https://test.com/x2.json',
  [DIVINATION_STATUS_ENUM.X3]:'https://test.com/x3.json'
}

const animationRef = useRef<
    | {
        [key in DIVINATION_STATUS_ENUM]: ReturnType<typeof lottie.loadAnimation> | null
      }
  >({
    [DIVINATION_STATUS_ENUM.X1]: null,
    [DIVINATION_STATUS_ENUM.X2]: null,
    [DIVINATION_STATUS_ENUM.X3]: null
  })
  
const initAnimations = async () => {
  const canvas = await initCanvas()
  // 载入
  lottie.setup(canvas)
  const ctx = canvas.getContext('2d')
  if (!ctx) return
  // load多个动画 用ref存储
  Object.entries(DIVINATION_STATUS_JSON).forEach(([key, value]) => {
    const animation = lottie.loadAnimation({
      loop: false,
      autoplay: false,
      name: `${DIVINATION_CANVAS_ID}-${key}`,
      path: value,
      rendererSettings: {
        context: ctx
      }
    })
    animationRef.current = {
      ...(animationRef.current ?? {}),
      [key]: animation
    }
  })
}

注意这里有一个坑点就小程序lottie-miniprogram库的path在小程序里面只支持在线地址,但是在lottie-web里面path是可以放在本地的。所以这里要去研究下。

如果你想放在线上而没有地址或者域名,可以考虑:

  • jsdelivr:类似免费cdn,或者oss,只能在开发调试的时候使用,并且勾上“不校验合法域名”,jsdelivr在国内的备案前几年被吊销了,小程序线上环境访问不了,需要有国内ICP备案。
  • 云厂商OSS:把动画json文件传到oss上,通过公开只读方式拿到线上地址,好处就是肯定有备案,稳定性也能保证,缺点就是要花一小部分钱(量少的时候一个月几块,流量中等的时候也只有十几块左右?)。

组件暴露方法启动动画

组件使用forwardRef,在组件内部暴露一个方法启动动画,并监听动画播放后的结束,可以监听complete或者enterFrame,视实际业务为准。

useImperativeHandle(ref, () =>{
    startAnimation: (type: DIVINATION_STATUS_ENUM, onEnd?: () => void) => {
      const animations = animationRef.current
      const curTypeAnimation = animations[type]
      if (!curTypeAnimation) return
      // 重置到lottie的第一帧并播放
      curTypeAnimation.goToAndPlay(0, true)
      const completeLister = () => {
        curTypeAnimation.goToAndStop(130, true)
        // 这里记得remove listener
        curTypeAnimation.removeEventListener('complete', completeLister)
        if (onEnd) {
          onEnd()
        }
      }
      //监听播放完成
      curTypeAnimation.addEventListener('complete', completeLister)
    }
)

或者

// 监听播放到某一帧
const completeLister = e => {
    const { currentTime } = e
    let onceOnKnownResult = false
    if (!onceOnKnownResult && Number(currentTime) > 100) {
      onceOnKnownResult = true
      onKnownResult?.(type)
    }
    if (Number(currentTime) > 100) {
      curTypeAnimation.goToAndStop(100, true)
      curTypeAnimation.removeEventListener('enterFrame', completeLister)
      onEnd?.(type)
    }
  }  
curTypeAnimation.addEventListener('enterFrame', completeLister)

问题一:如何让path能够本地

通过lottieanimationData:

import LottieAim from '@/constants/lottie/sheng'
const animation = lottie.loadAnimation({
    loop: false,
    autoplay: false,
    name: `${DIVINATION_CANVAS_ID}-${key}`,
    // 替换为animationData
    animationData: LottieAim,
    // path: value,
    rendererSettings: {
      context: ctx
    }
  })

lottiejson数据复制出来,新建一个文件保存:

//sheng.ts
// 这里一个小tip,先const data = {json数据},会自动将json格式格式化为object,再改成export default
export defaul {}//数据

问题二:lottie在canvas渲染出来后,有失真情况

在获取到canvas的时候进行放大dpr再缩小dpr,原理是canvas按放大后的尺寸渲染,initCanvas改造如下:

query
    .select(`#${DIVINATION_CANVAS_ID}`)
    .fields({ node: true, size: true })
    .exec(res => {
      const canvas = res[0].node
      const ctx = canvas.getContext('2d')
      const systemInfo = getSystemInfoSync()
      const dpr = systemInfo.pixelRatio
      canvas.width = DIVINATION_CONTAINER_SIZE * dpr
      canvas.height = DIVINATION_CONTAINER_SIZE * dpr
      ctx.scale(dpr, dpr)
      canvasRef.current = canvas
      resolve(canvas)
    })

问题三:canvas渲染出来的层级太高,当有弹窗的情况会暴露在弹窗外

模拟器上会有这个问题,线上版本不会有