6

前言

在项目中我们会写很多小视图到大视图之间的转换动作,比如一个小图到大图的详情,如果不加动画就会是很僵硬的小变大的效果,用户体验不是很好

通常的做法我们会加入动画来缓解这些问题,但是css方案不是通用的解决方案,而且不适合大块的更改,偏移值也是不好计算

在本文中,我们将探讨一种称为“ FLIP”的技术,该技术可用于以有效的方式对任何DOM元素的位置和尺寸进行动画处理,而不管其布局是如何计算或呈现的(例如,高度,宽度,浮点数) ,绝对定位,变换,flexbox,网格等)

为什么使用FLIP技术

你有没有试过动画heightwidthtopleft,或任何其他属性,除了transformopacity?您可能已经注意到,动画看起来有些_简陋_,这是有原因的。当任何触发布局更改的属性(例如height)时,浏览器必须递归检查其他元素的布局是否因此而发生了更改,这可能会很昂贵。如果该计算花费的时间超过一个动画帧(大约16.7毫秒),则将跳过该动画帧,因为该帧没有及时渲染,因此将导致“混乱”。在保罗·刘易斯(Paul Lewis)的文章“像素很贵”中,他进一步介绍了如何渲染像素以及各种性能支出。

简而言之,我们的目标是简短-我们希望尽快计算出最少的必要样式更改。这里的关键是唯一的动画transformopacity,和FLIP解释如何,我们只能通过模拟布局的变化transform

什么是FLIP?

FLIP是助记符装置和技术首先由保罗刘易斯杜撰,它代表˚F IRST,大号 AST, nvert,P躺在。他的文章很好地解释了该技术,但我将在此处进行概述:

  • First:在任何事情发生之前,记录将要转换的元素的当前(即第一)位置和尺寸。您可以使用element.getBoundingClientRect()它,如下所示。
  • Last:执行使过渡瞬间发生的代码,并记录元素的最终(即last)位置和尺寸。
  • Invert:由于元素位于最后一个位置,我们想通过transform修改其位置和尺寸来创建它位于第一个位置的错觉。这需要一点数学运算,但并不难。
  • Play:元素反转(并假装在第一个位置),我们可以通过将其设置为transform来将其移回到最后一个位置none

以下是如何使用Web Animations API实施这些步骤:
getBoundingClientRect
Element/animate

const elm = document.querySelector('.some-element');

// First: 获取当前元素位置属性
const first = elm.getBoundingClientRect();

// execute the script that causes layout change
doSomething();

// Last: 获取最终的位置属性
const last = elm.getBoundingClientRect();

// 反转: 计算开始和终点的差异 
// 计算初始位置和最终位置的边界
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;

// Play: 使原始从初始位置移动到最终位置
elm.animate([{
  transformOrigin: 'top left',
  transform: `
    translate(${deltaX}px, ${deltaY}px)
    scale(${deltaW}, ${deltaH})
  `
}, {
  transformOrigin: 'top left',
  transform: 'none'
}], {
  duration: 300,
  easing: 'ease-in-out',
  fill: 'both'
});

查看demo这里为了直观将动画时间加长至1秒

注意:并非所有浏览器都支持Web动画API。但是可以使用polyfill

共享元素过渡

在应用视图和状态之间转换元素的一种常见用例是,最终元素可能与初始元素不是同一DOM元素。
我们使用简单的代码来实现点击方法的效果:

const firstElm = document.querySelector('.first-element');

const first = firstElm.getBoundingClientRect();

doSomething();

const lastElm = document.querySelector('.last-element');
const last = lastElm.getBoundingClientRect();

图片点击放大demo

以下是使用共享元素过渡如何将两个完全不同的元素显示为同一元素的示例。单击其中一张图片以查看效果
demo

亲子过渡

在以前的实现中,元素范围基于window。对于大多数用例来说,这很好,但是请考虑以下情形:

  • 元素会改变位置,需要转换。
  • 该元素包含一个子元素,该子元素本身需要过渡到父元素内的其他位置。

