Vue 学习笔记(十五):状态过渡

状态过渡
State Transitions

Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。而对于数据元素本身,比如:

  • 数字和运算
  • 颜色的显示
  • SVG 节点的位置 (SVG指可缩放矢量图形,使用 XML 格式定义图像)
  • 元素的大小和其他的 property

这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。

状态动画与侦听器

Animating State with Watchers

侦听器可以监听到任何数值 property 的数值更新。
使用 GreenSock 一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>

<div id="animated-number-demo" class="demo">
<input v-model.number="number" type="number" step="20">
<p>{{ animatedNumber }}</p>
</div>

<script>
new Vue({
el: '#animated-number-demo',
data: {
number: 0,
tweenedNumber: 0
},
computed: {
animatedNumber: function () {
return this.tweenedNumber.toFixed(0);
}
},
watch: {
number: function (newValue) {
gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
}
}
})
</script>

渲染结果:

{{ animatedNumber }}

数值更新时,就会触发动画。

而对于不能直接像数字一样存储的值,比如 CSS 中的 color 的值,示例,Tween.jsColor.js 结合使用的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3"></script>

<div id="example-7" class="demo">
<input v-model="colorQuery" v-on:keyup.enter="updateColor" placeholder="Enter a color">
<button v-on:click="updateColor">Update</button>
<p>Preview:</p>
<span v-bind:style="{ backgroundColor: tweenedCSSColor }" class="example-7-color-preview"></span>
<p>{{ tweenedCSSColor }}</p>
</div>

<script>
// 当使用color.js这个库时,可以从公共命名空间net.brehaut中获取Color
var Color = net.brehaut.Color
new Vue({
el: '#example-7',
data: {
colorQuery: '',
color: {
red: 0,
green: 0,
blue: 0,
alpha: 1
},
tweenedColor: {}
},
created: function () {
// Object.assign()是浅拷贝
this.tweenedColor = Object.assign({}, this.color)
},
watch: {
color: function () {
function animate() {
if (TWEEN.update()) {
requestAnimationFrame(animate)
}
}

new TWEEN.Tween(this.tweenedColor)
.to(this.color, 750)
.start()

animate()
}
},
computed: {
tweenedCSSColor: function () {
return new Color({
red: this.tweenedColor.red,
green: this.tweenedColor.green,
blue: this.tweenedColor.blue,
alpha: this.tweenedColor.alpha
}).toCSS()
}
},
methods: {
updateColor: function () {
this.color = new Color(this.colorQuery).toRGB()
this.colorQuery = ''
}
}
})
</script>

<style>
.example-7-color-preview {
display: inline-block;
width: 50px;
height: 50px;
}
</style>

渲染结果

Preview:

{{ tweenedCSSColor }}

动态状态过渡

Dynamic State Transitions

像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。
当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.5/TweenLite.min.js"></script>
<div id="svg-polygon-demo" class="demo">
<svg width="200" height="200" class="demo-svg">
<polygon :points="points" class="demo-polygon"></polygon>
<circle cx="100" cy="100" r="90" class="demo-circle"></circle>
</svg>
<label>Sides: {{ sides }}</label>
<input
class="demo-range-input"
type="range"
min="3"
max="500"
v-model.number="sides"
>
<label>Minimum Radius: {{ minRadius }}%</label>
<input
class="demo-range-input"
type="range"
min="0"
max="90"
v-model.number="minRadius"
>
<label>Update Interval: {{ updateInterval }} milliseconds</label>
<input
class="demo-range-input"
type="range"
min="10"
max="2000"
v-model.number="updateInterval"
>
</div>
<script>
new Vue({
el: '#svg-polygon-demo',
data: function () {
var defaultSides = 10
var stats = Array.apply(null, { length: defaultSides })
.map(function () { return 100 })
return {
stats: stats,
points: generatePoints(stats),
sides: defaultSides,
minRadius: 50,
interval: null,
updateInterval: 500
}
},
watch: {
sides: function (newSides, oldSides) {
var sidesDifference = newSides - oldSides
if (sidesDifference > 0) {
for (var i = 1; i <= sidesDifference; i++) {
this.stats.push(this.newRandomValue())
}
} else {
var absoluteSidesDifference = Math.abs(sidesDifference)
for (var i = 1; i <= absoluteSidesDifference; i++) {
this.stats.shift()
}
}
},
stats: function (newStats) {
TweenLite.to(
this.$data,
this.updateInterval / 1000,
{ points: generatePoints(newStats) }
)
},
updateInterval: function () {
this.resetInterval()
}
},
mounted: function () {
this.resetInterval()
},
methods: {
randomizeStats: function () {
var vm = this
this.stats = this.stats.map(function () {
return vm.newRandomValue()
})
},
newRandomValue: function () {
return Math.ceil(this.minRadius + Math.random() * (100 - this.minRadius))
},
resetInterval: function () {
var vm = this
clearInterval(this.interval)
this.randomizeStats()
this.interval = setInterval(function () {
vm.randomizeStats()
}, this.updateInterval)
}
}
})

