【第1114期】打造高性能剪切动画

2023-10-17 16:10

本文主要是介绍【第1114期】打造高性能剪切动画,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

今天开始大部分地区都降温了,而且降的幅度很大。今日早读文章由布谷前端@程松翻译分享。

正文从这开始~

在谷歌的产品中我们经常会看到各种各样的交互动画,对于前端开发来说实现动画效果也是经常遇到且头疼的事情,原作者 Paul Lewis 是谷歌的一名工程师,在这篇文章中我们将探讨如何实现如下的交互设计

上面的交互效果是将一个菜单分为收起状态和展开状态,在收起状态下只展现收起状态下的一部分,所以我们可以看成收起状态是把展开态的一部分给剪切掉了,这也是为什么原文中作者使用 animating clips 来描述这种交互形式。

方案1:通过动画修改容器的宽高

我们定义好菜单收起和展开两种状态下的 width 和 height 属性,并且给元素加上 transition 过渡,通过切换元素的 class 实现动画效果。具体的实现代码如下:

.menu {overflow: hidden;width: 350px;height: 600px;transition: width 600ms ease-out, height 600ms ease-out;}.menu--collapsed {width: 200px;height: 60px;}

这个方法简单粗暴见效快,但对于老司机来说这并不是一个满意的方案,因为在修改元素的样式属性时浏览器会重新渲染页面,对 width 和 height 属性而言,每一次修改之后页面都需要进行重排,这个渲染过程开销相对而言比较大,所以动画很难达到60fps。

方案2:使用 CSS clip 或 clip-path 属性

除了上面说的修改宽和高之外我们还可以使用 clip 属性(已经废弃)来实现。当然,因为 clip 已经弃用了,我们应该使用 clip-path 属性搞定,但 clip-path 目前的浏览器兼容性并不好,所以目前使用clip 或 clip-path 也不是一个完美的解决方案。

.menu {position: absolute;clip: rect(0px 112px 175px 0px);transition: clip 600ms ease-out;}.menu--collapsed {clip: rect(0px 70px 34px 0px);}

这个方案性能上比方案1要好,但仍旧会在每一帧触发重绘。并且 clip 属性需要元素是 absolute 或 fixed 定位,这样可能会让 CSS 代码略麻烦。

方案3:CSS Animating 和 scale

方案1修改的是元素的大小,方案2是裁剪元素的可视区域,现在我们换一种思路。

首先从效果上看我们可以认为菜单的收起态是展开态缩放而来的,也就是说我们将展开态高度所放到原来的 1/5 就是收起态的高度。我们对元素做 transform: scale(0.2, x)(这里没考虑宽度的缩放比例) 操作,效果如下

现在菜单的高度缩小为原来的 1/5,但是问题就是菜单的内容也被相应的缩小了,要解决这个问题很简单,我们直接把菜单的内容放大 5 倍即可。

所以方案三其实是在容器上设置一个缩放,然后在内容上设置一个反向缩放(counter-scale),这样就可以保证在容器收起或展开的时候菜单里的内容不会被压缩或者放大。这个方法逻辑上稍显麻烦,但是修改 transfrom 属性不会触发重排或者重绘,并且可以利用 GPU 进行运算,因此可以达到更高的性能从而达到更高的帧率。

下面详细说一下实现步骤:

第一步:计算开始和结束状态

首先要得到菜单收起和展开状态下的大小,由此来计算出我们的缩放比例。当然,我们并不能一次取到菜单收起和展开状态下的大小,因此需要先切换 class 来改变元素的状态,然后获取到对应状态下的大小(或者将菜单第一个子元素的大小看做收起态的大小,下面代码就是这样做的)。

需要注意的是当我们执行 getBoundingClientRect()(或者 offsetWidth 和 offsetHeight)时,如果页面当前样式有变化,浏览器会强制重排。

