FLIP实现animation动画

本文代码均放在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来说,你的整体用法可能更零散更不规范一些。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • CSS参考手册 一、初识CSS3 1.1 CSS是什么 CSS3在CSS2.1的基础上增加了很多强大的新功能。目前...
    没汁帅阅读 3,534评论 1 13
  • 1 CALayer IOS SDK详解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi阅读 5,088评论 3 23
  • 一:在制作一个Web应用或Web站点的过程中,你是如何考虑他的UI、安全性、高性能、SEO、可维护性以及技术因素的...
    Arno_z阅读 1,126评论 0 1
  • 一、CSS入门 1、css选择器 选择器的作用是“用于确定(选定)要进行样式设定的标签(元素)”。 有若干种形式的...
    宠辱不惊丶岁月静好阅读 1,576评论 0 6
  • 通过jQuery,您可以选取(查询,query)HTML元素,并对它们执行“操作”(actions)。 jQuer...
    枇杷树8824阅读 647评论 0 3