# 共享元素动画
原生 App
中我们常见到这样的交互,如从商品列表页进入详情页过程中,商品图片在页面间飞跃,使得过渡效果更加平滑,另一个案例是朋友圈的图片预览放大功能。在 Skyline
渲染模式下,我们称其为共享元素动画,可通过 share-element
组件来实现。
在连续的 Skyline
页跳转时,页面间 key
相同的 share-element
节点将产生飞跃特效,开发者可自定义插值方式和动画曲线。通常作用于图片,为保证动画效果,前后页面的 share-element
子节点结构应该尽量保持一致。
# 立即体验
扫码打开小程序示例,交互动画 - 基础组件 - 共享元素动画 即可体验。
# 使用方法
属性 | 类型 | 默认值 | 必填 | 说明 | 最低版本 |
---|---|---|---|---|---|
key | string | 是 | 映射标记,页面内唯一 | 2.29.2 | |
transition-on-gesture | boolean | false | 否 | 手势返回时是否进行动画 | 2.29.2 |
shuttle-on-push | string | to | 否 | 指定 push 阶段的飞跃物 | 2.30.2 |
shuttle-on-pop | string | to | 否 | 指定 pop 阶段的飞跃物 | 2.30.2 |
rect-tween-type | string | materialRectArc | 否 | 动画插值曲线 | 2.30.2 |
on-frame | worklet callback | 否 | 动画帧回调 | 2.30.2 |
假定 A
页和 B
页存在对应的 share-element
组件
push
阶段:通过wx.navigateTo
由A
进入B
,称A
为源页面(from
页),B
为目标页(to
页)pop
阶段:通过wx.navigateBack
由B
返回A
,此时B
为源页面 (from
页),A
为目标页(to
页)
# 指定飞跃物
开发者可以指定选定源页面或目标页的 share-element
作为飞跃物。
由于涉及两个页面的组件,这里以目标页 share-element
组件指定的属性为准
push 阶段
:默认采用B
页的share-element
组件进行飞跃,设置属性shuttle-on-push=from
可切换成A
页的。pop 阶段
:默认采用A
页的share-element
组件,设置属性shuttle-on-pop=from
可切换成B
页的。
需注意的是 on-frame
回调总是在指定为飞跃物的 share-element
组件上触发。
# 动画帧回调
共享元素动画就是以源页面 share-element
所在矩形框为起点,目标页 share-element
所在矩形框为终点,进行插值计算的过程,动画时长与页面路由时间一致。
enum FlightDirection {
push = 0,
pop = 1
}
interface Rect {
top: number,
right: number,
bottom: number,
left: number,
width: number,
height: number
}
interface ShareElementFrameData {
// 动画进度,0~1
progress: number,
// 动画方向,push | pop
direction: FlightDirection
// 源页面 share-element 容器尺寸
begin: Rect,
// 目标页 share-element 容器尺寸
end: Rect,
// 由框架计算的当前帧飞跃物容器尺寸
current: Rect,
}
type ShareElementOnFrameCallback = (data: ShareElementFrameData) => undefined | Rect
开发者可通过 on-frame
回调来自定义插值方式。ShareElementFrameData
中包含了始末位置,以及框架按照指定的动画曲线 rect-tween-type
和当前进度 progress
计算的 current
位置。
默认插值方式为对 Rect
的 LTWH
分别进行线性插值。当 on-frame
返回 undefined
时,当前帧飞跃物的实时位置由 current
决定,开发者可返回按其他方式计算的 Rect
对象进行改写。
const lerp = (begin: number, end: number, t: number) => {
'worklet'
return begin + (end - begin) * t
}
const lerpRect = (begin: Rect, end: Rect, t: number) => {
'worklet'
const left = lerp(begin.left, end.left, t);
const top = lerp(begin.top, end.top, t);
const width = lerp(begin.width, end.width, t);
const height = lerp(begin.height, end.height, t);
const right = left + width
const bottom = top + height
return {
left,
top,
right,
bottom,
width,
height
}
}
# 动画插值曲线
除了可以自定义插值方式外,还可以自定义动画曲线,默认的动画曲线为 cubic-bezier(0.4, 0.0, 0.2, 1.0),记作 fastOutSlowIn
。
当 rect-tween-type
设置为如下类型时,默认的插值方式为对 Rect
的 LTWH
分别进行线性插值。
- linear
- elasticIn
- elasticOut
- elasticInOut
- bounceIn
- bounceOut
- bounceInOut
- cubic-bezier(x1, y1, x2, y2)
此外,rect-tween-type
还支持两类特殊的枚举值,对于这两个值,动画曲线仍是 fastOutSlowIn
,但插值方式有所不同,运动轨迹为弧线。
- materialRectArc:矩形对角动画
- materialRectCenterArc:径向动画
# 工作原理
下面以 push
过程为例介绍共享元素动画的各个阶段。
注意:实际上动画过程中 share-element
节点自身不进行动画,移动的是其子节点。
# 动画开始前
目标页还未渲染,在所有页面之上创建 overlay
层,飞跃物将在 overlay
层进行动画。
# 动画开始时刻
调用 wx.navigateTo
推入新页面时动画触发,progress = 0
时刻发生如下动作
- 目标页首帧渲染,框架计算出目标
share-element
节点位置大小。 - 源
share-element
节点离屏(不显示)。 - 将目标
share-element
节点移至overlay
层,作为飞跃物,位置大小同源share-element
节点。
# 动画过程中
根据指定的插值方式和动画曲线,目标 share-element
在 overlay
层进行动画,从起点向终点位置过渡。
# 动画结束时刻
progress = 1
时刻,将目标 share-element
从 overlay
层移动到目标页面,出现在终点位置。
# 注意事项
Skyline
版本share-element
无需结合page-container
使用- 给
share-element
组件设置padding
、justify-content
等影响子节点布局的样式将无法生效 - 可结合页面生命周期 onRouteDone 在路由完成时刻做一些状态恢复工作
# Q & A
# Q1: 设置了相同的 key,但没有看到飞跃动画
共享元素动画需保证下一个页面首帧即创建好 share-element
节点,并设置了 key,用于计算目标位置。
如果是通过 setData
设置的,可能会错过首帧。针对这种情况,可以 使用 Component 构造器构造下一个页面,只要在组件 attached
生命周期前(含)通过 setData
设置上去,就会在首帧渲染。
# Q2: 飞跃过程中,子节点不会跟随放大/缩小
动画过程中,飞跃物容器会不断变化大小和位置,如果子节点想自适应跟随变化,就需要通过百分比布局,而非写死固定宽高。
<!-- 由于子 view 固定大小,飞跃过程中仅位置发生变化,大小不变 -->
<share-element key="portrait">
<view style="width: 50px; height: 50px;"></view>
</share-element>
<!-- 由于子 view 设置跟父节点一样大,飞跃过程中位置、大小均会改变 -->
<share-element key="portrait" style="width: 50px; height: 50px;">
<view style="width: 100%; height: 100%;"></view>
</share-element>
# Q3: 多个 share-element 一起动画,覆盖层级问题
飞跃物在 overlay
层进行动画,层级在所有页面之上。飞跃物间按其在页面组件树的定义顺序(DFS
遍历),越往后的层级越高。
# Q4: 自定义路由手势返回时,未看到返回动画
首先须给组件设置 transition-on-gesture=true
属性。同时自定义路由手势返回时,只有调用 startUserGesture
接口后才会触发共享元素动画。
# Q5: 共享元素动画与路由动画关系
共享元素跟随页面路由动画一同开始和结束。如果设置自定义路由的页面进入曲线和 rect-tween-type
一致,则 onFrame
返回的 progress
值也与 PrimaryAnimation.value
的值始终保持一致。
# 示例用法
# 基础用法
商品列表页
<block wx:for="{{list}}" wx:key="id">
<share-element key="box" transition-on-gesture>
<image
src="{{src}}"
mode="aspectFill"
/>
</share-element>
</block>
商品详情页
<share-element key="box" transition-on-gesture>
<image
src="{{src}}"
mode="aspectFit"
/>
</share-element>
可在开发者工具中体验效果,这里需要注意图片 mode
不同带来的影响。
# 进阶用法
以仿朋友圈图片预览放大功能为例,介绍通过帧回调解决图片 mode
不同带来的跳变问题。相关功能已封装成 aniamted-image
组件,开发者可在此基础上进行修改。
这里简要介绍核心思路:
- 始终以
list
页share-element
为飞跃物 - 通过
on-frame
改写飞跃物容器的位置大小,使其充满overlay
层 - 以图片实际占据空间确定始末容器的位置大小,而不是
share-element
节点占据的空间 - 飞跃过程中使用缩略图,高清图下载完成后进行替换
默认计算方式,以 share-element
节点占据空间确定始末位置,会产生跳变。
以图片实际占据空间确定始末位置,始终采用单一 mode
效果,不会产生跳变。
使用最新 nightly
工具预览,移动端安卓 8.0.33 版本。