The core logic of the animation implementation scheme of circular scrolling after the table text exceeds

need

A few days ago, the company requested to make a dynamic table: probably configurable (color, font, number of rows, etc.), used for screen display. Therefore, it is required that when the table text exceeds the width, there can be an animation to scroll and display the content in a loop. It looks like this:
Please add a picture description
the project uses the vue framework, and the animation cannot call vue because it involves the preview (written in js), so it is implemented using the animate method of the jquery class library. The dynamic adaptation of table color, font size, etc. here is very simple, and two-way binding is enough, so I won’t go into details here.

train of thought

Implementation idea: find all span tags (content) to traverse, if the tag width exceeds the width of the parent element, add a scrolling effect to the span. ( There are not many table elements, so the performance problem is not obvious. If anyone has a better idea, welcome to give optimization suggestions. )
Animation implementation: through recursive calling, combined with a timer, the circular calling of two animations is realized.
Next, let's implement this effect in detail.

accomplish

Determine the idea, let's do it!

<template>
  <div style="width: 100%; height: 100%">
    <div :id="content.id" class="table-container">
      <table
        :id="`table_${content.id}`"
        :style="{
          'table-layout': 'fixed',
          fontSize: `30px`,
          'border-spacing': '0',
          'border-collapse': 'collapse'
        }"
      >
        <!-- 表头行 -->
        <tr
          :style="{
            width: '100%',
            height: `100px`,
            fontSize: `30px`
          }"
        >
          <!-- 表头行第一个【序号】可配置是否显示 thFlag支持配置 -->
          <th
            class="th0"
            :style="{
              display: thFlag ? 'table-cell' : 'none',
              width: '100px',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'text-align': 'center',
              'border-style': 'solid'
            }"
          >
            <div
              :style="{
                width: '100px',
                height: `50px`,
                lineHeight: `50px`,
                overflow: 'hidden'
              }"
            >
              <span>序号</span>
            </div>
          </th>
          <!-- 表头行数据 -->
          <th
            v-for="(ite, inde) in content.columns"
            :key="inde"
            :class="`th${inde + 1}`"
            :style="{
              width: `${ite.width}px`,
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'text-align': ite.textAlign,
              'border-style': 'solid',
            }"
          >
            <div
              :style="{
                width: `${ite.realw}px`,
                height: `30px`,
                lineHeight: `30px`,
                overflow: 'hidden',
                'text-align': ite.textAlign
              }"
            >
              <span>{
   
   { ite.headerName }}</span>
            </div>
          </th>
        </tr>
        <!-- 遍历实际数据 -->
        <!-- color那里可以配置基偶数行不同颜色 -->
        <tr
          v-for="(item, index) in content.rowStyle.rowCount"
          :key="index"
          :style="{
            width: '100%',
            height: `${content.rowStyle.realLineHeight}px`,
            color: index % 2 === 0 ? content.rowStyle.oddRowfontColor : content.rowStyle.evenRowfontColor,
            'background-color': index % 2 === 0 ? content.rowStyle.oddRowBgcolor : content.rowStyle.evenRowBgcolor
          }"
        >
          <!-- 第一列,序号:和上面序号的th对应 -->
          <td
            :style="{
              display: thFlag ? 'table-cell' : 'none',
              width: '100px',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'text-align': 'center',
              'border-style': 'solid',
            }"
          >
            <div
              :style="{
                width: '100px',
                height: `30px`,
                lineHeight: `30px`,
                overflow: 'hidden'
              }"
            >
              {
   
   { index + 1 }}
            </div>
          </td>
          <!-- 数据列,真实数据 -->
          <td
            v-for="(ite, inde) in content.columns"
            :key="inde"
            :style="{
              width: `${ite.realw}px`,
              height: '100%',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'border-style': 'solid'
            }"
          >
            <div
              :style="{
                width: `${ite.realw}px`,
                height: `30px`,
                lineHeight: `30px`,
                overflow: 'hidden',
                'text-align': ite.textAlign
              }"
            >
              <span v-show="ite.type !== 'img'" v-html="ite.keyValue[index]">{
   
   { ite.keyValue[index] }}</span>
              <span v-show="ite.type === 'img'" v-html="ite.keyValue[index]">{
   
   { ite.keyValue[index] }}</span>
            </div>
          </td>
        </tr>
      </table>
    </div>
  </div>
</template>

The html part is roughly as shown above: I deleted a lot of dynamic configuration data, or replaced it with actual data, just modify it by myself when using it.

Next, implement the js part of the logic.

