这次总结游戏中的双面的旋转卡牌实现效果,先从单面旋转到双面旋转的具体实现过程,一个有视觉3D效果的动画在flutter中首先想到的就是矩阵,通过改变轴心的位置让用户看起来很有立体的感觉。
先回顾一下游戏中的卡牌效果是什么样的,如下:
先发体验地址及仓库
github地址:https://github.com/tec8297729/hh_game
APP扫码下载体验,密码:111111
矩阵知识概念
flutter的小伙伴们,如果还没接触学习到矩阵就要先补一波知识点
实现绘制矩阵特效是通过Transform组件实现的,此组件中会有一个transform参数,而此参数就需要用到Matrix4矩阵(4D),它提供了对元素进行x/y/z轴的控制、视图距离等等方法
准备了几篇介绍矩阵相关的文章,不了解的同学可以先了解一下起!!
flutter官方文档Matrix4介绍》》传送门
Transform基础入门篇》》传送门
Matrix4矩阵变换进阶》》传送门
以下Transform组件视频介绍,请跨出墙外,你懂的
Matrix4矩阵用法
为了能快速融入此次旋转卡牌矩阵实现,针对部份矩阵知识点进行掌握讲解。
PS: 想要更全面的了解所有矩阵相关知识可以先查看上面介绍的文章及文档传送链接,矩阵Matrix3和Matrix4要说的东西还是比较多的,可能最终会偏理了主题方向。
好了开始进入正题:
1、Matrix4.identity() 创建生成一个矩阵
2、setEntry() 设置矩阵中的行、列、视图距离。。。第三个物体的视图距离相当于对着屏幕的远近,当越远时就看着此物体就会越全面,看着就如3D整体效果一样。
3、rotateY() 设置改变Y轴方向,像旋转卡牌其实只改变Y轴位置。
1 2 3 4 5 |
Transform( transform: Matrix4.identity() // 声明一个4D矩阵 ..setEntry(3, 2, 0.0001) // 设定值row行,col列, v视图距离。 ..rotateY(22), // 设置Y轴旋转位置 ); |
实现旋转卡牌
flutter基础页面搭建
首先就是搞一个flutter的基础页面环境出来,这里就不单个去写了,我就用现成的flutter_flexible脚手一键生成页面了(主要是便利快捷生成),毕竟主点是讲解动画技术。
PS:你也可以手动创建一个flutter空白页面,直接跳过此步骤。
1、输入命令安装脚手架cli,并且创建一个新的项目。
1 2 3 4 5 6 7 |
npm i -g flib-cli // 全局安装cli脚手架 flib create // 建一个新的flutter项目,默认直接回车就行。 // 进入项目目录,安装插件启动模拟器 flutter pub get flutter run |
2、接着进入lib\pages\AppHomePage\Home\Home.dart 页面组件中,去除演示demo代码,准备好基础页面环境。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import 'package:flutter/material.dart'; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('jonhuu.com'), ), ); } } |
2、找到lib\config\app_config.dart文件,配置一下跳过广告页面
1 2 3 4 5 6 7 8 |
import '../routes/routeName.dart'; class AppConfig { // 。。。省略其它 /// 是否直接跳过闪屏页面, static const notSplash = true; } |
实现单面的旋转卡牌
1、准备一些图片素材,可以在网上下载几张图片,或是直接使用hh_game仓库中的素材 ,把asset/eleItem目录中的所有文件复制一份入到自己的本地asset/eleItem目录中。
2、配置pubspec.yaml文件,把静态资源目录加入进来
1 2 3 4 5 6 7 8 9 |
# 。。。省略 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true assets: - asset/eleItem/ |
3、创建一个带有背景的方块小部件,直接使用上面的背景素材,先使用其中一个,后续旋转卡牌都会用到这些素材。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import 'package:flutter/material.dart'; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: _eleBox(), ), ); } Widget _eleBox() { return GestureDetector( onTap: () {}, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/1.png'), // 背景图片 fit: BoxFit.cover, ), ), ), ); } } |
此时在APP中可以看到带图片的组件了,但还需要在加入点击事件让它可以转起来。
4、开始让组件能动翻转起来,一共用到二个组件,AnimatedBuilder构建动画的组件,Transform用来构建矩阵旋转小部件的。
在home页面中加入AnimatedBuilder动画构建小部件,里面用的Transform小部件,让其静态部件动起来,例如Y轴变动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import 'package:flutter/material.dart'; import 'dart:math' as math; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; @override void initState() { controller = AnimationController( duration: Duration(milliseconds: 1500), vsync: this, )..addListener(() => this.setState(() {})); // 监听动画变更,更新页面 // 定义动画更新区间的值 animation = Tween<double>(begin: 0, end: 360.0).animate(controller); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: animateBuild(), ), ); } // 动画小部件 Widget animateBuild() { return AnimatedBuilder( animation: animation, // 动画效果 child: _eleBox(), builder: (BuildContext context, Widget child) { return Transform( transform: Matrix4.identity() ..setEntry(3, 2, 0.0001) // 第三参数定义视图距离,值越小物体就离你越远,看着就有立体感 // 旋转Y轴角度,pi为圆半径,animation.value为动态获取的动画值 ..rotateY(math.pi * animation.value / 180), alignment: FractionalOffset.center, // 以轴中心开始动画 child: child, ); }, ); } } |
为了测试能否动起来,在_eleBox 封装的组件中加入变量测试一下,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class _HomeState extends State<Home> with SingleTickerProviderStateMixin { bool isTest = true; // ...省略其它 Widget _eleBox() { return GestureDetector( // 增加了点击事件逻辑,测试用。 onTap: () { isTest = !isTest; isTest ? controller.reset() : controller.forward(); }, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/1.png'), // 背景图片 fit: BoxFit.cover, ), ), ), ); } } |
此时单面的卡牌旋转效果就有了,效果如下:
双面旋转卡牌实现
我们基于上面的单面的卡牌代码来改造,让它实现有二面的效果。
思考阶段
在跟着实现双面卡牌效果前,先自我独立思考一下,单面旋转卡牌效果实现了,在此基础上在改造成双面是否有思路实现呢???
旋转卡牌动起来,必然要调整角度,而角度是不是一个突然口呢!!在旋转卡牌中怎么获取旋转中的角度知道么?
建议先独立思考一下在跟着下面实现,打开思维想象空间,好好阅读单面卡牌实现的代码。
实现阶段
1、先创建一个双面卡牌的另一页显示的组件,其实也单面eleBox类似,只是换了一张背景图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class _HomeState extends State<Home> with SingleTickerProviderStateMixin { // ... Widget maskEleBox() { return GestureDetector( onTap: (){}, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/elebg.png'), fit: BoxFit.cover, ), ), ), ); } // ... } |
2、接着判断旋转的角度,来显示不同的eleBox组件
animation.value 动画的值是360,正好是旋转一圈的角度,那么180正好就是旋转了一半,根据值来改造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//... // 动画小部件 Widget animateBuild() { return AnimatedBuilder( animation: animation, // 动画效果 child: _eleBox(), builder: (BuildContext context, Widget child) { return Transform( transform: Matrix4.identity() ..setEntry(3, 2, 0.0001) // 旋转Y轴角度,pi为圆半径,animation.value为动态获取的动画值 ..rotateY(math.pi * animation.value / 360), alignment: FractionalOffset.center, // 判断180为半圈,显示不同的组件 child: animation.value < 180 ? maskEleBox() : child, ); }, ); } //... |
3、改造maskEleBox和_eleBox二个组件点击事件,因为这二个组件事件都是一样的,所以把点击事件抽离出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// ... // ele点击事件 void onClickEleBox() { if (animation.value < 360) { controller.forward(); // 播放动画 return; } controller.reverse(from: 360); // 倒放动画 } Widget maskEleBox() { return GestureDetector( onTap: onClickEleBox, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/elebg.png'), fit: BoxFit.cover, ), ), ), ); } Widget _eleBox() { return GestureDetector( // 点击事件 onTap: onClickEleBox, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/1.png'), // 背景图片 fit: BoxFit.cover, ), ), ), ); } |
最终效果如下:
完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
import 'package:flutter/material.dart'; import 'dart:math' as math; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; @override void initState() { controller = AnimationController( duration: Duration(milliseconds: 1500), vsync: this, )..addListener(() => this.setState(() {})); // 监听动画变更,更新页面 // 定义动画更新区间的值 animation = Tween<double>(begin: 0, end: 360.0).animate(controller); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: animateBuild(), ), ); } // 动画小部件 Widget animateBuild() { return AnimatedBuilder( animation: animation, // 动画效果 child: _eleBox(), builder: (BuildContext context, Widget child) { return Transform( transform: Matrix4.identity() ..setEntry(3, 2, 0.0001) // 第三参数定义视图距离,值越小物体就离你越远,看着就有立体感 // 旋转Y轴角度,pi为圆半径,animation.value为动态获取的动画值 ..rotateY(math.pi * animation.value / 360), alignment: FractionalOffset.center, // 以轴中心开始动画 child: animation.value < 180 ? maskEleBox() : child, ); }, ); } void onClickEleBox() { if (animation.value < 360) { controller.forward(); return; } controller.reverse(from: 360); } Widget maskEleBox() { return GestureDetector( onTap: onClickEleBox, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/elebg.png'), fit: BoxFit.cover, ), ), ), ); } Widget _eleBox() { return GestureDetector( // 点击事件 onTap: onClickEleBox, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/1.png'), // 背景图片 fit: BoxFit.cover, ), ), ), ); } } |
扩展篇:生成九宫格随机图片
模拟来生成一个类似的游戏的宫格,里面随机图片
1、先把一个双面的旋转卡牌封装成一个独立组件,在Home\components\EleItemBox.dart 创建此文件,然后把在home内的代码转到此里面, 动态传入index来显示不同的图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
// EleItemBox.dart文件内代码内容 import 'package:flutter/material.dart'; import 'dart:math' as math; class EleItemBox extends StatefulWidget { EleItemBox({@required this.index}); final int index; @override _EleItemBoxState createState() => _EleItemBoxState(); } class _EleItemBoxState extends State<EleItemBox> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; @override void initState() { controller = AnimationController( duration: Duration(milliseconds: 1500), vsync: this, )..addListener(() => this.setState(() {})); // 监听动画变更,更新页面 // 定义动画更新区间的值 animation = Tween<double>(begin: 0, end: 360.0).animate(controller); super.initState(); } @override Widget build(BuildContext context) { return animateBuild(); } // 动画小部件 Widget animateBuild() { return AnimatedBuilder( animation: animation, // 动画效果 child: _eleBox(), builder: (BuildContext context, Widget child) { return Transform( transform: Matrix4.identity() ..setEntry(3, 2, 0.0001) // 第三参数定义视图距离,值越小物体就离你越远,看着就有立体感 // 旋转Y轴角度,pi为圆半径,animation.value为动态获取的动画值 ..rotateY(math.pi * animation.value / 360), alignment: FractionalOffset.center, // 以轴中心开始动画 child: animation.value < 180 ? maskEleBox() : child, ); }, ); } void onClickEleBox() { if (animation.value < 360) { controller.forward(); return; } controller.reverse(from: 360); } Widget maskEleBox() { return GestureDetector( onTap: onClickEleBox, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/elebg.png'), fit: BoxFit.cover, ), ), ), ); } Widget _eleBox() { return GestureDetector( // 点击事件 onTap: onClickEleBox, child: Container( width: 300, height: 300, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('asset/eleItem/${widget.index}.png'), // 动态图片,添加部份 fit: BoxFit.cover, ), ), ), ); } } |
2、home页面组件内清空之前代码,引入封装好的组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import 'package:flutter/material.dart'; import 'components/EleItemBox.dart'; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: EleItemBox( index: 2, // 传入需要显示的背景图片 ), ), ); } } |
把之前的组件抽离封装后,开始进行生成多个。
3、下面来制作宫格块,动手能力强的小伙伴可以自己思考起尝试写写,封装的单个组件是有了,只是如何渲染多个出来。
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// 直接在Home.dart内添加,通过GridView.builder渲染多个组件 import 'package:flutter/material.dart'; import 'components/EleItemBox.dart'; import 'dart:math' as math; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Container( width: 300, height: 300, child: gridBody(), ), ), ); } Widget gridBody() { return GridView.builder( physics: NeverScrollableScrollPhysics(), // 禁止滚动 padding: EdgeInsets.all(0), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 80.0, // 每个元素的宽度 mainAxisSpacing: 2, // 每个元素底部间隔 crossAxisSpacing: 2, ), itemCount: 16, // 总数量 itemBuilder: (context, int index) { // 封装的eleBox组件 return EleItemBox( index: math.Random().nextInt(5), // 随机0-5之间的值,动态显示不同图片 ); }, ); } } |
最终效果如下:
此demo源码地址:https://github.com/tec8297729/hh_game/tree/eleboxDemo
往期相关文章技术总结