FLIP动画实现卡片到弹窗打开的动画效果(仿小红书)
最近在学习通过js实现对不定高的元素实现高度变化的过渡效果时,了解到了FLIP动画这个技术。
FLIP动画原理:
- First:在动画效果开始前,记录元素初始状态的位置和尺寸信息,可以通过getBoundingClientRect()方法获取
- Last:执行相应代码设置元素为动画的最终状态,并记录元素最终状态的位置和尺寸信息
- Invert:计算元素初始和最终状态之间的位置、大小变化,使用这些数字做一定的计算,通过transform属性让元素进行移动、缩放
- Play: 播放动画,首先让元素恢复到最初状态,再通过transform属性让元素从初始状态到最终状态,并通过transition给动画增加过渡效果实现平滑的动画效果
先浅浅实现一下FLIP动画实现不定高的高度渐变效果:
代码如下:
<template>
<div class="home-box">
<el-button
type="primary"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>展开按钮</el-button
>
<div class="text-box" ref="textBoxRef">
<div class="text">
FLIP动画的原理是通过计算元素在布局变化前后的位置和尺寸差异,然后应用反向变换来模拟元素的移动,最后播放动画,从而实现流畅的过渡效果。
FLIP技术的优势在于它提供了一种高效的方式来动态改变DOM元素的位置和尺寸,同时赋予动效,从而增强用户体验。通过FLIP技术,开发者可以更专注于布局变化,而无需手动计算和应用变换,简化了动画开发的复杂度,提高了开发效率。
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, getCurrentInstance } from "vue";
const instance = getCurrentInstance();
const handleMouseEnter = () => {
const targetNode = instance?.refs.textBoxRef as HTMLElement;
// F:记录初始状态的位置和尺寸 默认就是初始状态 height: 0
// L: 执行代码让动画进入最终状态,并记录最终状态的位移和大小
// 不定高,则设置高度为auto,并通过getBoundingClientRect获取最终高度
targetNode.style.height = "auto";
const lastInfo = targetNode.getBoundingClientRect();
// I:计算最终状态和初始状态的位移和尺寸差异,并应用反向变换
// P:执行动画,先让元素恢复到初始状态,再使用transition让元素从初始状态过渡到最终状态
targetNode.style.height = "0";
targetNode.style.transition = "height 0.3s";
// 过渡到最终状态,下一帧执行,给动画一点缓冲时间
requestAnimationFrame(() => {
targetNode.style.height = lastInfo.height + "px";
});
};
const handleMouseLeave = () => {
const targetNode = instance?.refs.textBoxRef as HTMLElement;
requestAnimationFrame(() => {
targetNode.style.height = "0";
});
};
</script>
<style lang="scss" scoped>
.home-box {
.text-box {
width: 300px;
height: 0;
overflow: hidden;
border-radius: 10px;
margin-top: 10px;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
.text {
padding: 10px;
}
}
}
</style>
接下来实现小红书点击卡片弹出内容弹窗的效果:
dom结构代码如下:
<template>
<div class="animation-page">
<div class="left-side"></div>
<div class="flip-animation">
<div
class="flip-card"
@click="openDialog"
v-for="(item, index) in list"
:key="index"
>
<img src="@/assets/images/2.jpg" alt="" />
</div>
</div>
<div class="dialog" v-if="showMask" @click="closeDialog" ref="dialogRef">
<div class="dialog-content" ref="dialogContentRef" @click.stop>
<div class="left-container" :style="{ width: mediaWidth + 'px' }">
<img src="@/assets/images/2.jpg" alt="" />
</div>
<div class="right-container">
<img src="@/assets/images/right.png" alt="" />
</div>
</div>
</div>
</div>
</template>
js部分的代码如下,注意查看代码中的注释部分哦,标注了需要注意的事项:
<script lang="ts" setup>
import { onMounted, ref, nextTick } from "vue";
const showMask = ref(false);
const loadImg = () => {
const maxImageHeight = window.innerHeight - 64;
return new Promise((resolve, reject) => {
const img = new Image();
img.src = "/src/assets/images/2.jpg";
img.onload = (e) => {
const realWidth = (maxImageHeight / e.target.height) * e.target.width;
resolve({
realWidth,
realHeight: maxImageHeight,
});
};
});
};
const mediaWidth = ref();
const zoom = ref();
let firstInfo = {},
lastInfo = {},
dialogNode = null,
maskNode = null,
convertY = 0,
convertX = 0;
const openDialog = async (e) => {
// F:记录初始状态的位置,首先为实现该效果,弹窗的初始位置需与卡片重叠,且左侧图片的大小刚好与卡片中的图片大小一致
firstInfo = e.target.getBoundingClientRect();
// 点击弹窗,获取当前图片的实际宽高,此处是因为小红书内容弹窗左侧的图片的宽高是动态计算的,所以需要动态获取
const { realWidth, realHeight } = await loadImg();
mediaWidth.value = realWidth;
// 展示弹窗,获取弹窗的最终状态的位置
showMask.value = true;
nextTick(() => {
dialogNode = document.querySelector(".dialog-content");
maskNode = document.querySelector(".dialog");
// L:记录弹窗的最终态的位置和大小信息
lastInfo = dialogNode.getBoundingClientRect();
// I:计算弹窗初始态和最终态的位移差和缩放的比例,并设置到弹窗上
// 卡片图片与弹窗图片保持一致,所以缩放比例就是卡片图片的宽度与弹窗图片宽度的比例
// 然后通过计算位移将弹窗移动到卡片的位置
zoom.value = firstInfo.width / mediaWidth.value;
convertX = firstInfo.x - lastInfo.x;
convertY = firstInfo.y - lastInfo.y;
// P: 设置弹窗到初始状态
// 注意这里的一个技巧:将缩放放置到translate的前面,这样计算位移就不需要考虑缩放后产生的位移差了
dialogNode.style.transform = `translate(calc(-50% + ${convertX}px), calc(-50% + ${convertY}px)) scale(${zoom.value}) `;
dialogNode.style.transformOrigin = "left top";
// P:下一帧取消回到初始状态的动画效果,则恢复到最终态
requestAnimationFrame(() => {
// 最后一定要再次设置元素的宽度,否则关闭弹窗时的宽度过渡不生效
dialogNode.style.transition = "transform 0.4s, width 0.4s";
dialogNode.style.width = lastInfo.width + "px";
dialogNode.style.transform = "";
maskNode.style.backgroundColor = "rgba(0, 0, 0, 0.4)";
});
});
};
const closeDialog = () => {
// F:记录当前弹窗的初始态信息,之前的lastInfo即为关闭弹窗时的初始态信息
// 这里需要对初始态进行处理,即弹窗右侧的文字内容最终需要隐藏才可实现效果
requestAnimationFrame(() => {
// 设置弹窗的宽度为图片的宽度,右侧部分溢出后隐藏,动画效果集中在左边图片部分
// 这里有个关键点就是在改变弹窗的宽度后,使用弹性布局的情况下,需要设置图片的flex-shrink:0,否则图片会随着弹窗的宽度变化而变化
dialogNode.style.width = mediaWidth.value + "px";
dialogNode.style.overflow = "hidden";
// L:记录当前弹窗的最终态信息,之前的初始态firstInfo即为弹窗的最终态信息
dialogNode.style.transition = "transform 0.4s, width 0.4s";
// I:计算位移和缩放差距,这里跟打开时的计算方式一致
// 多出的220px是因为右侧文字内容的宽度设置了定宽440px,在隐藏了右半部分后,会产生一半的位置偏移,需要减去
// P: 设置弹窗从初始状态, 再过渡到最终态,这里与打开有点不一样,原因是打开的时候我们已知的是最终态,而这里我们已知的是初始态
dialogNode.style.transform = `translate(calc(-50% + ${convertX}px - 220px), calc(-50% + ${convertY}px)) scale(${zoom.value})`;
dialogNode.style.transformOrigin = "left top";
maskNode.style.backgroundColor = "transparent";
});
dialogNode.addEventListener("transitionend", () => {
// 动画结束后移除弹窗
showMask.value = false;
});
};
const list = ref([
{ width: 250, height: 300 },
{ width: 250, height: 420 },
{ width: 250, height: 280 },
{ width: 250, height: 320 },
{ width: 250, height: 360 },
{ width: 250, height: 300 },
{ width: 250, height: 400 },
{ width: 250, height: 340 },
]);
</script>
css样式代码如下:
<style lang="scss" scoped>
.animation-page {
display: flex;
align-content: flex-start;
width: 1650px;
margin: 0 auto;
padding-top: 120px;
.left-side {
width: 260px;
}
.flip-animation {
display: grid;
grid-template-columns: repeat(5, 250px);
grid-gap: 10px;
}
}
.flip-card {
> img {
width: 100%;
border-radius: 20px;
}
}
.dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
transition: background-color 0.4s;
z-index: 9999;
.dialog-content {
display: flex;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1);
height: calc(100% - 64px);
background-color: #fff;
border-radius: 20px;
overflow: visible;
.left-container {
flex-shrink: 0;
flex-grow: 0;
height: 100%;
border-radius: 20px 0 0 20px;
overflow: hidden;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.right-container {
width: 440px;
flex-shrink: 0;
flex-grow: 1;
border-radius: 0 20px 20px 0;
overflow: hidden;
padding: 10px;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
}
}
</style>
原文地址:https://blog.csdn.net/qiansitong/article/details/142878113
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!