使用技术栈
- react hooks ts
- taro 小程序
整体思路
当需要滚动的文字从右侧滚到左侧并完全消失,此时下一个文字到达第一个文字的初始位置时,将整个动画滚动区域重置,相当于回到初始状态,紧接着继续重新动画。主要借鉴了taro-ui
的noticebar
动画思路。
文末放上一开始没用animation的写法的版本(但是小程序会有卡顿,所以就去参考taro-ui了)
开发流程
1. 获取宽度
首先要获取滚动区域的整体宽度,然后再获取animation属性给的元素的宽度,最后再获取滚动文字块的宽度。
2. 需要渲染的滚动元素个数
滚动元素的宽度 * 元素数量 >= 整个滚动区域 + 需要滚动的距离(滚动元素的宽度)
3. 开启动画
采用 Taro
的 createAnimation
方法,先要重置元素的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 版权协议,转载请附上原文出处链接和本声明。