✨✨做一个伪3D效果的卡片列表来复习一下CSS动画吧~
前言
本来是准备接着前面做的那个 使用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 的位置。
此时,显示效果就是这样了:
最后
当然本文个人感觉干货不是很多,只是在平时突然想到的一点儿小点子,但是动画效果其实不是很完美。只希望能给大家带来一点小小的灵感吧~~~
转载自:https://juejin.cn/post/7158848986659422244