在需求中我需要实现一个倒计时的效果,这个倒计时效果类似滚轮的效果
分析效果:
- 实现一个类似滚轮的效果,容器向上滚动
- 可以实现多个滚轮的联动
无缝滚动
容器中列举了所有的可能的值,实现上述效果,就是匀速更改容器的偏移值 这种方案类似于无缝轮播的方案,下面来介绍一下这种方案 在倒计时中,元素值从0到9一共十个值,下面是具体的代码
<div className={styles.slider}>
<div className={styles.pager_tape} style={{
transform: `translateY(${percent * -10}%)`
}}>
{
Array.from({length: 10}).map((_, index) => (
<span className={styles.ele}>{index}</span>
))
}
</div>
</div>
<script>
const [percent, setPercent] = useState(0);
useInterval(() => {
setPercent(prev => (prev + 1) % 10);
}, 1000)
</script>
<style>
.slider {
height: 36px;
width: 20px;
overflow: hidden;
position: relative;
}
.pager_tape {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
transition: transform 500ms;
}
.ele {
font-size: 26px;
line-height: 36px;
}
</style>
在上面代码中,我们通过设置容器的偏移位置来控制显示的数字(其中useInterval的代码可参考这篇文章定时器在hooks的使用和分装)。
上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。
但是由于设定了固定的过度动画时间,就可以看到下面这种情况:
解决这个问题,可以参考无缝滚动的思路,在9后面复制一份0,当容器滚动到9的时候,继续滚动到复制的0,这个时候直接把容器的偏移位置设置为0%,并且控制容器的过度动画transition-time为0。
具体代码如下:
<div className={styles.slider}>
<div className={styles.pager_tape} onTransitionEnd={endPlay} style={{
transition: playing ? 'transform 500ms' : '',
transform: `translateY(${percent * -36}px)`
}}>
{
Array.from({length: 10}).map((_, index) => (
<span className={styles.ele}>{index}</span>
))
}
<span className={styles.ele}>0</span>
</div>
</div>
<script>
export function useInterval(callback, delay) {
const savedCallback = useRef(() => {});
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}
return undefined;
}, [delay]);
}
const [percent, setPercent] = useState(0);
const [playing, setPlaying] = useState(true);
useInterval(() => {
let num = (percent + 1) % 11;
let status = true;
setPercent(num);
setPlaying(status)
}, 1000)
const endPlay = () => {
if (percent+1 === 11) {
setPercent(0);
setPlaying(false);
}
};
</script>
上述代码中,复制了一份0到9的后面,此外过度动画时间从js来控制,而不是使用写固定的css。
- 声明变量
playing来控制是否有过渡动画 - 监听容器的
transitionEnd事件 - 判断当前是否滚动到
9后面的0,如果是,则调用endPlay方法 endPlay主要作用用于把偏移位置重置到0,并且设置为playing为false以上面这种方式,对于用户来说,就没有先前的跳变的过程,滚动效果非常平滑。
利用两个元素实现
事实上我们并不需要这么多子节点,仔细看动图的效果。
事实上在在视口只有两个元素,一个值为当前的值,另外一个元素的值为(current + 1) % 11的值。
下面是调整后的DOM结构:
<div className="slider">
{[prev, cur].map((item, index) => (
<span key={index} className={`slider-text ${playing && 'slider-ani'}`}>
{item}
</span>
))}
</div>
prev表示之前的值,cur表示当前的值。下面将其封装成一个组件
const { value } = props;
const [prev, setPrev] = useState('');
const [cur, setCur] = useState('');
const [playing, setPlaying] = useState(false);
const play = (prev, current) => {
setPrev(prev);
setCur(current);
setPlaying(false);
setTimeout(() => {
setPlaying(true);
}, 16);
};
useEffect(() => {
if (isEffective(value)) {
play(cur, value);
} else {
setPrev(value);
setCur(value);
}
}, [value]);
监听useEffect监听value值的变化,只有当传入值有效的时候才会播放动画,下面是具体的css代码:
.slider {
display: flex;
flex-direction: column;
overflow: hidden;
height: 36px;
}
.slider-text {
font-size: 26px;
line-height: 36px;
height: 100%;
transform: translateY(0%);
}
.slider-ani {
transform: translateY(-100%);
transition: transform 500ms ease;
}
描述一下这种方案的思路:
- 声明两个变量,存放当前的值,prev: 之前的值;cur: 表示变化后的值
- 监听传入值的变化
- 首次传值:设置prev和cur为当前传入的值
- 第二次更改值:调用play方法
- 把prev的值设置为没有变化前的cur值,cur值更改为传入的值
- 设置playing为false,然后16ms后设置为true 这里有个问题就是为什么需要在16ms后设置playing为true,拆解一下这个过程你就明白了
- 上次移动后,playing为true,此时容器的偏移位置为
translateY(-100%) - 当新值传入时,我们需要更改元素的值,并且把容器位置重新置为
translateY(0),需要注意的是设置这个过程的时候是不能有动画效果的 - 16ms,这个表示屏幕的刷新每一帧画面需要的时间
如果要实现文章开头的效果,只需要多个组件挨着就可以实现了。
上面就是所有关于滚动效果的内容,欢迎关注我的公众号: 前端好好学