轮播组件,不管是轮播 banner 图、轮播中奖名单还是轮播气泡,都可以抽象为一个数组进栈出栈的过程:
const array = [0, 1, 2, 3, 4]
const out = array.shift()
array.push(out)
轮播气泡尤为典型,一端的气泡 fadeout,同时另一端的气泡 fadeIn,其它气泡同步顺移,这个过程以固定时间间隔无限循环。
那么如何将上述数据结构映射为视图呢?我想到了 vue 内置的过渡动画组件:transition-group
。官方文档 语焉不详的描述虽然初看不太明白,不过照猫画虎,还是可以很自然的想到下列实现方案:
<template>
<div id="app">
<transition-group name="bubble" tag="ul">
<li v-for="(val, i) in bubbles" :key="i">{{ val.text }}</li>
</transition-group>
</div>
</template>
<script>
export default {
data () {
return {
bubbles: [{ text: 'a' }, { text: 'b' }, { text: 'c' }, { text: 'd' }]
}
},
mounted () {
setInterval(() => {
const out = this.bubbles.shift()
this.bubbles.push(out)
}, 2000)
}
}
</script>
<style lang="scss">
li {
transition: all 1s;
display: block;
}
.bubble-enter {
opacity: 0;
transform: translateY(30px);
}
.bubble-leave-to {
opacity: 0;
transform: translateY(-30px);
}
/* 这个看似无用,但必须加上 */
.bubble-leave-active {
position: absolute;
}
</style>
由此我们踩到了第一个坑:确实轮播滚动起来了,但是没有渐变过渡的动画效果,单独使用 shift 或 push 都有,但是一起用就没了。中间的试探过程暂且不表,直接说结论:循环项的 key 必须和数组内容一样,也即 v-for="val in bubbles" :key="val"
。
stackoverflow 上也有人提到了这一点:
试探过程中我还发现了一个有意思的现象:当气泡发生位移时,在 devtools 中抓不到任何样式属性的变化,多出的一个 style 属性里什么都没有,新增的 class 名 bubble-move
并没有挂载任何样式。那么气泡到底是怎么动起来的呢?
文档说了,transition-group
是基于一个叫 flip 的动画队列,其实只要耐心阅读一小段 flip 的示例代码,就能解释上述现象,同时也容易理解官方文档里那个炫酷的棋盘 shuffle 动画的实现原理。
// Get the first position.
var first = el.getBoundingClientRect();
// Now set the element to the last position.
el.classList.add('totes-at-the-end');
// Read again. This forces a sync
// layout, so be careful.
var last = el.getBoundingClientRect();
// You can do this for other computed styles as well, if needed.
// Just be sure to stick to compositor-only
// props like transform and opacity.
var invert = first.top - last.top;
// Invert.
el.style.transform = `translateY(${invert}px)`;
// Wait for the next frame so we
// know all the style changes have taken hold.
requestAnimationFrame(function() {
// Switch on animations.
el.classList.add('animate-on-transforms');
// GO GO GOOOOOO!
el.style.transform = '';
});
// Capture the end with transitionend
el.addEventListener('transitionend',
tidyUpAnimations);
回到气泡组件上,现在的问题是数组内都是对象,对象不能作为 key,于是考虑新增一个专门用于动画的数组,key 和数组的内容一致:
<template>
<div id="app">
<transition-group name="bubble" tag="ul">
<li v-for="index in list" :key="index">{{ bubbles[index].text }}</li>
</transition-group>
</div>
</template>
<script>
export default {
data () {
return {
bubbles: [{ text: 'a' }, { text: 'b' }, { text: 'c' }, { text: 'd' }],
list: [0, 1, 2, 3]
}
},
mounted () {
setInterval(() => {
const out = this.list.shift()
this.list.push(out)
}, 2000)
}
}
</script>
不过效果仍然不理想:
可以看到 bubble-enter
和 bubble-leave-to
并没有添加到 class 中。
为了解决问题,中间走了一个大弯路。首先我用 setTimeout(() => { this.list.push(out) }, 0)
,得到了近乎最终的效果:
稍加了一些处理,终于实现了理想的效果,但是结果并不牢靠:iOS 上一切安好,在安卓上(app webview),一开始动画都是好的,但只要页面滑动两下,动画效果就混乱了,而且是必现的。手机浏览器里就没事,pc 上也没事。
滑动两下动画就乱了?偏偏是安卓 webview?跟定时器在移动端滚动过程中会中止有关?多次试验无法确证是否与此有关,换用 requestAnimationFrame
也不能解决问题。那这是安卓本身的 bug 吗?还是 transition-group
的 bug?不过就算是它们的 bug,问题也必须解决不是。我甚至一度想推翻重来,但拥挤的排期意味着我只能周末加班了,不甘呐。
码感敏锐的人可能发现了,我之前的用法看上去总有点不靠谱。文档既没说什么能做,也没说什么不能做,全靠自己试,于是生造了一些奇淫巧技骚操作。以前也遇到过两三个现象十分诡异的问题,但最终发现都是隐匿在某处的小细节造成的。我相信计算机不会错,错的一定是人。
于是我又去看文档,官方示例看上去是可以满足轮播场景的,同时执行 remove 和 add,数字移出和进入的动画可以同时进行,那么我的用法跟官方的区别到底在哪里呢?
其实就差在这里:
官方示例每次添加的都是新元素 nextNum++
,我每次加的都是老元素 list.shift()
,这可能会影响到 flip 动画中 first 和 last 状态的计算。因此按照示例的做法一改就好了:
<template>
<div id="app">
<transition-group name="bubble" tag="ul">
<li v-for="(index, i) in list" :key="index">{{ bubbles[i].text }}</li>
</transition-group>
</div>
</template>
<script>
export default {
data () {
return {
bubbles: [{ text: 'a' }, { text: 'b' }, { text: 'c' }, { text: 'd' }],
list: [0, 1, 2, 3],
next: 10
}
},
mounted () {
setInterval(() => {
this.list.shift()
this.list.push(this.next++)
}, 2000)
}
}
</script>
不过问题还是很明显:内容并没有跟着气泡循环,之前我们用 bubbles[index]
中的数据,后来我们改用 next++
表示新增数据,因此不能再用 bubbles[index]
。解决方法也很容易想到:bubbles
跟随 list
同步循环不就好了吗?
this.list.shift()
this.list.push(this.next++)
const out = this.bubbles.shift()
this.bubbles.push(out)
嗯,看上去是期待中的样子。
如果只希望展示 3 个气泡,只需要把 list 改为 [0, 1, 2] 就好了。
最后再提示一点:气泡作为容器,最好不要直接在容器元素上设置样式,比如气泡的样式是运营配置的,那么最好将 style 设置在容器的子元素上,避免干扰容器样式的计算(参见 flip 动画一节)。
<li style={/* 样式配置 */}></li> // not good
<li>
<div style={/* 样式配置 */}></div> // better
</li>
小结
1、最好完全按照官方示例使用 transition-group
;
2、容器与内容解耦。容器只负责定位布局,内容只负责样式。
共同学习,写下你的评论
评论加载中...
作者其他优质文章