前言
当页面中存在大量诸如页面拖动等动画时,手动处理手势事件,并将其绑定到诸如div等元素的属性将是一件费力且低效的工作。而react-use-gesture和react-spring分别对手势和动画进行了抽象,为使用react hooks创建交互式动画提供了一种更为便捷的解决方案。
本文将会先简要介绍使用react-use-gesture和react-spring创建交互式动画的方式,以及简要的实现解析。然后描述在开发过程中遇到的问题(兼容性问题&接口问题)及其解决方案。
react-use-gesture
简介
react-use-gesture
对浏览器的用户输入事件进行了封装,提供了统一的手势接口,使复杂的手势(如:拖动,缩放)易于配置。
提供了原生事件所没有的属性(如:速度,距离),丰富了事件所包含的信息。
使用
例子
一个典型的例子如下所示。使用手势hook中注册回调函数,回调函数负责处理事件并触发side effects。hook会返回一个bind函数,通过调用该函数,将其绑定到react节点上。
1 | const bind = useDrag(state => doSomethingWith(state), config) |
其中:
- 手势hook传入的第一个参数是回调函数,第二个参数是手势配置。
- bind函数中传入参数的方式,常用于给多个节点绑定同一个回调函数,详见这个例子。
手势hook
目前支持的有普通手势:useDrag
,useMove
,useHover
,useScroll
,useWheel
,usePinch
,以及复合手势:useGesture
。详细功能见列表。
需要区分:
useDrag,useMove和useHover
drag事件只有在用户使用控制器(鼠标 / 触控屏)拖动(按压 / 触摸)时触发;而move事件会在控制器hover(onPointerMove)时即触发。而hover事件只处理控制器进入和离开事件(onPointerEnter & onPointerLeave)
useWheel和useScroll
wheel事件只会被鼠标触发。scroll事件只有在元素真实发生滚动的时候才会触发;而鼠标在元素上滑动滚轮就可以触发wheel事件。
手势配置
比较常用的配置项有:domTarget / eventOptions,以及用于控制手势范围的 bounds / distanceBounds / angleBounds / rubberband,用于控制swipe的 swipeDistance / swipeVelocity / swipeDuration 等。详细内容可以在这里找到。
其中:
- domTarget用于替代bind函数,直接给dom ref绑定事件。当需要设置事件为passive来提升性能时,必须使用domTarget。
事件
recognizer会给事件回调传入丰富的事件信息,诸如:movement,offset,velocity等。详见文档。
其中较为常用的几个属性为:
movement和offset的区别
movement是单次拖动过程中手势拖动的向量,offset是在一个节点上所有手势拖动的向量和。
memo
用于记忆用户的自定义事件属性
cancel
取消当前事件
event
原始事件
注意
- 为了避免和浏览器默认的拖动产生冲突(如:图片和链接的默认拖动行为),需要设置css:
touch-action: none
。同时,如需兼容firefox,还需要prevent default,详见这里。
实现
初始化
- 用户调用接口,传入handlers和config
- 实例化controller
- 实例化接口对应的recognizer,将handlers传入recognizer
- 将recognizer需要注册的事件列表注册到dom
回调
- 当recognizer监听到对应的手势时,触发handlers
性能优化
Event delegation
在react-use-gesture提供了更加丰富的事件信息的同时,也增加了每次事件计算信息的性能消耗。通过事件代理对事件进行统一处理可以优化性能。
react-spring
简介
react spring提供了基于弹簧物理模拟的动画。它基于弹簧模型而不是常见的曲线 / 时长模型(尽管API中也支持指定动画时长),使其的动画效果更为自然。因为基于物理模型的动画将是连贯且更具交互性的。
Andy Matuschak (ex Apple UI-Kit developer) expressed it once: Animation APIs parameterized by duration and curve are fundamentally opposed to continuous, fluid interactivity.
使用
例子
React-spring的使用主要分为三步:
- 使用动画hook配置动画并获得返回的style属性和set函数
- 将style属性传入animated组件
- 使用set函数更新动画
1 | import {useSpring, animated} from 'react-spring' |
注意:
- set函数并不是直接设置style,而是提供了一个目标,而useSpring会自己计算出下一帧该如何变化以接近目标。
- App函数并不会在set之后重新执行,因为style实际上是mutable的。
- 另外一种更新动画的方式是在useSpring中直接传入新的值,但例子中使用的方法性能更优。
动画hook
hook | 描述 |
---|---|
useSpring | 动画化传入的参数 |
useSprings | 创建多个动画,使用各自的动画配置,用于静态列表 |
useTrail | 创建多个动画,使用同一个配置,每一个动画将会跟随前一个动画 |
useTransition | 组件出现和消失的动画 |
useChain | 串联动画,下一个动画会在上一个结束后开始 |
除了hook之外,react-spring还提供了动画组件,如:Spring,Trail等。参数与hook相似,这里不再赘述。
动画属性
字段类型(number + string)
reat-spring不仅仅支持例子中的数字类型的动画,也支持字符串类型(如:transform,color)的动画。详见Up-front interpolation。
插值(interpolate)
同时,react-spring也支持使用插值函数将一个更新后的数值映射到真实的动画属性。它允许用户重复使用一个计算结果,将其应用到多个动画属性上,提高了性能。一个例子可以在这里找到。
更新动画
使用hook返回的set函数更新目标,参数同hook参数一致。见properties。
from / to …
-
通过 mass / tension / friction 设置基于物理模拟的动画,或使用duration来设置基于时长的动画。
或者可以使用config.default / config.slow … 等预设动画。
实现
style:通过将css属性与动画组件直接绑定,跳过了业务组件的重新渲染,提高性能。
监听动画属性:React-spring的hooks返回的style属性并不是简单的
CSSProperties
类型的数据,而是SpringValues
类型。其中每个SpringValue
通过fluids库的addFluidObserver
函数监测变化。实现类似mobx中observable的效果。当style被传入animated组件时,getAnimatedState
函数会提取props中的fluidValue并添加到依赖集中,并为每个依赖通过addFluidObserver
监听变化。更新:当
SpringValue
更新时,会被加入一个Set
。在每个动画帧(使用rafz
库的onFrame
函数,调用requestAnimationFrame)的最后,这些更新会被读取并通过flushCalls
函数应用到组件上。
兼容性问题
Pointer event
问题
由于react-use-spring使用了pointer event,导致在低版本的浏览器中将无法使用。
解决方案
所幸pepjs提供了pointer event的polyfill方案,而其使用方式也十分简单:
安装pepjs
1
npm install pepjs
在代码中引入
1
import 'pepjs'
在对应的节点设置touch-action属性(浏览器为了优化性能,不指定touch-action默认不触发事件)
1
<button id='test' touch-action="none">Test button!</button>
注册事件
react
1
2
3export function Pointable() {
return <div touch-action="none" onPointerDown={(e) => console.log(e)} />
}DOM
1
document.getElementById("test").addEventListener("pointerdown", function(e) {console.log(e)}
注意:在使用pepjs时,当用户的触摸操作需要用到浏览器默认的行为(如:scroll)时,同样需要指定touch-action(如:touch-action=’auto’)。
IOS 13.0 & 13.1
问题
在IOS13.x及之后的版本中,移动端的safari&chrome开始支持pointer event。但是对于13.0 & 13.1,官方对于pointer event的实现(pointerevent.buttons的值)仍然有问题,见can i use。在这两个版本中,由于:
- pepjs不再polyfill
- react-use-gesture的实现依赖于pointerevent.buttons的值
造成了在13.0 & 13.1产生了一个pointerevent.buttons polyfill的真空地带,导致react-use-gesture失效。这个问题在issue中也有描述。
解决方案
因为这是pointer event实现的bug,react-use-gesture并不准备通过为这两个版本修改代码。因此我们需要自己fork一个react-use-gesture:
路径:./src/recognizers/DragRecognizer.ts
1 | onDragChange = (event: PointerEvent): void => { |
注意
修改后的库会有一个问题,即:使用鼠标拖动移出浏览器窗口后释放鼠标,再移入时,在IOS13.0和IOS13.1中,将继续处于拖动状态。
踩坑
react-spring v9 资源汇总
React-spring目前(2021/02/07)最新的稳定版本是8.0.27,最新的release candidate是9.0.0-rc.3。但是由于官网的文档仍然是v8的版本,且我们在使用v9时遇到了很多奇怪的问题,并不建议使用v9。
如果你仍然想要使用v9,以下是一些文档的链接:
react-spring v9 常见问题及解决方式
Q:在开发环境无问题,部署环境出现TypeError(如:
Uncaught TypeError: r.willAdvance is not a function
)A:因为v9 - rc.3在package.json中声明了
sideEffect: ture
。webpack时会进行剪枝(tree shaking)操作,导致报错(官方表示会在rc.4修复)。修复方法:1
2
3
4"scripts": {
"react-spring-issue-1078": "find node_modules -path \\*@react-spring/\\*/package.json -exec sed -i.bak 's/\"sideEffects\": false/\"sideEffects\": true/g' {} +",
"postinstall": "npm run react-spring-issue-1078",
}Q:useTransition / Transition 中各种不符合预期的表现
A:首先检查是否使用了v9新的API格式。然后尝试在items参数中传入数组而不是其他类型。尽管在文档的示例中有很多非数组的例子,但这些都不是标准的使用方法,在v9中可能导致各种预期之外的动画效果。
Q:useTransition / Transition 中的 Multi-stage transitions 的stage数量和设定不符
A:检查是否两个阶段设定的状态没有变化,如:
1
2
3
4
5leave={[
{opacity: 1},
{opacity: 1},
{opacity: 0},
]}上图的设定会导致中间的stage被无视,实际效果不是
opacity: 1->1->0
,而是opacity: 1->0
。一个备用方案是设定用户无法感知的变化,如:1
2
3
4
5leave={[
{opacity: 1},
{opacity: 0.99},
{opacity: 0},
]}