<script>
	export default {
    
    
		data() {
    
    
			return {
    
    
      		  	// 判断循环定时器是否要继续(退出组件时候设为false,防止继续调用。)
		      	intervalFlag: false,
      		  	// 保存定时器序列,当销毁组件或者修改表格数据需要重置表格时候,要把所有定时器清除掉。--定时器是在Windows对象上的,不会跟随组建销毁而销毁。(这里还涉及到一个疑问,最后说……)
		        timerList: [],
			}
		},
		methods:{
    
    
		    initAnimate() {
    
    
		      // 清空动画和定时器
		      this.stopAll();
		      this.$nextTick(() => {
    
    
		        this.intervalFlag = true;
		        // $_collection: th和td的集合
		        const $_collection = $(`#${
      
      this.content.id} th, #${
      
      this.content.id} td`);
		        // 遍历dom节点序列
		        [].forEach.call($_collection, (ele) => {
    
    
		          const span = $(ele).find('span')[0];
		          const div = $(ele).find('div')[0];
		          // divWidth : th或td下的div的宽度
		          const divWidth = $(div).width();
		          // spanWidth : 内部span的宽度
		          const spanWidth = $(span).width();
		          const flag = divWidth < spanWidth;
		          if (flag) {
    
    
		            // 文字超出表格宽度加动画
		            this.calculateAnimate($(span), $(div), divWidth, spanWidth, 1200);
		          }
		        });
		      });
		    },
		    // 单个span赋动画  divWidth : th或td里面的div | spanWidth : 内部span的宽度 | loopTime: 两次循环间隔时间(ms)
		    calculateAnimate($_span, $_div, divWidth, spanWidth, loopTime) {
    
    
		      let animate_loop = null;
		      // newMargin:每次需要向左偏移的距离
		      let newMargin;
		      // 这里有一个坑:居中的文字想向左偏移到消失要margin-left为   : -(自身+父元素宽度),左对齐文字只需要偏移-自身宽度即可。
		      // 动画效果即: 从margin-left:0  ->  margin-left:-newMargin ,然后第二段: margin-left: divWidth -> margin-left: 0;  等待loopTime   在进行第二轮动画。
		      const isCenter = $_div.css('text-align');
		      if (isCenter) {
    
    
		      // 判断是否居中,来判断需要偏移的距离(其实还需要计算右对齐需要偏移的距离,在这里偷个懒,大家自己算吧!)
		        newMargin = -spanWidth - divWidth;
		      } else {
    
    
		        newMargin = -spanWidth;
		      }
		      // 移动时间 【60ms移动1px】-计算时间。
		      // 第二个坑,当居中时候,移动距离变远了,但是视觉上移动距离是一样的,所以duration动画完成时间还是只计算spanWidth的。   ---   duration是第一段动画需要的时间。
		      const duration = spanWidth * 1 * 30;
		      // 动画函数(递归调用)--- 循环滚动
		      animate_loop = () => {
    
    
		      	// 每次先置为0的位置。
		        $_span.css({
    
     marginLeft: '0px' });
		        // 一定要先调用 .stop() 如果你不想焦头烂额的找bug的话…………
		        $_span.stop().animate(
		          {
    
    
		            marginLeft: `${
      
      newMargin}px`
		          },
		          {
    
    
		            duration,
		            easing: 'linear',
		            complete: () => {
    
    
		            //complete: 动画完成时的执行函数: 此时执行第二段动画。
		              $_span.css({
    
     marginLeft: `${
      
      divWidth}px` });
		              // stop()不要忘记,不要让它存到动画序列中和你捣乱…… 
		              $_span.stop().animate(
		                {
    
    
		                  marginLeft: '0px'
		                },
		                {
    
    
		                  duration: divWidth * 30, // 还是时间计算。
		                  easing: 'linear',
		                  complete: () => {
    
    
		                  	// 第二次执行结束后,调用自身函数,定时器中递归调用。
		                    // intervalFlag:全局判断是否还要继续循环(当组件销毁时使用)
		                    if (this.intervalFlag) {
    
    
		                      // 将定时器放入数组,方便清空定时器  --  不这么做的话,定时器肆意捣乱,是第三个坑吧!
		                      this.timerList.push(
		                        setTimeout(() => {
    
    
		                        // 循环调用自身。
		                          animate_loop();
		                          // 清出第一个定时器标识,防止数组冗余 -存一个清一个。
		                          this.timerList.shift();
		                        }, loopTime)
		                      );
		                    }
		                  }
		                }
		              );
		            }
		          },
		          
		        );
		      };
		      $_span.css('margin-left', '0px');
		      animate_loop();
		    },
		    // 停止所有动画
		    stopAll() {
    
    
		      // 清空残留定时器
		      this.timerList.forEach((item) => {
    
    
		        clearTimeout(item);
		      });
		      this.timerList = [];
		      // 关闭所有定时器,初始化dom位置
		      const $_collection = $(`#${
      
      this.content.id} th, #${
      
      this.content.id} td`);
		      this.intervalFlag = false;
		      [].forEach.call($_collection, (ele) => {
    
    
		        const span = $(ele).find('span')[0];
		        $(span).stop();
		        $(span).css('margin-left', 0);
		      });
		    },
		},
	  	beforeDestroy() {
    
    
	  	  this.intervalFlag = false;
	 	   this.stopAll();
	 	},
	}
</script>

In this way, the text should move. At this point, the table text exceeds the animation and is completed.

The code is written more casually, and the boss can give suggestions for optimization and expand ideas. A small function with a simple function but a lot of pitfalls. There are still many usage scenarios.

question:
In fact, although it has been implemented, there is still a problem (it is solved but I don’t know why), and I really hope that some big guys can help solve the confusion :

In the timer loop, I didn't put the timer in the array at the beginning and clear it in time. Because the timer is considered to be automatically destroyed at the end of execution.
But there are many operations in the project that need to reset the animation. Then I found out that if the animation is reset after changing the table width or text width: (It is said that all the logic is re-executed, including the logic of calculating width and time.) The new animation should be executed normally.

However, after execution, it is often found that the animation effect is incorrect. After various debugging and printing, 执行的是上一次的数据。
I suspect that the real data of the dom has not been obtained, so I try to execute it in this.$nextTick(). It turned out that it was useless...
After many attempts (I used the api of animate once... but it didn't work), I turned my attention from the api of jquery's animate animation to this evil timer.
After several times of debugging, I found that every time the animation is reset, the data executed for the first time is correct 从第二次开始是错的. — The problem is clearly with the timer - it pollutes the data - the new data is overwritten by the data from that function call (closure) inside the timer. .

I stored the timer in the array again, and cleared the redundant timer in time every time I reset it. Sure enough, the problem was solved. !

But the problem is: I don't understand why the data execution of the remaining timer will overwrite the new data ...

If you have questions, answers, or need to add, you can leave a message or private me.

Waiting for thoughts...

Guess you like

Origin blog.csdn.net/hanyanshuo/article/details/127616421