由于先前计算的范围是相对于的window,因此我们对子元素的计算将不可用。为了解决这个问题,我们需要确保相对于父元素计算边界:

const parentElm = document.querySelector('.parent');
const childElm = document.querySelector('.parent > .child');

// First: parent and child
const parentFirst = parentElm.getBoundingClientRect();
const childFirst = childElm.getBoundingClientRect();

doSomething();

// Last: parent and child
const parentLast = parentElm.getBoundingClientRect();
const childLast = childElm.getBoundingClientRect();

// Invert: parent
const parentDeltaX = parentFirst.left - parentLast.left;
const parentDeltaY = parentFirst.top - parentLast.top;

// Invert: child relative to parent
const childDeltaX = (childFirst.left - parentFirst.left)
  - (childLast.left - parentLast.left);
const childDeltaY = (childFirst.top - parentFirst.top)
  - (childLast.top - parentLast.top);
  
// Play: using the WAAPI
parentElm.animate([
  { transform: `translate(${parentDeltaX}px, ${parentDeltaY}px)` },
  { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

childElm.animate([
  { transform: `translate(${childDeltaX}px, ${childDeltaY}px)` },
  { transform: 'none' }
], { duration: 300, easing: 'ease-in-out' });

这里还要注意几件事:

  1. 家长和孩子(的时机选择durationeasing等等)也_没有_一定要达到这种技术。随时发挥创造力!
  2. 在本示例中,有目的地省略了更改父级和/或子级(widthheight)中的尺寸,因为它是一个高级且复杂的主题。让我们将其保存为另一个教程。
  3. 您可以将共享元素和父子技术结合使用,以获得更大的灵活性。

使用Flipping.js充分灵活性

上面的技术看似简单明了,但是一旦您必须跟踪多个元素的转换,它们就会变得很乏味。Android通过以下方式减轻了这一负担:

  • 将共享元素过渡到核心SDK
  • 允许开发人员通过使用通用android:transitionNameXML属性来标识共享哪些元素

一个名为Flipping.js的小型库。通过向data-flip-key="..."HTML元素添加属性,可以可预测和有效地跟踪可能因状态而异的位置和尺寸的元素。

例如,考虑以下初始视图:

 <section class="gallery">

<div class="photo-1" data-flip-key="photo-1">
        <img src="/photo-1">
</div>

<div class="photo-2" data-flip-key="photo-2">
        <img src="/photo-2">
</div>

<div class="photo-3" data-flip-key="photo-3">
        <img src="/photo-3">
</div>

    </section>

而这个单独的细节视图:

 <section class="details">

<div class="photo" data-flip-key="photo-1">
        <img src="/photo-1">
</div>

      
        Lorem ipsum dolor sit amet...
      
    
</section>

注意,在上面的示例中,有2个元素具有相同的data-flip-key="photo-1"。Flipping.js通过选择满足以下条件的第一个元素来跟踪“活动”元素:

  • 元素存在于DOM中(即,它尚未被删除或分离)
  • 元素不是隐藏的(提示:对于隐藏元素elm.getBoundingClientRect()将具有{ width: 0, height: 0 }
  • selectActive选项中指定的任何自定义逻辑。

Flipping.js入门

根据您的需求,有几种不同的Flipping软件包:

  • flipping.js:微小且低级;仅在元素范围更改时发出事件
  • flipping.web.js:使用WAAPI为过渡设置动画
  • flipping.gsap.js:使用GSAP为过渡设置动画
  • 更多适配器即将推出!

您可以直接从unpkg获取缩小的代码:

或者,您可以npm install flipping --save将其导入到您的项目中:

// import not necessary when including the unpkg scripts in a <script src="..."> tag
import Flipping from 'flipping/adapters/web';

const flipping = new Flipping();

// First: let Flipping read all initial bounds
flipping.read();

// execute the change that causes any elements to change bounds
doSomething();

// Last, Invert, Play: the flip() method does it all
flipping.flip();

处理由于函数调用而导致的FLIP转换是一种常见的模式,该.wrap(fn)方法通过首先调用.read(),然后获取函数的返回值,然后调用.flip(),然后返回return来透明地包装(或“修饰”)给定的函数。值。这导致更少的代码:

const flipping = new Flipping();

const flippingDoSomething = flipping.wrap(doSomething);

// anytime this is called, FLIP will animate changed elements
flippingDoSomething();

这是一个flipping.wrap()用于轻松实现字母转换效果的示例。单击任意位置以查看效果。
demo

将Flipping.js添加到现有项目

另一篇文章中,我们使用有限状态机创建了一个简单的React Gallery应用程序。它可以按预期工作,但是UI可以在状态之间使用一些平滑的过渡,以防止“跳跃”并改善用户体验。让我们将Flipping.js添加到我们的React应用程序中以完成此操作。(请记住,Flipping.js与框架无关。)

步骤1:初始化Flipping.js

Flipping实例将驻留在React组件本身上,因此它仅与该组件内发生的更改隔离。通过在componentDidMount生命周期挂钩中进行设置来初始化Flipping.js :

 componentDidMount() {
    const { node } = this;
    if (!node) return;
    
    this.flipping = new Flipping({
      parentElement: node
    });
    
    // initialize flipping with the initial bounds
    this.flipping.read();
  }

通过指定parentElement: node,我们告诉Flipping只data-flip-key在render中查找带有的元素App,而不是整个文档。
然后,使用data-flip-key属性(类似于React的keyprop)修改HTML元素,以标识唯一和“共享”的元素:

 renderGallery(state) {
    return (
      <section className="ui-items" data-state={state}>
        {this.state.items.map((item, i) =>
          <img
            src={item.media.m}
            className="ui-item"
            style={{'--i': i}}
            key={item.link}
            onClick={() => this.transition({
              type: 'SELECT_PHOTO', item
            })}
            data-flip-key={item.link}
          />
        )}
      </section>
    );
  }
  renderPhoto(state) {
    if (state !== 'photo') return;
    
    return (
      <section
        className="ui-photo-detail"
        onClick={() => this.transition({ type: 'EXIT_PHOTO' })}>
        <img
          src={this.state.photo.media.m}
          className="ui-photo"
          data-flip-key={this.state.photo.link}
        />
      </section>
    )
  }

通知如何img.ui-itemimg.ui-photo由下式表示data-flip-key={item.link},并data-flip-key={this.state.photo.link}分别为:当上的用户点击img.ui-item,即item设定为this.state.photo,使.link数值将是相等的。

并且由于它们相等,所以翻转将从img.ui-item缩略图平滑过渡到较大img.ui-photo

现在,我们需要做两件事:

  1. this.flipping.read()每当组件_将_更新时调用
  2. this.flipping.flip()每当组件_确实_更新时调用

你们中的一些人可能已经猜到了这些方法调用的发生位置:componentWillUpdate分别是componentDidUpdate和:

 componentWillUpdate() {
    this.flipping.read();
  }
  
  componentDidUpdate() {
    this.flipping.flip();
  }

而且,就像这样,如果您使用的是Flipping适配器(例如flipping.web.jsflipping.gsap.js),则Flipping将使用a跟踪所有元素,[data-flip-key]并在它们更改时将其平滑过渡到新的边界。这是最终结果:

demo

如果您想自己实现自定义动画,则可以将其flipping.js用作简单的事件发射器。阅读文档以获取更多高级用例。

Flipping.js及其适配器默认处理共享元素和父子转换,以及:

  • 转换中断(在适配器中)
  • 输入/移动/离开状态
  • 插件对插件的支持,例如mirror,它允许新输入的元素“镜像”另一个元素的运动
  • 以及将来的更多计划!

资源资源

类似的库包括:

  • 保罗·刘易斯本人撰写的FlipJS,它处理简单的单元素FLIP转换
  • React-Flip-Move,Josh Comeau有用的React库
  • BarbaJS,不一定是FLIP库,而是一个允许您在不同URL之间添加平滑过渡而无需页面跳转的库。

更多资源:


雾岛听风
11.9k 声望8.6k 粉丝

丰富自己,胜过取悦别人。