水平無限循環彈幕的實現

前言

在項目實踐中應該有不少場景會用到彈幕,那麼如何實現一個完美版本的彈幕呢?接下來咱們原理加代碼帶你實現一個完整的彈幕組件(react版本)css

無限循環的水平彈幕實現原理

針對實現原理,這裏我畫了一張原理圖,你們能夠看一下:node

水平彈幕的實現有兩種狀況:react

一、當彈幕的個數加起來的寬度不足以覆蓋屏幕的可視化區域web

二、當彈幕的個數加起來的寬度超過屏幕的可視化區域bash

針對以上兩種狀況咱們有不一樣的展現效果,以下連接的展現效果:app

針對第一種狀況,實現原理很簡單,當從初始化位置開始滾動的時候,計算滾動的距離,當滾動結束後,立馬讓其回到初始化位置。flex

第二種狀況稍微複雜一些,咱們須要利用人眼的視覺暫留效果,實現彈幕的偷樑換柱,具體怎麼實現呢?動畫

  1. 初始化位置在屏幕最右側,也就是隱藏在屏幕外面,這一點和上一種狀況一致ui

  2. 咱們須要使用一個計算好的速度作一次動畫滑到屏幕的最左側,也就是後面循環往復的動畫的初始化位置,這個初始化位置和第一點的初始化位置不是同一個。 2.1. 計算這個速度很簡單,只須要知道咱們作徹底部動畫的時間以及彈幕的總長度,獲得的即是平均速度,以後再乘以屏幕的可視化區域寬度this

  3. 須要計算好咱們在原有彈幕個數的基礎上須要補充多少個彈幕才能超過屏幕可視化區域,作這個步驟是由於,只有補充這些彈幕,才能保證補充的彈幕的第一個滑到最左側的時候整個彈幕總體瞬間回到初始化位置的時候,不會讓用戶看出端倪,也就是沒有頓挫感。

  4. 定好keyframe的具體參數便可開始作動畫。

實現的代碼

實現的代碼是一個組件,這個組件有興趣的童鞋能夠將其豐富化,增長更多的參數,支持各類方向的循環滾動。

class InfiniteScroll extends React.Component {
  componentDidMount() {
    const { animationName, scrollDirection } = this.props
    setTimeout(() => {
      if (this.scrollInstance) {
        const appendElementWidth = []
        const appendElement = []
        const visibleWidth = this.scrollInstance.clientWidth
        // 滾動的初始位置從視口的最右邊開始,後面支持更多方向
        const initPosition = visibleWidth
        let scrollContainerWidth = 0
        let isCoverViewPort = false
        // 遍歷滾動的全部元素
        for (let i = 0; i < this.scrollInstance.children.length; i += 1) {
          const style = this.scrollInstance.children[i].currentStyle || window.getComputedStyle(this.scrollInstance.children[i])
          // width 已經包含了border和padding,若是box-sizing變化了呢?
          const width = this.scrollInstance.children[i].offsetWidth// or use style.width
          const margin = parseFloat(style.marginLeft) + parseFloat(style.marginRight)

          const clientWidth = (width + margin)
          scrollContainerWidth += clientWidth
          // 保存須要追加到原有滾動元素的列表後面
          if (scrollContainerWidth < visibleWidth * 2 && !isCoverViewPort) {
            appendElementWidth.push(clientWidth)
            appendElement.push(this.scrollInstance.children[i].cloneNode(true))

            if (scrollContainerWidth >= visibleWidth && !isCoverViewPort) {
              isCoverViewPort = true
            }
          }
        }
        // 該參數記錄是否彈幕的寬度超過了屏幕可視化區域的寬度
        const isScrollWidthLargeViewPort = scrollContainerWidth > visibleWidth

        // const styleSheet = document.styleSheets[0]

        // 注意這裏的動畫初始化位置兩種狀況是不同的,在以前的步驟上有說過的(垂直方向的沒實現,能夠忽略掉~)
        const keyframes = `
        @keyframes ${animationName}{
          from{
            transform: ${scrollDirection === 'horizon' ? `translateX(${isScrollWidthLargeViewPort ? 0 : initPosition}px)` : 'translateY(0px)'};
          }
          to {
            transform: ${scrollDirection === 'horizon' ? `translateX(-${scrollContainerWidth}px)` : `translateX(-${scrollContainerWidth}px)`};
          }
        }
        @-webkit-keyframes ${animationName}{
          from{
            -webkit-transform: ${scrollDirection === 'horizon' ? `translateX(${isScrollWidthLargeViewPort ? 0 : initPosition}px)` : 'translateY(0px)'};
          }
          to {
            -webkit-transform: ${scrollDirection === 'horizon' ? `translateX(-${scrollContainerWidth}px)` : `translateX(-${scrollContainerWidth}px)`};
          }
        }
        `
        const style = document.createElement('style')
        const head = document.head || document.getElementsByTagName('head')[0]

        style.type = 'text/css'
        const textNode = document.createTextNode(keyframes);
        style.appendChild(textNode);
        head.appendChild(style)
        // 若是css是external的話會報錯:Uncaught DOMException: Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules
        // styleSheet.insertRule(keyframes, styleSheet.cssRules.length);
        const previousWidth = scrollContainerWidth
        // 這個計算以後scrollContainerWidth就會包含那些補充了的彈幕的寬度,因此須要保留一個原始值,供後面的過渡動畫使用
        if (isScrollWidthLargeViewPort) {
          appendElement.map(node => this.scrollInstance.appendChild(node))
          appendElementWidth.map(it => scrollContainerWidth += it)
        }

        // TODO: 動畫的速度之後須要使用props
        // 由於初始化位置在視口外,可是咱們動畫的初始位置都是在0px上,因此就會有一個時差出現,
        // 由於在animation生效以前須要有一個過渡動畫,兩者的時間是相等的
        const delay = (this.scrollInstance.children.length * 3 * visibleWidth) / previousWidth
        const styleText = isScrollWidthLargeViewPort ? `
        width: ${scrollContainerWidth}px;
        transform: translateX(0px);
        -webkit-transform: translateX(0px);
        transition: ${delay}s linear;
        -webkit-transition: ${delay}s linear;
        animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear ${delay}s infinite;
        -webkit-animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear ${delay}s infinite;
      ` : `
      width: ${scrollContainerWidth}px;
      animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear infinite;
      -webkit-animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear infinite;
    `
        this.scrollInstance.style.cssText = styleText
      }
    }, 500)
  }
  render() {
    const { scrollContent, scrollItemClass, scrollClass, scrollDirection } = this.props

    const scrollClasses = scrollDirection === 'vertical' ? `scroll-list vertical ${scrollClass}` : `scroll-list horizon ${scrollClass}`

    return (
      <div className={scrollClasses} ref={ref => this.scrollInstance = ref}>
        {
          scrollContent.map((content, index) => (<div key={index} className={scrollItemClass}>{content}</div>))
        }
      </div>
    )
  }
}
複製代碼

對應的CSS文件以下:

.scroll-list{
  display: flex;
  &.horizon {
    flex-direction: row;
  }
  &.vertical{
    flex-direction: column;
  }
}
複製代碼

完整的應用參考:jsFiddle

至此完整版的水平循環彈幕實現完畢,有問題的歡迎留言~~

相關文章
相關標籤/搜索