使用技术栈

  • react hooks ts
  • taro 小程序

整体思路

当需要滚动的文字从右侧滚到左侧并完全消失,此时下一个文字到达第一个文字的初始位置时,将整个动画滚动区域重置,相当于回到初始状态,紧接着继续重新动画。主要借鉴了taro-uinoticebar动画思路。

文末放上一开始没用animation的写法的版本(但是小程序会有卡顿,所以就去参考taro-ui了)

开发流程

1. 获取宽度

首先要获取滚动区域的整体宽度,然后再获取animation属性给的元素的宽度,最后再获取滚动文字块的宽度。

2. 需要渲染的滚动元素个数

滚动元素的宽度 * 元素数量 >= 整个滚动区域 + 需要滚动的距离(滚动元素的宽度)

3. 开启动画

采用 TarocreateAnimation 方法,先要重置元素的x方向的位置为0,然后再设置它需要的滚动距离。

代码

TSX

import { View } from '@tarojs/components'
import Taro from '@tarojs/taro'
import './index.less'
import { forwardRef, useState, useEffect, useMemo, ReactElement, useImperativeHandle, useRef } from 'react';
import getEleInfo from "@/utils/getEleInfo";
import { randomStr } from '@/utils/random';

type PropsType = {
  title: string | ReactElement
  /** 滚动速度 */
  speed?: number
  /** 每段文字间的间距 */
  space?: number
  color?: string
  className?: string
}

export type ScrollBarInstance = {
  /** 重新获取dom并开启动画 */
  renderDom: () => void
  /** 取消滚动 */
  cancelScroll: () => void
}

type AnimationData = {
  actions: object[]
}

export default forwardRef<ScrollBarInstance, PropsType>((
  { title, speed = 50, space = 50, color = '#000', className }, ref
) => {
  const [barElemId] = useState(`b_${randomStr()}`)
  const [animElemId] = useState(`a_${randomStr()}`)
  const [itemElemId] = useState(`i_${randomStr()}`)
  // 需要渲染的滚动元素个数
  const [eleNum, setEleNum] = useState<number>(1)
  const timer = useRef<NodeJS.Timer>()

  const [animationData, setAnimationData] = useState<AnimationData>({
    actions: [{}]
  })

  /** 渲染dom并开启动画 */
  const renderDom = async () => {
    // 滚动区域的整体宽度
    const scrollW = (await getEleInfo(`#${barElemId}`))?.[0]?.width ?? 0
    // 注意:这里很重要 animationData 给谁就要等他加载完再给 animation 否则会无效
    const animElemW = (await getEleInfo(`#${animElemId}`))?.[0]?.width ?? 0
    const itemW = (await getEleInfo(`#${itemElemId}`))?.[0]?.width ?? 0
    // console.log('scrollW: ', scrollW, 'animElemW: ', animElemW, 'itemW: ', itemW);
    if(!itemW || !scrollW || !animElemW) {
      return
    }
    // 需要渲染的滚动元素个数(需要大于 滚动文字 + 外部滚动区域宽度)
    const n = Math.min(Math.ceil((scrollW + itemW) / itemW), 20)
    const duration = scrollW / speed * 1000
    const _animation = Taro.createAnimation({
      duration: duration,
      timingFunction: 'linear',
    })
    const resetAnimation = Taro.createAnimation({
      duration: 0,
      timingFunction: 'linear'
    })
   
    /** 重置并开启动画 */
    const animationRun = () => {
      resetAnimation.translateX(0).step()
      setAnimationData(resetAnimation.export())
      setTimeout(() => {
        _animation.translateX(-itemW).step()
        setAnimationData(_animation.export())
      }, 10);
    }
    animationRun()
    clearInterval(timer.current)
    timer.current = setInterval(animationRun, duration + 10)

    setEleNum(n)
  }

  /** 取消滚动 */
  const cancelScroll = () => {
    clearInterval(timer.current)
  }

  useImperativeHandle(ref, () => ({
    renderDom,
    cancelScroll,
  }))

  useEffect(() => {
    setTimeout(() => {
      renderDom()
    }, 100);
    return () => {
      clearInterval(timer.current)
    }
  }, [])

  // 滚动的元素
  const RenderItem = () => {
    return (
      <View id={itemElemId} className="com-scroll-bar-item" style={{paddingRight: space, color: color}}>
        {title}
      </View>
    )
  }

  // 渲染滚动元素的列表
  const renderList = useMemo(() => () => {
    const arr: ReactElement[] = []
    for(let i = 0; i < eleNum; i++) {
      arr.push(<RenderItem key={i} />)
    }
    return arr
  }, [eleNum, space, color])

  return (
    <View id={barElemId} className={`com-scroll-bar ${className ?? ''}`}>
      <View
        id={animElemId}
        className="com-scroll-bar-itemWrap"
        animation={animationData}
      >
        { renderList() }
      </View>
    </View>
  )
})

CSS

.com-scroll-bar {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  white-space: nowrap;
  display: flex;
  align-items: center;
  .com-scroll-bar-itemWrap {
    display: inline-block;
    font-size: 20px;
    .com-scroll-bar-item {
      display: inline-block;
    }
  }
}

导入的getEleInfo函数和randomStr函数

import { createSelectorQuery } from '@tarojs/taro'
// import { BoundingClientRectCallbackResult } from '@tarojs/taro/types/api/wxml';

