GSYFlutterBook/Flutter-N9.md

235 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 如何利用 Flutter 实现炫酷的 3D 卡片和帅气的 360° 展示效果
我正在参加「创意开发 投稿大赛」详情请看:[掘金创意开发大赛来了!](https://juejin.cn/post/7120441631530549284)
本篇将带你在 Flutter 上快速实现两个炫酷的动画特效,希望最后的效果可以惊艳到你。
这次灵感的来源于更新 MIUI 13 时刚好看到的卡片效果,其中除了卡片会跟随手势出现倾斜之外,内容里的部分文本和绿色图标也有类似悬浮的视差效果,恰逢此时灵机一动,**我们也来用 Flutter 快速实现炫酷的 3D 视差卡片,最后再拓展实现一个支持帅气的 360° 展示的卡片效果**。
> ❤️ **本文正在参加征文投稿活动,还请看官们走过路过来个点赞一键三连,感激不尽~**
![](http://img.cdn.guoshuyu.cn/20220723_N9/image1.gif)
既然需要卡片跟随手势产生不规则形变,我们第一个想到的肯定是**矩阵变换**,在 Flutter 里我们可以使用 `Matrix4` 配合 `Transform` 来实现矩阵变换效果。
开始之前,首先我们创建用 `Transform` 嵌套一个 `GestureDetector` ,并绘制出一个 300x400 的圆角卡片,用于后续进行矩阵变换处理。
```dart
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),
),
),
),
);
```
![](http://img.cdn.guoshuyu.cn/20220723_N9/image2.png)
接着,如下代码所示,因为我们需要卡片跟随手势进行矩阵变换,所以我们可以直接在 `GestureDetector``onPanUpdate` 里获取到手势信息,例如 `localPosition` 位置信息,然后把对应的 `dx``dy`赋值到 `Matrix4``rotateX``rotateY` 上实现旋转。
```dart
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` 也是一个道理。
![](http://img.cdn.guoshuyu.cn/20220723_N9/image3.png)
但是当我们实际运行上述代码之后,如下图所示,可以看到基本上我们只是稍微移动手指,卡片就会陷入疯狂旋转的情况,并且实际的旋转速度会比 GIF 里快很多。
![](http://img.cdn.guoshuyu.cn/20220723_N9/image4.gif)
**问题的原因其实是因为 `rotateX` 和 `rotateY` 需要的是一个 `angle` 参数**,假设这里对 `rotateX``rotateY` 设置 `pi / 4` ,就可以看到卡片在 X 轴和 Y 轴上都产生了 45 度的旋转效果。
```dart
Transform(
transform: Matrix4.identity()
..rotateX(pi / 4)
..rotateY(pi / 4),
alignment: FractionalOffset.center,
```
![](http://img.cdn.guoshuyu.cn/20220723_N9/image5.png)
所以如果直接使用手势的 `localPosition` 作用于 `Matrix4` 肯定是不行的,我们首先需要对手势数据进行一个采样,**因为代码里我们设置了 `FractionalOffset.center` ,所以我们可以用卡片的中心点来计算手指位置,再进行压缩处理**。
如下代码所示,我们通过以卡片中心点为原点进行计算,其中 `/ 2` 就是得到卡片的中心点,`/ 100` 是对数据进行压缩采样,*但是为什么 `touchX``touchY` 的计算方式是相反的呢*
```dart
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` 去计算得到正确数据
| ![](http://img.cdn.guoshuyu.cn/20220723_N9/image6.png) | ![](http://img.cdn.guoshuyu.cn/20220723_N9/image7.gif) |
| ----------------------------------------------------------- | ------------------------------------------ |
如果觉得太抽象,可以结合上边右侧的动图,和**大家买股票一样,图中显示红色时是正数,显示绿色时是负数**,可以看到:
- 手指往左移动时,第一行 TouchX 是红色正数,被设置给 `rotateY` 然后卡片绕 Y 轴正方向旋转
- 手指往下移动时,第二行 TouchY 是红色正数,被设置给 `rotateX` 然后卡片绕 X 轴正方向旋转
到这里我们就初步实现了卡片跟随手机旋转的效果,**但是这时候的立体旋转效果看起来其实“很别扭”,总感觉差了点什么,其实这是因为卡片在旋转时没有产生视觉上的深度感知**。
所以我们可以通过矩阵的透视变换调整视觉效果,而为了在 Z 方向实现深度感知,我们需要在矩阵中配置 `.setEntry(3, 2, 0.001)` ,这里的 3 表示第 3 列2 表示第 2 行,因为是从 0 开始排列,所以也就是图片中 Z 的位置。
![](http://img.cdn.guoshuyu.cn/20220723_N9/image8.png)
其实 `.setEntry(3, 2, 0.001)` 就是调整 Z 轴的视角,而在 Z 上的 0.001 就是需要的透视效果测量值,类似于相机上的对焦点进行放大和缩小的作用,这个数字越大就会让交点处看起来好像离你视觉更近,所以最终代码如下
```dart
Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(touchY)
..rotateY(touchX),
alignment: FractionalOffset.center,
```
运行之后,可以看到在增加了 Z 角度的视角调整之后,这时候看起来的立体效果就好了很多,并且也有了类似 3D 空间的感觉。
![](http://img.cdn.guoshuyu.cn/20220723_N9/image9.gif)
接着我们在卡片上放上一个添加一个 `13``Text` 文本,运行之后可以看到此时文本是跟随卡片发生变化,而接下来我们需要做的,就是**通过另外一个 `Transform` 来让 `Text` 文本和卡片之间产生视差,从而出现悬浮的效果**。
| ![](http://img.cdn.guoshuyu.cn/20220723_N9/image10.png) | ![](http://img.cdn.guoshuyu.cn/20220723_N9/image11.gif) |
| ----------------------------------------------------------- | ---------------------------------------- |
所以接下来需要给文本内容设置一个 `translate``Matrix4` ,让它向着倾斜角度的相反方向移动,然后对前面的 `touchX``touchY` 进行放大,然后再通过 `- 10` 操作来产生一个位差。
```dart
Transform(
transform: Matrix4.identity()
..translate(touchX * 100 - 10,
touchY * 100 - 10, 0.0),
```
> `-10` 这个是我随意写的,你也可以根据自己的需求调节。
例如,这时候当卡片往左倾斜时,文字就会向右移动,从而产生视觉差的效果,得到类似悬浮的感觉。
| ![](http://img.cdn.guoshuyu.cn/20220723_N9/image12.png) | ![](http://img.cdn.guoshuyu.cn/20220723_N9/image13.gif) |
| ----------------------------------------------------------- | ----------------------------------------- |
完成这一步之后,接下来可以我们对文本内容进行一下美化处理,例如增加渐变颜色,添加阴影,更换字体,目的是让字体看起来更加具备立体的效果,**这里使用的 `shader` ,也可以让文字在移动过程中出现不同角度的渐变效果**。
| ![](http://img.cdn.guoshuyu.cn/20220723_N9/image14.png) | ![](http://img.cdn.guoshuyu.cn/20220723_N9/image15.gif) |
| ----------------------------------------------------------- | ----------------------------------------- |
最后,我们还需要对卡片旋转进行一个范围约束,这里主要是通过卡片大小比例:
-`onPanUpdate` 时对 `touchX``touchY` 进行范围约束,从而约束的卡片的倾斜角度
- 增加了 `startTransform` 标志位,用于在 `onTapUp` 或者 `onPanEnd` 之后,恢复卡片回到默认状态的作用。
```dart
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:
```
到这里,我们只需要在全局再进行一些美化处理,运行之后就会如下图所示,在配合阴影和渐变效果,整体的视觉立体感会更强烈,此时我们基本就实现了一开始想要的功能,
![](http://img.cdn.guoshuyu.cn/20220723_N9/image16.gif)
> 完整代码可见: [card_perspective_demo_page.dart](https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/card_perspective_demo_page.dart)
>
> Web 体验地址PC 端记得开 Chrome 手机模式: [3D 视差卡片](http://guoshuyu.cn/home/web/#3D%20%E9%80%8F%E8%A7%86%E5%8D%A1%E7%89%87) 。
那有人可能就想问了: *学会了这个我们还可以实现什么*
举个例子,比如我们可以实现一个 “伪3D” 的 360° 卡片效果,利用堆叠实现立体的电子银行卡效果。
依旧是前面的手势旋转逻辑,只是这里我们可以把具有前后画面的银行卡图片,通过 `IndexedStack` 嵌套起来,**嵌套之后主要是根据旋转角度来调整 `IndexedStack` 里需要展示的图片,然后利用透视旋转来实现类似 3D 物体的 360° 旋转展示**。
![](http://img.cdn.guoshuyu.cn/20220723_N9/image17.png)
**这里的关键是通过手势旋转角度,判断当前需要展示 `IndexedStack` 里的哪个卡片**,因为 Flutter 使用的 Skia 是 2D 渲染引擎,如果没有这部分逻辑,你就只会看到单张图片画面的旋转效果。
```dart
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 的视觉作用** 。
| ![](http://img.cdn.guoshuyu.cn/20220723_N9/image18.gif) | ![](http://img.cdn.guoshuyu.cn/20220723_N9/image19.gif) |
| ---------------------------------------------------- | ----------------------------------------- |
**最后我们只需要用 `Text` 在卡片上添加“模拟”凹凸的文字,就实现了我们现实中类似银行卡的卡面效果**
![](http://img.cdn.guoshuyu.cn/20220723_N9/image20.gif)
> 完整代码可见: [card_3d_demo_page.dart](https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/card_3d_demo_page.dart)
>
> Web 体验地址PC 端记得开 chrome 手机模式: [ 360° 可视化 3D 电子银行卡](http://guoshuyu.cn/home/web/#3D%20%E5%8D%A1%E7%89%87%E6%97%8B%E8%BD%AC)
好了,本篇动画特效就到为止,**如果你有什么想法,欢迎留言评论,感谢大家耐心看完,也还请看官们走过路过的来个点赞一键三连,感激不尽**