共计 7203 个字符,预计需要花费 19 分钟才能阅读完成。
前言
在我的项目中咱们会写很多小视图到大视图之间的转换动作,比方一个小图到大图的详情,如果不加动画就会是很生硬的小变大的成果,用户体验不是很好
通常的做法咱们会退出动画来缓解这些问题,然而 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 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'});
这里还要留神几件事:
- 家长和孩子(的机会抉择
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="..."> 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 的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 共享元素过渡