export type BoundingClientRectCallbackResult = {
  /** 节点的下边界坐标 */
  bottom: number
  /** 节点的 dataset */
  dataset: TaroGeneral.IAnyObject
  /** 节点的高度 */
  height: number
  /** 节点的 ID */
  id: string
  /** 节点的左边界坐标 */
  left: number
  /** 节点的右边界坐标 */
  right: number
  /** 节点的上边界坐标 */
  top: number
  /** 节点的宽度 */
  width: number
}

/** 获取dom元素的信息 */
const getEleInfo = (selector: string): Promise<BoundingClientRectCallbackResult[] | null> => {
  return new Promise(resolve => {
    const query = createSelectorQuery()
    query.select(selector).boundingClientRect()
    query.exec(res => {
      // console.log('res: ', res);
      resolve(res)
    })
  })
}

export default getEleInfo

export const randomStr = () => Math.ceil(Math.random() * 10e5).toString(36)

这里是采用requestAnimationFrame改变state的写法

主要思路为: 需要渲染的滚动元素个数需要大于两个滚动区域的宽度。就拿上面的文字作为例子大概需要渲染4个元素才能达到该要求,通过两个state来控制这4个元素的移动,首先requestAnimationFrame函数会保证文字一直往 x 方向移动,然后每个元素从左边滚动到离开视野后,就将其 x 位置放到4个元素的最后面,然后如此循环,页面render次数太多了,小程序那看上去不太流畅。

代码

import { View } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { forwardRef, useState, useEffect, useMemo, ReactElement, useImperativeHandle } from 'react';
import './index.less'
import getEleInfo from "../utils/getEleInfo";

type PropsType = {
  title: string | ReactElement
  /** 滚动速度 */
  speed?: number
  /** 每段文字间的间距 */
  space?: number
  color?: string
}

export type ScrollBarInstance = {
  refreshEleInfo: () => void
  cancelScroll: () => void
}

export default forwardRef<ScrollBarInstance, PropsType>((
  { title, speed = 2, space = 50, color = '#000' }, ref
) => {
  // 滚动元素的宽度
  const [itemWidth, setItemWidth] = useState(0)
  // dom已经挂载完毕
  const [isReady, setIsReady] = useState(false)
  // 需要渲染的滚动元素个数
  const [eleNum, setEleNum] = useState<number>(1)
  // 跟随第一个元素x方向运动的变量
  const [timerX, setTimerX] = useState<number>(0)
  // 保持其它元素x方向运动的变量
  const [timerX2, setTimerX2] = useState<number>(0)
  // 当前跳转到后面的元素的索引
  const [curI, setCurI] = useState<number>(-1)

  const refreshEleInfo = async () => {
    // 滚动区域的整体宽度
    const barEleW = (await getEleInfo('.com-scroll-bar'))?.[0]?.width ?? 0
    const itemW = (await getEleInfo('.item'))?.[0]?.width ?? 0
    console.log('scrollW: ', barEleW, 'itemW: ', itemW);
    if(!itemW || !barEleW) {
      return
    }
    // 需要渲染的滚动元素个数(需要大于两个宽度)
    const n = Math.min(Math.ceil((barEleW * 2) / itemW), 20)
    setItemWidth(itemW)
    setEleNum(n)
    setIsReady(true)
  }

  useEffect(() => {
    setTimeout(() => {
      refreshEleInfo()
    }, 100);
  }, [])

  // 滚动的元素
  const RenderItem = ({ x }: { x: number }) => {
    return (
      <View className="com-scroll-bar-itemWrap" style={{
        transform: `translate(${x}px, -50%)`,
        color: color
      }}>
        <View className="com-scroll-bar-item">{title}</View>
      </View>
    )
  }

  // 渲染滚动元素的列表
  const renderList = useMemo(() => () => {
    const arr: ReactElement[] = []
    for(let i = 0; i < eleNum; i++) {
      const x = (i <= curI ? timerX : timerX2) + (itemWidth + space) * i
      arr.push(<RenderItem key={i} x={x} />)
    }
    return arr
  }, [eleNum, isReady, timerX, timerX2, curI])

  // 开启滚动
  useEffect(() => {
    let timer
    if(isReady) {
      (function loop() {
        setTimerX(v => v -= speed)
        setTimerX2(v => v -= speed)
        timer = requestAnimationFrame(loop)
      })()
    }
    return () => {
      cancelAnimationFrame(timer)
    }
  }, [isReady])
  
  const cancelScroll = () => {
    
  }

  useEffect(() => {
    const eleWidth = itemWidth + space
    // 第一个元素跳到后面
    if(timerX <= -eleWidth) {
      setTimerX(eleWidth * (eleNum - 1))
    }
    // 元素跳到后面
    if(Math.abs(timerX2) >= eleWidth * (curI + 2)) {
      setCurI(i => ++i)
    }
    // 一轮循环结束
    if(timerX2 <= -eleWidth * eleNum) {
      setTimerX2(0)
      setCurI(-1)
    }
  }, [timerX, timerX2])

  useEffect(() => {
    console.log('--title', title);
  }, [title])

  useImperativeHandle(ref, () => ({
    refreshEleInfo,
    cancelScroll
  }))

  return (
    <View className='com-scroll-bar'>
      <View className='scroll-bar-item-none'>
        <RenderItem x={0} />
      </View>
      { renderList() }
    </View>
  )
})

版权声明:本文为weixin_52014110原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_52014110/article/details/127010445