我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
本篇将带你在 Flutter 上快速实现两个炫酷的动画特效,希望最后的效果可以惊艳到你。
这次灵感的来源于更新 MIUI 13 时刚好看到的卡片效果,其中除了卡片会跟随手势出现倾斜之外,内容里的部分文本和绿色图标也有类似悬浮的视差效果,恰逢此时灵机一动,我们也来用 Flutter 快速实现炫酷的 3D 视差卡片,最后再拓展实现一个支持帅气的 360° 展示的卡片效果。
❤️ 本文正在参加征文投稿活动,还请看官们走过路过来个点赞一键三连,感激不尽~
既然需要卡片跟随手势产生不规则形变,我们第一个想到的肯定是矩阵变换,在 Flutter 里我们可以使用 Matrix4
配合 Transform
来实现矩阵变换效果。
开始之前,首先我们创建用 Transform
嵌套一个 GestureDetector
,并绘制出一个 300x400 的圆角卡片,用于后续进行矩阵变换处理。
Transform(
transform: Matrix4.identity(),
child: GestureDetector(
child: Container(
width: 300,
height: 400,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blueGrey,
borderRadius: BorderRadius.circular(20),
),
),
),
);
接着,如下代码所示,因为我们需要卡片跟随手势进行矩阵变换,所以我们可以直接在 GestureDetector
的 onPanUpdate
里获取到手势信息,例如 localPosition
位置信息,然后把对应的 dx
和 dy
赋值到 Matrix4
的 rotateX
和 rotateY
上实现旋转。
child: Transform(
transform: Matrix4.identity()
..rotateX(touchY)
..rotateY(touchX),
alignment: FractionalOffset.center,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
touchX = details.localPosition.dx;
touchY = details.localPosition.dy;
});
},
child: Container(
这里有个需要注意的是:上面代码里 rotateX
使用的是 touchY
,而 rotateY
使用的是 touchX
,为什么要这样做呢?
⚠️ 举个例子,当我们手指左右移动时,是希望卡片可以围绕 Y 轴进行旋转,所以我们会把touchX
传递给了rotateY
,同样touchY
传递给rotateX
也是一个道理。
但是当我们实际运行上述代码之后,如下图所示,可以看到基本上我们只是稍微移动手指,卡片就会陷入疯狂旋转的情况,并且实际的旋转速度会比 GIF 里快很多。
问题的原因其实是因为 rotateX
和 rotateY
需要的是一个 angle
参数,假设这里对 rotateX
和 rotateY
设置 pi / 4
,就可以看到卡片在 X 轴和 Y 轴上都产生了 45 度的旋转效果。
Transform(
transform: Matrix4.identity()
..rotateX(pi / 4)
..rotateY(pi / 4),
alignment: FractionalOffset.center,
所以如果直接使用手势的 localPosition
作用于 Matrix4
肯定是不行的,我们首先需要对手势数据进行一个采样,因为代码里我们设置了 FractionalOffset.center
,所以我们可以用卡片的中心点来计算手指位置,再进行压缩处理。
如下代码所示,我们通过以卡片中心点为原点进行计算,其中 / 2
就是得到卡片的中心点,/ 100
是对数据进行压缩采样,但是为什么 touchX
和 touchY
的计算方式是相反的呢?
touchX = (cardWidth / 2 - details.localPosition.dx) / 100;
touchY = (details.localPosition.dy - cardHeight / 2 ) / 100;
如下图所示,因为在设置 rotateX
和 rotateY
时,赋予 > 0
的数据时卡片就会以图片中的方向进行旋转,由于我们是需要手指往哪边滑动,卡片就往哪边倾斜,所以:
- 当我们往左水平滑动时,需要卡片往左边倾斜,也就是图中绕 Y 轴转动的
>0
的方向,并且越靠近左边需要正向的 Angle 数值越大,由于此时localPosition.dx
是越往左越小,所以需要利用CardWidth / 2 - details.localPosition.dx
进行计算,得到越往左有越大的正向 Angle 数值 - 同理,当我们往下滑动时,需要卡片往下边倾斜,也就是图中绕 X 轴转动的
>0
的方向,并且越靠近下边需要正向 Angle 数值越大,由于此时localPosition.dy
越往下越大,所以使用details.localPosition.dy - cardHeight / 2
去计算得到正确数据
如果觉得太抽象,可以结合上边右侧的动图,和大家买股票一样,图中显示红色时是正数,显示绿色时是负数,可以看到:
- 手指往左移动时,第一行 TouchX 是红色正数,被设置给
rotateY
, 然后卡片绕 Y 轴正方向旋转 - 手指往下移动时,第二行 TouchY 是红色正数,被设置给
rotateX
, 然后卡片绕 X 轴正方向旋转
到这里我们就初步实现了卡片跟随手机旋转的效果,但是这时候的立体旋转效果看起来其实“很别扭”,总感觉差了点什么,其实这是因为卡片在旋转时没有产生视觉上的深度感知。
所以我们可以通过矩阵的透视变换调整视觉效果,而为了在 Z 方向实现深度感知,我们需要在矩阵中配置 .setEntry(3, 2, 0.001)
,这里的 3 表示第 3 列,2 表示第 2 行,因为是从 0 开始排列,所以也就是图片中 Z 的位置。
其实 .setEntry(3, 2, 0.001)
就是调整 Z 轴的视角,而在 Z 上的 0.001 就是需要的透视效果测量值,类似于相机上的对焦点进行放大和缩小的作用,这个数字越大就会让交点处看起来好像离你视觉更近,所以最终代码如下
Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(touchY)
..rotateY(touchX),
alignment: FractionalOffset.center,
运行之后,可以看到在增加了 Z 角度的视角调整之后,这时候看起来的立体效果就好了很多,并且也有了类似 3D 空间的感觉。
接着我们在卡片上放上一个添加一个 13
的 Text
文本,运行之后可以看到此时文本是跟随卡片发生变化,而接下来我们需要做的,就是通过另外一个 Transform
来让 Text
文本和卡片之间产生视差,从而出现悬浮的效果。
所以接下来需要给文本内容设置一个 translate
的 Matrix4
,让它向着倾斜角度的相反方向移动,然后对前面的 touchX
和 touchY
进行放大,然后再通过 - 10
操作来产生一个位差。
Transform(
transform: Matrix4.identity()
..translate(touchX * 100 - 10,
touchY * 100 - 10, 0.0),
-10
这个是我随意写的,你也可以根据自己的需求调节。
例如,这时候当卡片往左倾斜时,文字就会向右移动,从而产生视觉差的效果,得到类似悬浮的感觉。
完成这一步之后,接下来可以我们对文本内容进行一下美化处理,例如增加渐变颜色,添加阴影,更换字体,目的是让字体看起来更加具备立体的效果,这里使用的 shader
,也可以让文字在移动过程中出现不同角度的渐变效果。
最后,我们还需要对卡片旋转进行一个范围约束,这里主要是通过卡片大小比例:
- 在
onPanUpdate
时对touchX
和touchY
进行范围约束,从而约束的卡片的倾斜角度 - 增加了
startTransform
标志位,用于在onTapUp
或者onPanEnd
之后,恢复卡片回到默认状态的作用。
Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(startTransform ? touchY : 0.0)
..rotateY(startTransform ? touchX : 0.0),
alignment: FractionalOffset.center,
child: GestureDetector(
onTapUp: (_) => setState(() {
startTransform = false;
}),
onPanCancel: () => setState(() => startTransform = false),
onPanEnd: (_) => setState(() {
startTransform = false;
}),
onPanUpdate: (details) {
setState(() => startTransform = true);
///y轴限制范围
if (details.localPosition.dx < cardWidth * 0.55 &&
details.localPosition.dx > cardWidth * 0.3) {
touchX = (cardWidth / 2 - details.localPosition.dx) / 100;
}
///x轴限制范围
if (details.localPosition.dy > cardHeight * 0.4 &&
details.localPosition.dy < cardHeight * 0.6) {
touchY = (details.localPosition.dy - cardHeight / 2) / 100;
}
},
child:
到这里,我们只需要在全局再进行一些美化处理,运行之后就会如下图所示,在配合阴影和渐变效果,整体的视觉立体感会更强烈,此时我们基本就实现了一开始想要的功能,
完整代码可见: card_perspective_demo_page.dart
Web 体验地址,PC 端记得开 Chrome 手机模式: 3D 视差卡片 。
那有人可能就想问了: 学会了这个我们还可以实现什么?
举个例子,比如我们可以实现一个 “伪3D” 的 360° 卡片效果,利用堆叠实现立体的电子银行卡效果。
依旧是前面的手势旋转逻辑,只是这里我们可以把具有前后画面的银行卡图片,通过 IndexedStack
嵌套起来,嵌套之后主要是根据旋转角度来调整 IndexedStack
里需要展示的图片,然后利用透视旋转来实现类似 3D 物体的 360° 旋转展示。
这里的关键是通过手势旋转角度,判断当前需要展示 IndexedStack
里的哪个卡片,因为 Flutter 使用的 Skia 是 2D 渲染引擎,如果没有这部分逻辑,你就只会看到单张图片画面的旋转效果。
if (touchX.abs() % (pi * 3 / 2) >= pi / 2 ||
touchY.abs() % (pi * 3 / 2) >= pi / 2) {
showIndex = 0;
} else {
showIndex = 1;
}
运行效果如下图所示,可以看到在视差和图片切换的作用下,我们用很低的成本在 Flutter 上实现了 “伪3D” 的卡片的 360° 展示,类似的实现其实还可以用于一些商品展示或者页面切换的场景,本质上就是利用视差的效果,在 2D 屏幕上模拟现实中的画面效果,从而达到类似 3D 的视觉作用 。
最后我们只需要用 Text
在卡片上添加“模拟”凹凸的文字,就实现了我们现实中类似银行卡的卡面效果。
完整代码可见: card_3d_demo_page.dart
Web 体验地址,PC 端记得开 chrome 手机模式: 360° 可视化 3D 电子银行卡
好了,本篇动画特效就到为止,如果你有什么想法,欢迎留言评论,感谢大家耐心看完,也还请看官们走过路过的来个点赞一键三连,感激不尽~