前言
在我的项目中咱们会写很多小视图到大视图之间的转换动作,比方一个小图到大图的详情,如果不加动画就会是很生硬的小变大的成果,用户体验不是很好
通常的做法咱们会退出动画来缓解这些问题,然而css计划不是通用的解决方案,而且不适宜大块的更改,偏移值也是不好计算
在本文中,咱们将探讨一种称为“ FLIP”的技术,该技术可用于以无效的形式对任何DOM元素的地位和尺寸进行动画解决,而不论其布局是如何计算或出现的(例如,高度,宽度,浮点数) ,相对定位,变换,flexbox,网格等)
为什么应用FLIP技术
你有没有试过动画height
,width
,top
,left
,或任何其余属性,除了transform
和opacity
?您可能曾经留神到,动画看起来有些_简陋_,这是有起因的。当任何触发布局更改的属性(例如height
)时,浏览器必须递归查看其余元素的布局是否因而而产生了更改,这可能会很低廉。如果该计算破费的工夫超过一个动画帧(大概16.7毫秒),则将跳过该动画帧,因为该帧没有及时渲染,因而将导致“凌乱”。在保罗·刘易斯(Paul Lewis)的文章“像素很贵”中,他进一步介绍了如何渲染像素以及各种性能收入。
简而言之,咱们的指标是简短-咱们心愿尽快计算出起码的必要款式更改。这里的要害是惟一的动画transform
和opacity
,和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 changedoSomething();// 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 childconst parentFirst = parentElm.getBoundingClientRect();const childFirst = childElm.getBoundingClientRect();doSomething();// Last: parent and childconst parentLast = parentElm.getBoundingClientRect();const childLast = childElm.getBoundingClientRect();// Invert: parentconst parentDeltaX = parentFirst.left - parentLast.left;const parentDeltaY = parentFirst.top - parentLast.top;// Invert: child relative to parentconst childDeltaX = (childFirst.left - parentFirst.left) - (childLast.left - parentLast.left);const childDeltaY = (childFirst.top - parentFirst.top) - (childLast.top - parentLast.top); // Play: using the WAAPIparentElm.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' });
这里还要留神几件事:
- 家长和孩子(的机会抉择
duration
,easing
等等)也_没有_肯定要达到这种技术。随时施展创造力! - 在本示例中,有目的地省略了更改父级和/或子级(
width
,height
)中的尺寸,因为它是一个高级且简单的主题。让咱们将其保留为另一个教程。 - 您能够将共享元素和父子技术联合应用,以取得更大的灵活性。
应用Flipping.js充沛灵活性
下面的技术看似简单明了,然而一旦您必须跟踪多个元素的转换,它们就会变得很乏味。Android通过以下形式加重了这一累赘:
- 将共享元素过渡到外围SDK
- 容许开发人员通过应用通用
android:transitionName
XML属性来标识共享哪些元素
一个名为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获取放大的代码:
- https://unpkg.com/ flipping @ latest /dist/flipping.js
- https://unpkg.com/ flipping @ latest /dist/flipping.web.js
- https://unpkg.com/ flipping @ latest /dist/flipping.gsap.js
或者,您能够npm install flipping --save
将其导入到您的我的项目中:
// import not necessary when including the unpkg scripts in a <script src="..."> tagimport Flipping from 'flipping/adapters/web';const flipping = new Flipping();// First: let Flipping read all initial boundsflipping.read();// execute the change that causes any elements to change boundsdoSomething();// Last, Invert, Play: the flip() method does it allflipping.flip();
解决因为函数调用而导致的FLIP转换是一种常见的模式,该.wrap(fn)
办法通过首先调用.read()
,而后获取函数的返回值,而后调用.flip()
,而后返回return来通明地包装(或“润饰”)给定的函数。值。这导致更少的代码:
const flipping = new Flipping();const flippingDoSomething = flipping.wrap(doSomething);// anytime this is called, FLIP will animate changed elementsflippingDoSomething();
这是一个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的key
prop)批改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-item
和img.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
。
当初,咱们须要做两件事:
this.flipping.read()
每当组件_将_更新时调用this.flipping.flip()
每当组件_的确_更新时调用
你们中的一些人可能曾经猜到了这些办法调用的产生地位:componentWillUpdate
别离是componentDidUpdate和:
componentWillUpdate() { this.flipping.read(); } componentDidUpdate() { this.flipping.flip(); }
而且,就像这样,如果您应用的是Flipping适配器(例如flipping.web.js
或flipping.gsap.js
),则Flipping将应用a跟踪所有元素,[data-flip-key]
并在它们更改时将其平滑过渡到新的边界。这是最终后果:
demo
如果您想本人实现自定义动画,则能够将其flipping.js
用作简略的事件发射器。浏览文档以获取更多高级用例。
Flipping.js及其适配器默认解决共享元素和父子转换,以及:
- 转换中断(在适配器中)
- 输出/挪动/来到状态
- 插件对插件的反对,例如
mirror
,它容许新输出的元素“镜像”另一个元素的静止 - 以及未来的更多打算!
资源资源
相似的库包含:
- 保罗·刘易斯自己撰写的FlipJS,它解决简略的单元素FLIP转换
- React-Flip-Move,Josh Comeau有用的React库
- BarbaJS,不肯定是FLIP库,而是一个容许您在不同URL之间增加平滑过渡而无需页面跳转的库。
更多资源:
- 动画无动画–约书亚·科莫
- 翻转动画–保罗·刘易斯
- 像素很贵-Paul Lewis
- 通过页面转换改善用户流– Luigi de Rosa
- 用户体验设计的智能过渡– Adrian Zumbrunnen
- 怎样才能实现良好的过渡?–尼克·巴比奇(Nick Babich)
- Google资料设计中的静止准则
- 与React Native共享元素过渡