当前位置: 首页 > 工具软件 > Flip! > 使用案例 >

FLIP实现animation动画

夏侯承恩
2023-12-01

本文代码均放在git仓库

FLIP是什么

首先FLIP并不是一项新技术,可以把它理解为一种实现动画的新的理念或者新的方法。

FLIP是 First、Last、Invert和 Play四个单词首字母的缩写。

  • First,指的是在任何事情发生之前(过渡之前),记录当前元素的位置和尺寸,即动画开始之前那一刻元素的位置和尺寸信息,可以使用 getBoundingClientRect()这个 API来处理;
  • Last:执行一段代码,让元素发生相应的变化,并记录元素在动画最后状态的位置和尺寸,即动画结束之后那一刻元素的位置和尺寸信息;
  • Invert:计算元素第一个位置(First)和最后一个位置(Last)之间的位置或者尺寸变化,然后使用这些数字做一定的计算,让元素进行移动(通过 transform来改变元素的位置和尺寸),从而创建它位于初始位置的一个错觉。即让元素处于动画的结束状态,然后使用 transform属性将元素反转回动画的开始状态(这个状态的信息在 First步骤就拿到了);
  • Play:将元素反转(假装在first位置),我们可以把 transform设置为 none,因为失去了 transform的约束,所以元素肯定会往本该在的位置(即动画结束时的那个状态)进行移动,也就是last的位置,如果给元素加上 transition的属性,那么这个过程自然也就是以一种动画的形式发生了。

举个?

var app = document.getElementById('app');
var first = app.getBoundingClientRect(); // 初始态
app.classList.add('app-to-end');
var last = app.getBoundingClientRect(); // 终态
var invert = first.top - last.top;
// 使元素看起来好像在起始点
app.style.transform = `translateY(${invert}px)`;
requestAnimationFrame(function() {
   // 启用tansition,并移除翻转的改变
  app.classList.add('animate-on-transforms');
  app.style.transform = '';
  // 此时,方块就会从假装在起始点开始transition
});
app.addEventListener('transitionend', () => {
  app.classList.remove('animate-on-transforms');
});

复制代码
<div id="app"></div>
<style>
    #app {
        position: absolute;
        width:20px;
        height:20px;
        background: red;
    }
    .app-to-end {
        top: 100px;
    }
    .animate-on-transforms {
        transition: all 2s;
    }
</style>
复制代码

在React中是什么用的?

微信app里聊天界面点击预览图片时,图片从对话框到全屏预览的这个过程,用了一个过渡的动画,呈现出图片从小图到大图和从大图恢复到小图的全过程。代码仓库

First

<ul className="pic-list">
  {
    listData.map((item, index) => (
      <li
        key={index}
        className="pic-item"
        onClick={this.previewItem.bind(this, 1, item)}  // 给每一个item绑定处理函数
        title="点击预览">
        <img src={item.bgPic} alt="" className="pic" />
      </li>
    ))
  }
</ul>
复制代码

在previewItem函数中的处理逻辑:计算初始态

previewItem (status, previewImgInfo = null, e) {
    previewVisibleStatus = status;
    if (previewVisibleStatus === 1) {
      // 计算初始态
      const currentPreviewEle = e.target;
      rectInfo = currentPreviewEle.getBoundingClientRect()
      previewFirstRect[0] = rectInfo.left
      previewFirstRect[1] = rectInfo.top
      this.setState({
        previewImgInfo,
        previewStatus: 1
      })
    } else {
      this.setState({
        previewStatus: 1
      })
    }
}
复制代码

此时会触发一次render()函数

{
  (previewVisibleStatus === 1 || previewVisibleStatus === 2) ? (
    <>
      <div
        className="preview-box"
        onClick={this.previewItem.bind(this, 2)}
        style={{
          opacity: previewStatus === 3 && previewVisibleStatus !== 2 ? .65 : 0
        }}
      ></div>
      <img
        ref={this.previewRef}
        src={previewImgInfo.bgPic} // 图片URL
        style={{
          // 此时previewStatus是1,transform属性为translate3d(0, 0, 0) scale(1)
          transform: previewStatus === 2 || previewVisibleStatus === 2
            ? `xxxxxx` : 'translate3d(0, 0, 0) scale(1)',
          transformOrigin: '0 0'
        }}
        onClick={this.previewItem.bind(this, 2)}
        onTransitionEnd={this.transEnd.bind(this)}
        alt="" />
    </>
  ) : null
}
复制代码

Last

