1. 需求分析
用于循环播放展示一组消息通知; 通知消息渲染完成,获取消息的长度和盒子的长度; 使用【taro react】---- 获取元素的位置和宽高等信息异步获取内容和盒子的宽高信息; 通过 CSS3 的 animation 实现内容的移动; 注意:第一次移动和第二次移动的长度不相同,因此需要监听第一次动画完成 onAnimationEnd。
import { createSelectorQuery } from '@tarojs/taro';
function isWindow(val){
return val === window
}
export const getRect = (elementRef) => {
const element = elementRef
// 判断传入元素是否是window窗口,是window窗口,直接获取窗口的宽高
if (isWindow(element)) {
const width = element.innerWidth
const height = element.innerHeight
return {
top: 0,
left: 0,
right: width,
bottom: height,
width,
height,
}
}
// 是元素,同时可以获取元素的宽高等信息
if (element && element.getBoundingClientRect) {
return element.getBoundingClientRect()
}
// 都不满足,返回默认值
return {
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 0,
height: 0,
}
}
export const getRectByTaro = async (element) => {
// 元素存在,判断使用对应环境获取元素信息
if (element) {
if(process.env.TARO_ENV === "h5"){
// H5环境使用元素的获取元素信息方法
return Promise.resolve(getRect(element))
} else if(process.env.TARO_ENV === "weapp"){
// 微信小程序环境调用 boundingClientRect 获取元素信息
return new Promise((resolve) => {
createSelectorQuery()
.select(`.${element.props.class.split(' ').filter(item => item).join('.')}`)
.boundingClientRect(resolve).exec()
})
}
}
// 返回默认值
return Promise.resolve({
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 0,
height: 0,
})
}
3. 监听内容加载获取盒子和内容元素信息
初始化加载如果又内容,就初始化滚动参数; 使用 setTimeout,实现先渲染,后获取渲染后的元素信息; 判断盒子和内容是否都渲染完成,没有渲染就直接返回; 通过 getRectByTaro 获取盒子和内容的宽度; 判断 scrollable 是否允许进行消息滚动; 判断内容和盒子的宽度,如果内容小于盒子,则不进行消息滚动; 如果可以消息滚动,则记录盒子和内容的宽度,计算动画滚动的时间; 设置第一次滚动的className; 不能滚动,就清除滚动动画的className。
// 初始化加载如果又内容,就初始化滚动参数
useEffect(() => {
initScrollWrap(content)
},[content])
// 初始化设置函数
const initScrollWrap = (value) => {
let timer = setTimeout(async () => {
clearTimeout(timer)
if(!wrapRef.current || !contentRef.current){
return
}
const wrapObj = await getRectByTaro(wrapRef.current)
const wrapW = wrapObj.width
const offsetObj = await getRectByTaro(contentRef.current)
const offsetW = offsetObj.width
const canScroll = scrollable == null ? offsetW > wrapW : scrollable
if (canScroll) {
setWrapWidth(wrapW)
setOffsetWidth(offsetW)
setAnimationDuration(offsetW / speed)
setAnimationClass('rui-play')
} else {
setAnimationClass('')
}
},0)
}
4. 监听第一次结束设置后续动画
第一次动画结束,将判断第一次动画的变量设置为 false; 异步设置计算动画的时间; 设置无限循环移动的动画className。
// 第一次运动结束后设置新的时间和动画
const onAnimationEnd = () => {
setFirstRound(false)
let timer = setTimeout(() => {
setAnimationDuration((offsetWidth + wrapWidth) / speed)
setAnimationClass('rui-play-infinite')
clearTimeout(timer)
}, 0)
}
5. CSS3 移动动画
.rui-play-infinite{
animation: rui-notice-bar-play-infinite linear infinite both running;
}
.rui-play{
animation: rui-notice-bar-play linear both running;
}
@keyframes rui-notice-bar-play {
to {
transform: translate3d(-100%,0,0)
}
}
@keyframes rui-notice-bar-play-infinite {
to {
transform: translate3d(-100%,0,0)
}
}
6. 完整 JSX 代码
import { View } from '@tarojs/components';
import { useAsyncState } from '@utils/event';
import { useEffect, useRef } from 'react';
import { getRectByTaro } from '@utils/use-client-rect';
import './index.scss'
const RuiNoticebar = (props) => {
let {
children,
content,
scrollable = null,
speed = 50,
delay = 1
} = props;
// 获取盒子和内容的宽度,设置移动的时间,是否是第一次移动,以及移动的class
let wrapRef = useRef(null)
let contentRef = useRef(null)
let [wrapWidth, setWrapWidth] = useAsyncState(0)
let [offsetWidth, setOffsetWidth] = useAsyncState(0)
let [animationDuration, setAnimationDuration] = useAsyncState(0)
let [firstRound, setFirstRound] = useAsyncState(true)
let [animationClass, setAnimationClass] = useAsyncState('')
// 初始化加载如果又内容,就初始化滚动参数
useEffect(() => {
initScrollWrap(content)
},[content])
// 初始化设置函数
const initScrollWrap = (value) => {
let timer = setTimeout(async () => {
clearTimeout(timer)
if(!wrapRef.current || !contentRef.current){
return
}
const wrapObj = await getRectByTaro(wrapRef.current)
const wrapW = wrapObj.width
const offsetObj = await getRectByTaro(contentRef.current)
const offsetW = offsetObj.width
const canScroll = scrollable == null ? offsetW > wrapW : scrollable
if (canScroll) {
setWrapWidth(wrapW)
setOffsetWidth(offsetW)
setAnimationDuration(offsetW / speed)
setAnimationClass('rui-play')
} else {
setAnimationClass('')
}
},0)
}
// 第一次运动结束后设置新的时间和动画
const onAnimationEnd = () => {
setFirstRound(false)
let timer = setTimeout(() => {
setAnimationDuration((offsetWidth + wrapWidth) / speed)
setAnimationClass('rui-play-infinite')
clearTimeout(timer)
}, 0)
}
// 设置内容的动画参数
const contentStyle = {
animationDelay: `${firstRound ? delay : 0}s`,
animationDuration: `${animationDuration}s`,
transform: `translateX(${firstRound ? 0 : `${wrapWidth}px`})`,
}
return <View className='rui-noticebar-temp-content rui-flex-ac'>
<View
className='rui-flex-ac rui-noticebar-wrap-content'
ref={wrapRef}>
<View
className={'rui-fg rui-noticebar-content ' + animationClass}
style={contentStyle}
onAnimationEnd={onAnimationEnd}
ref={contentRef}>
{ children }
{ content }
</View>
</View>
</View>
}
export default RuiNoticebar;
7. 完整 SCSS 代码
.rui-noticebar-temp-content{
height: 80px;
.rui-noticebar-wrap-content{
flex: 1;
height: 80px;
line-height: 80px;
overflow: hidden;
position: relative;
}
.rui-noticebar-content{
position: absolute;
white-space: nowrap;
}
.rui-ellipsis {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.rui-play-infinite{
animation: rui-notice-bar-play-infinite linear infinite both running;
}
.rui-play{
animation: rui-notice-bar-play linear both running;
}
@keyframes rui-notice-bar-play {
to {
transform: translate3d(-100%,0,0)
}
}
@keyframes rui-notice-bar-play-infinite {
to {
transform: translate3d(-100%,0,0)
}
}
}
8. 效果
9. 使用实例
import { View, Text, Image } from '@tarojs/components';
import { RuiCustomWhite } from '@com/RuiCustom';
import RuiNoticebar from '@com/RuiNoticebar';
import Img from '@utils/icon/icon';
const NoticebarPage = (props) => {
return <View className='rui-luck-page-content'>
<RuiCustomWhite isBorder title='NoticeBar'/>
<View className='rui-tac rui-mt20 rui-mb20 rui-fs30'>基础使用</View>
<View className='rui-ml30 rui-mr30 rui-mt15 rui-mb15'>
<RuiNoticebar content="NutUI 是京东风格的移动端组件库,使用 Vue 语言来编写可以在 H5,小程序平台上的应用,帮助研发人员提升开发效率,改善开发体验。"/>
</View>
<View className='rui-tac rui-mt20 rui-mb20 rui-fs30'>添加图标</View>
<View className='rui-ml30 rui-mr30 rui-mt15 rui-mb15 rui-flex-ac'>
<Image src={Img.iconAlarmIcon} className='rui-fa rui-icon-36'></Image>
<View className='rui-fg rui-ml15'>
<RuiNoticebar content={
<View className='rui-fs26 rui-colorl6'>NutUI 是京东风格的移动端组件库,使用 Vue 语言来编写可以在 H5,小程序平台上的应用,帮助研发人员提升开发效率,改善开发体验。</View>
}/>
</View>
</View>
<View className='rui-tac rui-mt20 rui-mb20 rui-fs30'>自定义速度</View>
<View className='rui-ml30 rui-mr30 rui-mt15 rui-mb15 rui-flex-ac'>
<Image src={Img.iconAlarmIcon} className='rui-fa rui-icon-36'></Image>
<View className='rui-fg rui-ml15'>
<RuiNoticebar speed={100} content={
<View className='rui-fs26 rui-colorl6'>NutUI 是京东风格的移动端组件库,使用 Vue 语言来编写可以在 H5,小程序平台上的应用,帮助研发人员提升开发效率,改善开发体验。</View>
}/>
</View>
</View>
<View className='rui-tac rui-mt20 rui-mb20 rui-fs30'>停止滚动</View>
<View className='rui-ml30 rui-mr30 rui-mt15 rui-mb15 rui-flex-ac'>
<Image src={Img.iconAlarmIcon} className='rui-fa rui-icon-36'></Image>
<View className='rui-fg rui-ml15'>
<RuiNoticebar
scrollable={false}
content={
<View className='rui-fs26 rui-colorl6'>NutUI 是京东风格的移动端组件库,使用 Vue 语言来编写可以在 H5,小程序平台上的应用,帮助研发人员提升开发效率,改善开发体验。</View>
}/>
</View>
</View>
<View className='rui-tac rui-mt20 rui-mb20 rui-fs30'>children</View>
<View className='rui-ml30 rui-mr30 rui-mt15 rui-mb15 rui-flex-ac'>
<View className='rui-fg'>
<RuiNoticebar scrollable={false}>
<View className='rui-fs26 rui-colorl6 rui-flex-ac'>
<Image src={Img.iconAlarmIcon} className='rui-fa rui-icon-36'></Image>
<Text className='rui-ml15'>NutUI 是京东风格的移动端组件库。</Text>
</View>
</RuiNoticebar>
</View>
</View>
</View>
}
export default NoticebarPage;