前几天公司要求做一个动态表格:大概就是可配置的(颜色,字体,行数等等),用作屏幕展示。因此就要求表格文字在超出宽度时候可以有动画循环滚动展示内容。大概这个样子:
项目使用vue框架,动画因为涉及到预览(用js写的)不能调用vue,因此使用jquery类库的animate方法实现。这里表格颜色、字体大小等等动态适配很简单,双向绑定就可以,在此不做赘述。
实现思路:找到所有的span标签(内容)进行遍历,如果标签宽度超过父元素宽度,就给span添加滚动效果。(表格元素不会很多,因此性能问题不明显,如果有大佬有更好的思路,欢迎给出优化建议 )
动画实现:通过递归调用的方式,配合定时器,实现两段动画的循环调用。
接下来让我们来具体实现这个效果。
确定了思路,开搞!
<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`
}"
>
<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>
<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
}"
>
<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>
html部分大概如上所示:很多动态配置数据我删掉了,或者换成了实际数据,使用时候自行修改就好了。
<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>
就这样,文字应该就动起来了。至此表格文字超出动画实现完成。
代码写的比较随意,大佬可以给提优化建议,扩充下思路。功能简单但是坑不少的一个小功能。使用场景还是挺多的。
question:
其实虽然实现了,还是有遇到一个问题的(解决了但不知其所以然),万分希望有大佬可以帮忙解惑 :
在定时器循环那里,一开始我是没有把定时器放到数组并及时清空的。因为认为定时器在执行结束就自动销毁了。
但是项目中需要重置动画的操作很多。之后我就发现,如果改了表格宽度或文字宽度后重置动画:(按说所有逻辑都是重新执行的,包括计算宽度,时间的逻辑。)应该可以正常执行新的动画。
但是执行后发现经常出现动画效果不对,经过各种debug和打印发现执行的是上一次的数据。
我怀疑是没有拿到dom的真实数据,又尝试在this.$nextTick()中执行。结果发现并没什么卵用……
经过多次尝试(animate的api我用了一个遍……但没卵用),我把注意力从jquery的animate动画的api转向了这个万恶的定时器。
几次debug之后我发现,每次重置动画,第一次执行的数据都是对的,从第二次开始是错的
。 — 问题很明显的指向了定时器 - 它污染了数据 - 新数据被定时器里的那次函数调用(闭包)里的数据覆盖掉了。。
我又将定时器存到数组,在每次重置都及时清除冗余的定时器,果然,问题解决了。!
但是问题就是:我不理解为什么残留的那一次定时器的数据执行会覆盖掉新的数据……
有问题、解答或需要补充的地方可以留言或私我。
静等思路……