function valueToPoint (value, index, total) {
var x = 0
var y = -value * 0.9
var angle = Math.PI * 2 / total * index
var cos = Math.cos(angle)
var sin = Math.sin(angle)
var tx = x * cos - y * sin + 100
var ty = x * sin + y * cos + 100
return { x: tx, y: ty }
}

function generatePoints (stats) {
var total = stats.length
return stats.map(function (stat, index) {
var point = valueToPoint(stat, index, total)
return point.x + ',' + point.y
}).join(' ')
}
</script>
<style>
.demo-svg { display: block; }
.demo-polygon { fill: #41B883; }
.demo-circle {
fill: transparent;
stroke: #35495E;
}
.demo-range-input {
display: block;
width: 100%;
margin-bottom: 15px;
}
</style>

渲染结果

把过渡放到组件里

Organizing Transitions into Components

管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。将之前的示例改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!-- Organizing Transitions into Components -->
<!-- 把过渡放到组件里 -->
<!-- 管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,
幸好很多的动画可以提取到专用的子组件。
我们来将之前的示例改写一下 -->
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<div id="example-8" class="demo">
<input v-model.number="firstNumber" type="number" step="20"> +
<input v-model.number="secondNumber" type="number" step="20"> =
{{ result }}
<p>
<animated-integer v-bind:value="firstNumber"></animated-integer> +
<animated-integer v-bind:value="secondNumber"></animated-integer> =
<animated-integer v-bind:value="result"></animated-integer>
</p>
</div>
<script>
// 这种复杂的补间动画逻辑可以被复用
// 任何整数都可以执行动画
// 组件化使我们的界面十分清晰
// 可以支持更多更复杂的动态过渡
// 策略。
Vue.component('animated-integer', {
template: '<span>{{ tweeningValue }}</span>',
props: {
value: {
type: Number,
required: true
}
},
data: function () {
return {
tweeningValue: 0
}
},
watch: {
value: function (newValue, oldValue) {
this.tween(oldValue, newValue)
}
},
mounted: function () {
this.tween(0, this.value)
},
methods: {
tween: function (startValue, endValue) {
var vm = this
function animate() {
if (TWEEN.update()) {
requestAnimationFrame(animate)
}
}

new TWEEN.Tween({ tweeningValue: startValue })
.to({ tweeningValue: endValue }, 500)
.onUpdate(function () {
vm.tweeningValue = this.tweeningValue.toFixed(0)
})
.start()

animate()
}
}
})
// 所有的复杂度都已经从 Vue 的主实例中移除!
// (对比14.StateTransitionsAnimatingStatewithWatchers.html)
new Vue({
el: '#example-8',
data: {
firstNumber: 20,
secondNumber: 40
},
computed: {
result: function () {
return this.firstNumber + this.secondNumber
}
}
})
</script>

渲染结果

+ = {{ result }}

+ =

赋予设计以生命

Bringing Designs to Life

只要一个动画,就可以带来生命。不幸的是,当设计师创建图标、logo 和吉祥物的时候,他们交付的通常都是图片或静态的 SVG。所以,虽然 GitHub 的章鱼猫(octocat)、Twitter 的小鸟以及其它许多 logo 类似于生灵,它们看上去实际上并不是活着的。

Vue 可以帮到你。因为 SVG 的本质是数据,我们只需要这些动物兴奋、思考或警戒的样例。然后 Vue 就可以辅助完成这几种状态之间的过渡动画,来制作你的欢迎页面、加载指示、以及更加带有情感的提示。

Sarah Drasner 展示了下面这个 demo,这个 demo 结合了时间和交互相关的状态改变:

1
2
<p data-height="265" data-theme-id="light" data-slug-hash="YZBGNp" data-default-tab="result" data-user="sdras" data-embed-version="2" data-pen-title="Vue-controlled Wall-E" class="codepen">查看 <a href="https://codepen.io">CodePen</a> 上 Sarah Drasner (<a href="https://codepen.io/sdras">@sdras</a>) 的例子 <a href="https://codepen.io/sdras/pen/YZBGNp/">Vue-controlled Wall-E</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

渲染结果

查看 CodePen 上 Sarah Drasner (@sdras) 的例子 Vue-controlled Wall-E.

编辑的话,需求去 codepen.io 网站。codepen是在线前端开发工具,即时预览,以及链接分享,直接让他人观看到code渲染结果而无需在本地运行。

附 codepen hello world 分享示例:

1
2
<p data-height="265" data-theme-id="light" data-slug-hash="BaKZEqm" data-default-tab="result" data-user="zhang" data-embed-version="2" data-pen-title="Vue-controlled Wall-E" class="codepen"> 查看 <a href="https://codepen.io">CodePen</a> 上 Stan Zhang (<a href="https://codepen.io/sdras">@codingames</a>) 的例子 <a href="https://codepen.io/sdras/pen/BaKZEqm/">Vue-controlled Wall-E</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

渲染结果

查看 CodePen 上 Stan Zhang (@codingames) 的例子 Vue-controlled Wall-E.

SVG 有些复杂,应该一时用不到?暂时先放过。