function calculateCollapsedScale () {// The menu title can act as the marker for the collapsed state.const collapsed = menuTitle.getBoundingClientRect();// Whereas the menu as a whole (title plus items) can act as// a proxy for the expanded state.const expanded = menu.getBoundingClientRect();return {x: collapsed.width / expanded.width,y: collapsed.height / expanded.height  }}

菜单默认状态下是展开的,通过上面的代码可以得到菜单收起态和展开态的缩放比例,然后使用 JavaScript 修改元素的 tansform 属性,这样就可以实现展开和收起动画。

.menu {will-change: transform;transition: 200ms linear;}
var { x, y } = calculateCollapsedScale();var invX = 1 / x;var invY = 1 / y;function toggle() {if (menu.classList.contains('expanded')) {collapse();return;}expand();}function collapse() {  menu.classList.remove('expanded');  menu.style.transform = `scale(${x}, ${y})`;  menuContents.style.transform = `scale(${invX}, ${invY})`;}function expand() {  menu.classList.add('expanded');  menu.style.transform = `scale(1, 1)`;  menuContents.style.transform = `scale(1, 1)`;}menuBtn.addEventListener('click', toggle);


从效果看可以发现虽然菜单的收起态和展开态没有问题,但是在变换过程中内容看起来变形了。因为我们是通过容器的缩放和内容的反向缩放(counter-scale)来实现动画的,要保证内容在动画过程中不变形,就要保证容器的缩放比例和内容的缩放比例在动画过程中始终保持相乘等于1。我们用高度举例,收起态的高度是展开态的 1/5(0.2),所以收起态下内容的高度是放大了5倍,那么展开动画就是容器高度缩放比例从 0.2 -> 1,内容高度缩放比例从 5 -> 1。假设缓动函数是线性的,我们把过程分成五个阶段,用表格表示:

容器高度缩放比例内容高度缩放比例0.250.440.630.8211
所以我们可以看到,在整个动画过程中,容器高度缩放比例 * 内容高度缩放比例 > 1,这也就是解释了为什么我们看到的动画中内容变形了。

第二步:构造CSS动画

针对上面的问题,我们可以使用 CSS 动画解决,因为 CSS 动画是基于关键帧,因此我们可以将动画过程分成 100 个关键帧,计算出每一个关键帧下容器和内容的缩放比例,并保证在这 100 个关键帧中这两个比例相乘始终等于 1,接着将这 100 个关键帧拼接成 CSS 动画并插入到页面中给元素调用。这样我们就可以保证在动画过程中不会出现变形,并且一开始就完成所有的计算量,避免了在 JavaScript 中动态计算,从而避免了由于 JavaScript 阻塞导致的动画卡顿。

将 CSS 动画插入到页面中会导致浏览器重新渲染页面样式,但这个过程只会在最开始执行一次,所以影响并不大。下面是通过 JavaScript 生成 CSS 动画的代码

function createKeyframeAnimation () {// Figure out the size of the element when collapsed.let {x, y} = calculateCollapsedScale();let animation = '';let inverseAnimation = '';for (let step = 0; step <= 100; step++) {// Remap the step value to an eased one.let easedStep = ease(step / 100);// Calculate the scale of the element.const xScale = x + (1 - x) * easedStep;const yScale = y + (1 - y) * easedStep;animation += `${step}% {transform: scale(${xScale}, ${yScale});}`;// And now the inverse for the contents.const invXScale = 1 / xScale;const invYScale = 1 / yScale;inverseAnimation += `${step}% {transform: scale(${invXScale}, ${invYScale});}`;}return `@keyframes menuAnimation {${animation}}@keyframes menuContentsAnimation {${inverseAnimation}}`;}

代码中的 ease() 表示缓动函数,在实际应用中,我们可以如下一样自己定义一个,或者使用 Tween.js 这样的项目。

function ease (v, pow=4) {return 1 - Math.pow(1 - v, pow);}

震惊!谷歌竟然能看函数曲线!So Google It!

第三步:执行 CSS 动画

通过前两步我们已经生成了 CSS 动画并且插入到了页面中,接下来我们通过切换元素的 class 来触发动画。

.menu--expanded {animation-name: menuAnimation;animation-duration: 0.2s;animation-timing-function: linear;}.menu__contents--expanded {animation-name: menuContentsAnimation;animation-duration: 0.2s;animation-timing-function: linear;}

结合上面的 CSS 代码,使用 JavaScript 切换元素的 class 就可以触发元素执行动画。这里要注意在 JavaScript 中我们已经引入了缓动函数,所以在 CSS 代码中需要将 animation-timing-function 属性设置为 linear,如果设置成其他值的就相当于在我们原先的缓动基础上再叠加一次缓动效果。

现在我们搞定了展开动画,对于收起动画可以将展开动画逆向执行,这个方法看起来是没有问题的,但是因为是完全逆向执行的,所以假设我们展开动画缓动函数是 ease-out ,那么收起动画看起来就是 ease-in,所以视觉效果上会有一些区别。所以更好的方案是在 JavaScript 中生成两套 CSS 动画,分别是展开动画和收起动画,这样就可以保证两个动画的关键帧是遵循同样的缓动函数。

const xScale = 1 + (x - 1) * easedStep;const yScale = 1 + (y - 1) * easedStep;

再进一步:圆形菜单

现在可以用同样的原理实现一个圆形的菜单动画,其实代码层面基本是一致的。只是圆形这种情况下我们需要设置一个 border-raidu: 50% 来实现圆形,然后用一个设置了 overflow: hidden 的元素将菜单包裹起来,这样就可以在菜单展开后看起来是一个矩形。

当然,这里还涉及一些其他样式上的修改,因为在圆形彩蛋动画中变换中心并不是左上角或者中心,而且点击后加号按钮会消失,所以还需要注意这些细节。在低 DPI 的屏幕上 Chrome 浏览器会出现文字模糊的 bug,具体的 bug 说明在这里。圆形菜单动画代码戳这里;

总结

现在我们已经实现了一种高性能动画方案,原理是将容器和内容同时缩放并且保证两个缩放比例相乘等于 1。具体的过程是先通过 JavaScript 计算出 100 个关键帧并拼接成 CSS 动画,然后触发元素执行 CSS 动画即可。

如果浏览器支持 Web Animation 的话可以直接调用 Web Animation API 执行动画,但问题是 Web Animation 的兼容性不太好,所以使用的话需要做如下兼容处理。

if ('animate' in HTMLElement.prototype) {// Animate with Web Animations.} else {// Fall back to generated CSS Animations or JS.}

想要看具体的实现代码,移步 UI Element Samples Github 仓库。

相关文章

  • infinite scroll

  • performant parallaxing

最后,为你推荐:

【第1001期】构建高性能展开&收缩动画


【第892期】功能性动画如何提升用户体验


关于本文

译者:@程松

译文:https://zhuanlan.zhihu.com/p/26519940

作者:@Paul Lewis、@Stephen McGruer

原文:https://developers.google.com/web/updates/2017/03/performant-expand-and-collapse


【第1107期】iPhone X 适配 手Q H5页面通用解决方案

【第1106期】Element 中的键盘可访问性

这篇关于【第1114期】打造高性能剪切动画的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/226489

相关文章

Python使用Tkinter打造一个完整的桌面应用

《Python使用Tkinter打造一个完整的桌面应用》在Python生态中,Tkinter就像一把瑞士军刀,它没有花哨的特效,却能快速搭建出实用的图形界面,作为Python自带的标准库,无需安装即可... 目录一、界面搭建:像搭积木一样组合控件二、菜单系统:给应用装上“控制中枢”三、事件驱动:让界面“活”

基于Python+PyQt5打造一个跨平台Emoji表情管理神器

《基于Python+PyQt5打造一个跨平台Emoji表情管理神器》在当今数字化社交时代,Emoji已成为全球通用的视觉语言,本文主要为大家详细介绍了如何使用Python和PyQt5开发一个功能全面的... 目录概述功能特性1. 全量Emoji集合2. 智能搜索系统3. 高效交互设计4. 现代化UI展示效果

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.

使用WPF实现窗口抖动动画效果

《使用WPF实现窗口抖动动画效果》在用户界面设计中,适当的动画反馈可以提升用户体验,尤其是在错误提示、操作失败等场景下,窗口抖动作为一种常见且直观的视觉反馈方式,常用于提醒用户注意当前状态,本文将详细... 目录前言实现思路概述核心代码实现1、 获取目标窗口2、初始化基础位置值3、创建抖动动画4、动画完成后

Nginx使用Keepalived部署web集群(高可用高性能负载均衡)实战案例

《Nginx使用Keepalived部署web集群(高可用高性能负载均衡)实战案例》本文介绍Nginx+Keepalived实现Web集群高可用负载均衡的部署与测试,涵盖架构设计、环境配置、健康检查、... 目录前言一、架构设计二、环境准备三、案例部署配置 前端 Keepalived配置 前端 Nginx

使用animation.css库快速实现CSS3旋转动画效果

《使用animation.css库快速实现CSS3旋转动画效果》随着Web技术的不断发展,动画效果已经成为了网页设计中不可或缺的一部分,本文将深入探讨animation.css的工作原理,如何使用以及... 目录1. css3动画技术简介2. animation.css库介绍2.1 animation.cs

C#实现高性能Excel百万数据导出优化实战指南

《C#实现高性能Excel百万数据导出优化实战指南》在日常工作中,Excel数据导出是一个常见的需求,然而,当数据量较大时,性能和内存问题往往会成为限制导出效率的瓶颈,下面我们看看C#如何结合EPPl... 目录一、技术方案核心对比二、各方案选型建议三、性能对比数据四、核心代码实现1. MiniExcel

基于Python打造一个智能单词管理神器

《基于Python打造一个智能单词管理神器》这篇文章主要为大家详细介绍了如何使用Python打造一个智能单词管理神器,从查询到导出的一站式解决,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 项目概述:为什么需要这个工具2. 环境搭建与快速入门2.1 环境要求2.2 首次运行配置3. 核心功能使用指

利用Python打造一个Excel记账模板

《利用Python打造一个Excel记账模板》这篇文章主要为大家详细介绍了如何使用Python打造一个超实用的Excel记账模板,可以帮助大家高效管理财务,迈向财富自由之路,感兴趣的小伙伴快跟随小编一... 目录设置预算百分比超支标红预警记账模板功能介绍基础记账预算管理可视化分析摸鱼时间理财法碎片时间利用财

Python结合PyWebView库打造跨平台桌面应用

《Python结合PyWebView库打造跨平台桌面应用》随着Web技术的发展,将HTML/CSS/JavaScript与Python结合构建桌面应用成为可能,本文将系统讲解如何使用PyWebView... 目录一、技术原理与优势分析1.1 架构原理1.2 核心优势二、开发环境搭建2.1 安装依赖2.2 验