前言
本来是准备接着前面做的那个 使用SVG实现动态分布的圆环发散路径动画 的效果,希望通过 纯 Div + CSS 的方式来实现。但是目前看起来进度比较缓慢,虽然做出来了大致样式,但是动画还没加上,所以得后面再继续弄。
今天主要是通过写一个 “伪3D” 的卡片组效果,顺道也复习一下基础的 CSS 动画。
因为作者平时都是写的后台管理的项目,所以接触 CSS 动画之类的东西也比较少,实现方式和效果看起来可能没有那么好,希望大家多多包涵。
开始
本身这里最初的设想是希望 像打牌那样的初始效果,然后 通过点击卡片抽出单张选中卡片预览,再加上一些简单的过渡动画。不过后来又觉得 直接做成平铺列表然后选中卡片移动到最右侧 的效果也行,所以干脆都加上算了。
当然为了保证正确的层级关系,还是使用了 定位配合 zIndex 来确定每个卡片的 层级顺序和位置。
卡片的排列模式也是通过一个变量来确定的,然后增加了一个切换按钮,后面如果有需要的话改成 props 配置也行。
话不多说,直接开干吧。
模拟数据
当然,在开始之前肯定要 模拟一个卡片数组 cards 与一个排列模式变量 clutter。
data() { return { activeIndex: -1, clutter: true, // 杂乱 cards: [] }; }, created() { this.initData(); }, methods: { initData() { const arr = new Array(12).fill(1); this.cards = arr.map((_, index) => { return this.computedStyle(index, 12); }); }, resetData() { this.clutter = !this.clutter; this.initData(); }, computedStyle(index, length) { const clutter = this.clutter; const defaultStyles = { "--max-index": length + 1, "--bg-color": randomRgbColor(), "--card-index": index }; return defaultStyles; } }
这里的 initData 和 resetData 肯定就不需要介绍啦,就是重置和切换排列模式,当然因为改变排列模式后样式也有改动,所以在resetData 中也重新调用了initData来重新生成卡片数组。
至于computedStyle则是 计算每个卡片的样式并返回样式结果的,不管什么排列模式下 这几个 CSS 变量都是必须且固定的,所以就先放上来了。
data 中的activeIndex 则是代表当前激活的卡片下标的。
键盘事件
这里的初衷是为了方便 快速切换激活卡片、在卡片太多时也可以减少误触。
这里唯一注意的一点就是,在执行键盘监听事件的时候,需要判断一下当前的 focus 元素,如果是在一些可聚集的元素中则不能触发翻页。
mounted() { let addIndex = () => { if (this.activeIndex < this.cards.length - 1) { this.activeIndex++; } else { this.activeIndex = 0; } }; let lessIndex = () => { if (this.activeIndex > 0) { this.activeIndex--; } else { this.activeIndex = this.cards.length - 1; } }; let keyboardDeal = (e) => { if (document.activeElement !== document.body) return; // 方向键--上 if (e.keyCode === 38) { addIndex(); } // 方向键--下 if (e.keyCode === 40) { lessIndex(); } // 方向键--左 if (e.keyCode === 37) { lessIndex(); } // 方向键--右 if (e.keyCode === 39) { addIndex(); } }; window.addEventListener("keyup", keyboardDeal); this.$on("hook:beforeDestroy", () => { window.removeEventListener("keyup", keyboardDeal); }); },
另外,这里通过 this.$on("hook:beforeDestroy") 来注册组件 在销毁之前需要执行的回调函数,功能与选项式 API 中的 beforeDestroy 配置项功能一致。
在组件即将销毁时,会清除掉组件中的键盘监听事件。
通过document.activeElement 是否等于 body 元素,来确定当前焦点位置。一般只有在该属性等于body的时候,才代表当前页面中没有其他聚焦元素,可以正常执行我们定义的按键翻页。
模板
至于 html 部分,倒是没有太多细节的内容,仅仅只是 Vue 的遍历语法和动态样式两个知识点,相信大部分同学都很清楚。
<template> <div class="AnimationCards"> <h1>AnimationCards Page</h1> <p> <el-button @click="resetData">乱序</el-button> </p> <div class="demo-content"> <div class="animation-cards-box"> <div v-for="(styles, index) in cards" :class="[ 'animation-card', { 'is-active': activeIndex === index, 'is-clutter': clutter, 'is-list': !clutter } ]" :key="index" :style="styles" @click="activeIndex = activeIndex === index ? -1 : index" > <span>Card {{ index }}</span> </div> </div> </div> </div> </template>
这里动态绑定的 is-clutter 和 is-list 其实也可以放到父级 animation-cards-box 中。
至于每个 card 元素动态绑定的 style,里面有定义好的 CSS 变量,提供给每个卡片的动画和默认样式使用。这个用法我在之前的文章中也提到过,标签的内联样式也可以直接声明 CSS 变量。
样式与动画
因为采用了两种排列方式,每种排列方式的动画都不一样,所以样式和动画都有不同。
但是因为共同都是采用的定位来确定层级的,所以也有一样的地方。
下面这部分是一样的样式部分:
.animation-cards-box { width: 100%; height: 100%; box-sizing: border-box; padding: 20px; position: relative; .animation-card { width: 200px; background-color: var(--bg-color); border-radius: 8px; cursor: pointer; position: absolute; top: 20px; bottom: 20px; box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.8); transition: all ease-in-out 0.4s; display: flex; justify-content: center; align-items: center; font-size: 32px; font-weight: bold; color: #ffffff; z-index: var(--card-index); } }
通过上面模板部分绑定的 CSS 变量来确定具体样式。
列表状态
在列表分布的状态下,所有卡片 依次向右排布,后面的卡片覆盖前面的卡片;在卡片被激活后,会移动到最右侧最顶层显示,大致效果如下:
1. 所以,首先是正常情况下的分布样式
.animation-card.is-list { &:hover { & ~ .animation-card { transform: translateX(24px); } } &.is-active { z-index: var(--max-index) !important; transform: translateX(calc(var(--max-index) * 20px - var(--card-index) * 20px - 40px)); } }
在 hover 时,所有 后面的兄弟节点会向右移动 20px;激活后(is-active),被激活的卡片会通过 zIndex 置顶,并通过 transform 向右平移到 最右侧。
2. 然后,是 js 中的 computedStyle 样式计算,按顺序每个卡片向右移动 16 px(后面可以根据需求修改):
computedStyle(index, length) { const clutter = this.clutter; const defaultStyles = { "--max-index": length + 1, "--bg-color": randomRgbColor(), "--card-index": index }; if (!clutter) defaultStyles["left"] = `${16 * ++index}px`; return defaultStyles; }
3. 最后,是列表横向排布时的动画帧。
@keyframes eject { 50% { transform: translateX(calc(-100% - 20px)) rotate(-20deg); } } @keyframes reject { 50% { transform: translateX(calc(100% + 20px)) rotate(10deg); } }
这里区分了 非激活 -> 被激活 的 eject 和 从被激活 -> 非激活状态的 reject 两个动画:
- eject :动画中间状态是 整体 向左偏移 整个卡片宽度加上 20px 的距离,并向左稍微旋转 20deg
- reject :动画中间状态是 整体 向右偏移 整个卡片宽度加上 20px 的距离,并向右稍微旋转 10deg
另外eject的动画时间比reject 多一倍,也是为了把注意力集中在被激活卡片。
本身我是希望最后才把 zIndex 设置成最大值的,但是因为用了CSS变量的关系,动画帧定义不知道咋写了。如果有大佬知道也请告诉我一下,非常感谢~~
扇形乱序分布
这也是我里边说“乱序”状态吧,因为最初的一版卡片分布是按下标依次一左一右排列的,所以顺序有一点问题;不过后面也加了一个完整扇形的效果。
基础样式与列表状态一样,有区别的只是 激活/非激活的样式计算和动画定义部分。
这个效果大概像这样:
1. 首先,一样是两个状态的样式定义:
.animation-card.is-clutter { transform: translateX(0%) rotate(var(--rotate-deg)); transform-origin: bottom center; &.is-active { animation: rotation ease-in-out 0.8s; transform: translateX(calc(120%)) rotate(0deg); z-index: var(--max-index) !important; } }
因为是 类似扇形的效果,需要保证旋转轴的位置在底部,所以需要设置 transform-origin。
- 正常状态 下,卡片只是稍微旋转
- 被激活状态 下,卡片会平移到最后侧并取消旋转角度,同时置顶
2. 然后,是样式计算部分
computedStyle(index, length) { const clutter = this.clutter; const defaultStyles = { "--max-index": length + 1, "--bg-color": randomRgbColor(), "--card-index": index }; if (clutter) { let rotate = 0; if (index % 2 === 1) { rotate = length - index; } else { rotate = index - length; } defaultStyles["--rotate-deg"] = rotate + "deg"; } return defaultStyles; }
根据下标的奇偶性,分别向左、向右旋转特定角度。
3. 最后,就是动画定义
这里为了方便,只定义了 从 非激活 -> 激活状态 的动画,取消激活时则是直接通过 transition 定义的一个过渡效果。
@keyframes rotation { 0% { transform: translateX(0%) rotate(var(--rotate-deg)); } 60% { transform: translateX(calc(130%)) rotate(2deg); } 70% { transform: translateX(calc(110%)) rotate(-2deg); } 80% { transform: translateX(calc(125%)) rotate(1deg); } 90% { transform: translateX(calc(115%)) rotate(-1deg); } 100% { transform: translateX(calc(120%)) rotate(0deg); } }
这里动画 初期依旧保持正常状态,到 60% 时到达目标区域,后面的过程就是一个轻微晃动的效果,在100% 时停留在目标位置。
扇形正序
早上突然发现,正序其实修改不大,只需要调整 computedStyle 方法即可。
computedStyle(index, length) { const clutter = this.clutter; const defaultStyles = { "--max-index": length + 1, "--bg-color": randomRgbColor(), "--card-index": index }; const tangle = 48; const unitArc = tangle / length; if (clutter) { let rotate = unitArc * index - 48 / 2; defaultStyles["--rotate-deg"] = rotate + "deg"; return defaultStyles; }
这里的 tangle 是两边的最大旋转角度,可以根据需要自定义。unitArc 则是单位旋转角度,然后 初始角度 则是在左边 二分之一 tangle 的位置。
此时,显示效果就是这样了:
最后
当然本文个人感觉干货不是很多,只是在平时突然想到的一点儿小点子,但是动画效果其实不是很完美。只希望能给大家带来一点小小的灵感吧~~~