接着会触发componentDidUpdate生命周期函数,在该生命周期函数中主要是执行updatePreviewStatus函数。

updatePreviewStatus () {
    if (this.state.previewStatus === 1) {
      // 计算终态
      if (previewVisibleStatus === 1) {
        const lastRectInfo = this.previewRef.current.getBoundingClientRect()
        previewLastRect[0] = lastRectInfo.left
        previewLastRect[1] = lastRectInfo.top
        scaleValue = rectInfo.width / lastRectInfo.width
      }
      this.setState({
        previewStatus: 2
      })
    } else if (this.state.previewStatus === 2) {
      // Play
      setTimeout(() => {
        this.setState({
          previewStatus: 3
        })
      }, 0)
    }
}
复制代码

Invert

再次触发render()函数

{
  (previewVisibleStatus === 1 || previewVisibleStatus === 2) ? (
    <>
      <div
        className="preview-box"
        onClick={this.previewItem.bind(this, 2)}
        style={{
          opacity: previewStatus === 3 && previewVisibleStatus !== 2 ? .65 : 0
        }}
      ></div>
      <img
        ref={this.previewRef}
        src={previewImgInfo.bgPic}  // 图片URL
        style={{
          // 此时previewStatus是2,transform属性为下面这一大坨
          transform: previewStatus === 2 || previewVisibleStatus === 2
            ? `translate3d(${previewFirstRect[0] - previewLastRect[0]}px, ${previewFirstRect[1] - previewLastRect[1]}px, 0) scale(${scaleValue})`
            : 'translate3d(0, 0, 0) scale(1)',
          transformOrigin: '0 0'
        }}
        onClick={this.previewItem.bind(this, 2)}
        onTransitionEnd={this.transEnd.bind(this)}
        alt="" />
    </>
  ) : null
}
复制代码

此时,transform属性是

translate3d(${previewFirstRect[0] - previewLastRect[0]}px, ${previewFirstRect[1] - previewLastRect[1]}px, 0) scale(${scaleValue})
复制代码

也就是说第二次执行render函数的时候,图片在(translate3d + scale)的作用下缩放回了初始位置。

Play

再次触发componentDidUpdate生命周期函数,在该生命周期函数中主要是执行updatePreviewStatus函数。

updatePreviewStatus () {
    if (this.state.previewStatus === 1) {
      // xxxxx
    } else if (this.state.previewStatus === 2) {
      // Play
      setTimeout(() => {
        this.setState({
          previewStatus: 3
        })
      }, 0)
    }
}
复制代码

触发render()函数

<>
  <div
    className="preview-box"
    onClick={this.previewItem.bind(this, 2)}
    style={{
      opacity: previewStatus === 3 && previewVisibleStatus !== 2 ? .65 : 0
    }}
  ></div>
  <img
    ref={this.previewRef}
    className={`img${(previewStatus === 3 && previewVisibleStatus === 1) || previewVisibleStatus === 2 ? ' active' : ''}`}
    src={previewImgInfo.bgPic}
    onClick={this.previewItem.bind(this, 2)}
    onTransitionEnd={this.transEnd.bind(this)}
    alt="" />
</>
复制代码

为图片添加 transition属性,并移除相关 transform属性,即可启动动画。

css:
.img.active {
  transition: all .36s ease-in-out;
}
复制代码

至于放大后的图片恢复到小图这一个阶段,可以看成是另外一个 FLIP动画,继续套用即可,只不过这个动画就是上一个放大动画的逆向,所需的尺寸和位置信息都已经拿到了,可以省去调用 getBoundingClientRect的过程。

为什么要用FLIP?

有些人可能比较疑惑,如果想要实现动画的话,直接 transform不就好了,为什么要多此一举搞个 FLIP的概念出来?

对于一些动画,你明确的知道它的初始态(First)和结束态(Last),比如你就想让一个元素从 left:10px;移动到 left:100px;,那么你直接 transform就好了,根本没必要 FLIP,用了反而多此一举;

但除此之外,还有一部分你无法明确的初始态(First)或结束态(Last)的动画。这样情况下使用FLIP就会比较爽快了。但是你可能又会说,我即时不知道结束态,但是可以使用浏览器 API进行测量啊。

不好意思,这正是 FLIP要做的事情之一,你还是在无意识地情况下用到了这个东西,只不过相对于被前人总结并优化后的 FLIP来说,你的整体用法可能更零散更不规范一些。

转载于:https://juejin.im/post/5ce53ca3e51d4577583ddc2d

 类似资料: