当前位置: 首页 > Web前端 > HTML5

前端动画你一定知道:React和Vue都在用FLIP的思想来实现小姐姐的流畅动作

时间:2023-04-05 23:31:24 HTML5

前言在Vue官网的转场动画章节,可以看到炫酷的动画效果。乍一看,我们手写这个逻辑会很复杂。我们来看看本文的最终效果。好吧,这与这个案例非常相似。预览也可以直接在预览网址查看:http://sl1673495.gitee.io/fli...图片素材依旧引用自知乎问题《有个漂亮女朋友是种怎样的体验?》,侵删。分析需求拿到这个需求后,第一反应是做什么?假设第一行第一张图片移动到第二行第三列,要先计算第一行的高度,再计算第二行的前两个元素的宽度,然后用CSS或SomeanimationAPI搬过去?这样做是可以的,但是当图片的高度不固定,宽度不固定,而且需要一次性移动很多张图片时,计算方法非常复杂。并且在这种情况下,需要手动管理图片的坐标,非常不利于维护和扩展。换个思路,我们是不是可以直接通过原生的API,很自然的把DOM元素添加到DOM树中,然后让浏览器帮我们把这个端点值,最后我们可以动画化位移呢?在文档中,我们发现了一个名词:FLIP,给了我们一个线索,是不是可以用这个东西来写这个动画?答案是肯定的,顺着这个线索在Aerotwist社区找到一篇文章:flip-your-animations,以这篇文章为起点,一步步达到类似的效果。FLIPFLIP到底是什么?我们先看看它的定义:First即将动画的元素的初始状态(比如位置,透明度等)。Last要设置动画的元素的最终状态。反转是关键的一步。假设我们图片的初始位置是left:0,top:0,动画之后元素的最终位置是left:100,top100,那么很明显这个元素向右下角移动了100px。不过这时候我们不是按照常规的思路计算它最终的位置,然后命令元素从0,0移动到100,100,而是先让元素自己移动(比如在Vue中用数据驱动,在数组前面添加几张图片,之前的图片会自己移到最下面)。这里有一个需要注意的关键知识点,我在之前的文章《深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调》中也提到过:DOM元素属性(如left、right、transform等)的变化会集中延迟到浏览器端。帧的下一帧是统一渲染的,所以我们可以得到这样一个中间时间点:DOM状态(位置信息)发生了变化,但是浏览器还没有渲染。有了这个前提,我们就可以保证Vue会先操作DOM的变化。这个时候浏览器还没有渲染,我们已经可以拿到DOM状态变化后的位置了。具体来说,假设我们的图片是排成两行的,图片数组的初始状态是[img1,img2,此时我们在图片的头部添加两个元素[img3,img4,img1,img2]数组,那么img1和img2自然会被推到下一行。假设img1的初始位置是0,0,数据驱动引起DOM变化后的位置是100,100,那么此时浏览器还没有渲染,我们可以设置img1.style.transform=translateat这个点(-100px,-100px),让它先Invert,回到位移前的位置。Play反转后,很简单的让它动起来,然后让它回到0、0的位置。本文将使用最新的WebAnimationAPI来实现最终的Play。MDN文档:WebAnimation实现图像渲染一开始很简单,只需将图像排列成4列即可:.wrap{display:flex;flex-wrap:wrap;}.img{width:25%;

那么重点就是imgs数组添加元素后如何制作平滑的路径动画。下面来实现添加图片的方法add:asyncadd(){constnewData=this.getSister()awaitpreload(newData)}先随机取出几张图片作为元素放入数组中,使用newImage预加载这些图片,以防止将一堆空白图像渲染到屏幕上。然后定义一个计算一组DOM元素位置的函数getRects,使用getBoundingClientRect获取最新的位置信息。在获取图片元素的旧位置和新位置时会用到该方法。functiongetRects(doms){returndoms.map((dom)=>{constrect=dom.getBoundingClientRect()const{left,top}=return{left,top}})}//当前存在的图像constprevImgs=this.$refs.imgs.slice()constprevPositions=getRects(prevImgs)记录图片的旧位置后,可以在数组中添加新图片:this.imgs=newData.concat(this.imgs)然后来更关键的一点。我们知道Vue是异步渲染的,也就是改变imgs数组后DOM不会马上改变。这个时候我们就需要用到nextTickAPI,它会调用你传入的回调函数放入microTask队列中。上面提到的事件循环的文章中提到,microTask队列的执行必须发生在浏览器重新渲染之前。由于this.imgs=newData.concat(this.imgs)首先被调用,因此触发了Vue的响应式依赖更新。这个时候Vue会先把这个DOM更新的渲染函数放到microTask队列中。当队列为[changeDOM]时。调用nextTick(callback)后,回调函数也会加入到队列中,此时的队列为[changeDOM,callback]。现在,聪明的你一定明白为什么在nextTick的回调函数中可以获取到最新的DOM状态。由于我们之前保存了图片元素节点的数组prevImgs,所以在nextTick中调用同样的getRect方法获取旧图片的最新位置。asyncadd(){//最新的DOM状态this.$nextTick(()=>{//再次调用相同的方法获取最新的元素位置constcurrentPositions=getRects(prevImgs)})},此时我们已经有了Invert步骤的关键信息,新位置和旧位置,那么下一步就很简单了,循环image数组,做一个反转后Play的动画就可以了。prevImgs.forEach((imgRef,imgIndex)=>{constcurrentPosition=currentPositions[imgIndex]constprevPosition=prevPositions[imgIndex]//倒置位置,虽然图片移动到最新的位置,但是你先回去,等我'll让你做动画constinvert={left:prevPosition.left-currentPosition.left,top:prevPosition.top-currentPosition.top,}constkeyframes=[//初始位置是反转位置{transform:`translate(${invert.left}px,${invert.top}px)`,},//图像在更新后的位置{transform:"translate(0)"},]constoptions={duration:300,easing:"cubic-bezier(0,0,0.32,1)",}//开始移动!constanimation=imgRef.animate(keyframes,options)})至此,一个非常流畅的路径动画效果就完成了。完整的实现如下:asyncadd(){constnewData=this.getSister()awaitpreload(newData)constprevImgs=this.$refs.imgs.slice()constprevPositions=getRects(prevImgs)this.imgs=newData.concat(this.imgs)this.$nextTick(()=>{constcurrentPositions=getRects(prevImgs)prevImgs.forEach((imgRef,imgIndex)=>{constcurrentPosition=currentPositions[imgIndex]constprevPosition=prevPositions[imgIndex=constin{left:prevPosition.left-currentPosition.left,top:prevPosition.top-currentPosition.top,}const关键帧=[{transform:`translate(${invert.left}px,${invert.top}px)`,},{transform:"translate(0)"},]constoptions={duration:300,easing:"cubic-bezier(0,0,0.32,1)",}constanimation=imgRef.animate(关键帧,options)})})},乱序现在我们要实现官网demo中的shuffle效果,加上添加图片逻辑的铺垫,是不是已经觉得脑洞大开呢?没错,画面被破坏的再厉害,只要如果我们有了“图片开头的位置”和“图片结尾的位置”,那么我们就可以轻松的做路径动画了。现在我们需要做的是提取动画的逻辑。我们来分析一下整个环节:保存旧位置->改变数据驱动视图更新->获取新位置->使用FLIP做动画其实只需要传入一个update方法告诉我们如何更新图片数组,而这个逻辑完全可以抽象成一个函数。scheduleAnimation(update){//获取旧图像的位置constprevImgs=this.$refs.imgs.slice()constprevSrcRectMap=createSrcRectMap(prevImgs)//更新数据update()//DOM更新后this.$nextTick(()=>{constcurrentSrcRectMap=createSrcRectMap(prevImgs)Object.keys(prevSrcRectMap).forEach((src)=>{constcurrentRect=currentSrcRectMap[src]constprevRect=prevSrcRectMap[src]constinvert={left:prevRect.left-currentRect.left,顶部:prevRect.top-currentRect.top,}constkeyframes=[{transform:`translate(${invert.left}px,${invert.top}px)`,},{transform:""},]constoptions={duration:300,easing:"cubic-bezier(0,0,0.32,1)",}constanimation=currentRect.img.animate(keyframes,options)})})}然后添加图像,乱序函数变得非常简单://appendpicturesasyncadd(){constnewData=this.getSister()awaitpreload(newData)this.scheduleAnimation(()=>{this.imgs=newData.concat(this.imgs)})},//乱序图像shuffle(){this.scheduleAnimation(()=>{this.imgs=shuffle(this.imgs)})}源码地址https://github.com/sl1673495/...总结FLIPFLIP不仅可以做位置变化的动画,还可以很方便的实现透明度,宽高等,比如一个动画经常出现在一个e-commerce平台,点击一张产品图片后,产品从原来的位置慢慢放大到一个完整的页面。掌握了FLIP的思想后,只要知道元素在动画前的状态和元素在动画后的状态,就可以很轻松的通过“倒置状态”,让它们在流畅的动画后到达目的地,而此时的DOM状态是干净的,而不是通过繁重的计算迫使它从0,0转移到100,100,并在DOM样式上留下类似transform:translate(100px,100px)的东西。WebAnimation使用WebAnimationAPI让我们可以使用JavaScript更直观的描述我们需要元素做的动画。想象一下,如果这个需求是用CSS实现的,我们很可能会这样完成这个需求:.transitionDuration="0s"this._reflow=document.body.offsetHeightcurrentRect.img.classList.add("move")currentImgStyle.transform=currentRect.img.style.transitionDuration=""currentRect.img.addEventListener("transitionend",()=>{currentRect.img.classList.remove("move")})这是一个比较原生的选择CSS样式实现的FLIP动画的控制方式,这段代码让我觉得不舒服:需要和CSS交互通过类的增删改查,整个过程不直观。需要监听动画完成事件,做一些清理操作,容易漏掉。需要使用document.body.offsetHeight来触发强制同步布局,比较hack的知识点。需要使用this._reflow=document.body.offsetHeight给元素实例添加一个无意义的属性,防止被Rollup等tree-shaking打包工具误删除。+1比较hack的知识点。利用WebAnimationAPI的代码变得非常直观和可维护:constkeyframes=[{transform:`translate(${invert.left}px,${invert.top}px)`,},{transform:""},]constoptions={duration:300,easing:"cubic-bezier(0,0,0.32,1)",}constanimation=currentRect.img.animate(keyframes,options)关于兼容性问题,W3CWebAnimationAPIPolyfill有已提供,您可以放心使用。期待不久的将来,我们可以摒弃旧的动画模式,迎接这个更新更好的API。希望这篇文章能给大家一些动画的后顾之忧,谢谢!??谢谢大家1.如果本文对您有帮助,请点赞支持。您的“喜欢”是我创作的动力。2、关注公众号“前端从进阶到入院”加我为好友,我拉你进“前端进阶交流群”,大家一起交流,共同进步。