Y轴方向跑马灯的实现

899 阅读2分钟

跑马灯是比较常见的页面展现元素。写这篇博客的目的主要是掘金上面关于前端跑马灯的教程基本没有,仅有的一个也是x轴方向的,今天写一个y轴方向的。


Demo地址: github.com/ZSX-DB/Reus…

首先我们需要定义 html 结构和 css,我将 box 的高度和 item 的高度都设置了 40px,这些都是可更改的。

.box{
    overflow: hidden;
    margin-top: 200px;
    width: 500px;
    height: 40px;
    border: 1px solid #000;
    box-sizing: border-box;
    color: #fff;
    background-color: #000;
}

.item{
    display: flex;
    align-items: center;
    padding-left: 10px;
    color: #fff;
    cursor: pointer;
 }



<div class="box">
    <div class="item">y轴跑马灯实现</div>
    <div class="item">前端实现</div>
    <div class="item">可能使用margin-top</div>
    <div class="item">短暂停留</div>
</div>

CSS实现

使用 @keyframes 可以定位帧动画, 通过设置 infinite 来实现不间断轮播的效果, 设置item滚动, 当item为最后一个时, 重置。

以下是定义的@keyframes

.box{
    ...
    animation: run 8.4s infinite;
}

@keyframes run {
    0%{
        transform: translateY(0px);
    }
    25%{
        transform: translateY(-40px);
    }
    50%{
        transform: translateY(-80px);
    }
    75%{
        transform: translateY(-120px);
    }
    100%{
         transform: translateY(-160px);
    }
}

为了保证滚动的顺滑, 还需要在尾部加一个<div class="item">y轴跑马灯实现</div>在尾部。

JavaScript实现

上面的动画高度依赖于item的条数, 如果item有很多条, 并且实际开发中需求大概是滚动到哪一条, 停顿, 过会后再滚动。这些如果用CSS写将会特别麻烦甚至无法实现。因此我们用js来实现。

有以下的思路

  1. 设置一个定时器setInterval, 定义步进距离, 步进时间, 来模拟帧动画
  2. 当一个item完全滚动到可视区, 清除计时器, 再添加一个setTimeout, 停顿时间结束后再重启计时器
  3. 当滚动到最后的item时, 重置高度

使用margin-top来动态改变item的位置, 只需要设置第一个item的规则, 后面的item会自动排布, 为了知道当第一个item的margin-top为多少时重置, 还需要给定item的数量

    function runYMarquee(firstNode, nodeNum, step = 1, time = 20) {
        let ch = firstNode.clientHeight

        const start = marginTop => {
            let mt = marginTop
            let interval = setInterval(()=>{
                // 判断不是第一项和最后一项
                if(Number.isInteger(mt/ch) && mt !== marginTop && mt !== - (ch * nodeNum) ){
                    clearInterval(interval)
                    setTimeout(()=>{
                        start(mt)
                    }, 1000)
                }else if(mt === - (ch * nodeNum)){
                    // 到达最后一项,归零
                    clearInterval(interval)
                    // 恢复原来的位置
                    firstNode.style.marginTop = '0'
                    setTimeout(()=>{
                        start(0)
                    },1000)
                }else {
                    // 如果是start的第一个执行此项
                    mt -= step
                    firstNode.style.marginTop = `${mt}px`
                }
            }, time)
        }

        // 这里设置setTimeout是为了首条item的延迟滚动
        setTimeout(()=>{
            start(0)
        }, 1000)

    }
    
    let i = document.querySelector('.item')
    runYMarquee(i, 4)

注: 以上仍然需要在最后添加<div class="item">y轴跑马灯实现</div>来实现顺滑效果

优化

如果使用margin-top来改变位置, 将会频繁触发浏览器的重排重绘, 而且需要手动在末尾添加item, 并且无法设置停顿时间

基于此, 我写了一个优化版本, 优化了下列几项

  • 使用transfrom中的translateY属性替代margin-top, 可以生成一个合成层, 由GPU控制,支持硬件加速,并不需要软件方面的渲染
  • clone第一个节点, 自动追加于最后, 不需要手动添加
  • 支持自定义设置停顿时间

translateY的对高度的控制是独立的, 因此必须在item外部再套一层div, 通过控制该层div来控制高度。

<div class="box">
    <div id="marquee-box">
        <div class="item" onclick="log(1)">y轴跑马灯实现</div>
        <div class="item" onclick="log(2)">前端实现</div>
        <div class="item" onclick="log(3)">可能使用margin-top</div>
        <div class="item" onclick="log(4)">短暂停留</div>
    </div>
</div>

实现的思路与上面一样, 代码如下

    function runMarquee(node, step = 1, time = 20, stopTime = 1000) {
        // 最后追加子节点
        let firstNode = node.children[0]
        let newNode = firstNode.cloneNode(true)
        node.append(newNode)
        // 获取children的数量
        let childNodeNum = node.children.length
        // 获取此时的node高度
        let ch = node.clientHeight
        // 子节点的高度
        let cnh = ch / childNodeNum
        // 最后停止的高度
        let lastHeight = ch * ((childNodeNum - 1) / childNodeNum)
        const start = initTY => {
            // 初始化translateY
            let ty = initTY
            let interval = setInterval(()=>{
                // 判断不是第一项或最后一项
                if(Number.isInteger(ty / cnh) && ty !== initTY && ty !== -lastHeight ){
                    clearInterval(interval)
                    setTimeout(()=>{
                        start(ty)
                    }, stopTime)
                }else if(ty === -lastHeight){
                    clearInterval(interval)
                    node.style.transform = `translateY(0)`
                    setTimeout(()=>{
                        start(0)
                    },stopTime)
                }else {
                    ty -= step
                    node.style.transform = `translateY(${ty}px)`
                }
            }, time)
        }

        setTimeout(()=>{
            start(0)
        }, stopTime)

    }
    
    runMarquee(document.querySelector('#marquee-box'))

使用需满足两个条件

  • 外部box的高度必须与item相同
  • step必须能被item的高度整除

如果这篇文章对你有用, 请给我点个赞吧, 有疑问欢迎在下方评论区交流。