文章目录
- ◯、Hero 构造函数
- 一、圆形方形组件
- 二、创建页面 1 的组件 ( Hero 组件 1 )
- 三、创建页面 2 的组件 ( Hero 组件 2 )
- 四、完整代码示例
- 五、相关资源
◯、Hero 构造函数
Hero 构造函数 :
/// 创建一个 Hero 组件 ;////// tag , child 参数不能为空 ; /// child 参数的值不能是 Hero 组件以及 Hero 组件子类 ;const Hero({Key? key,required this.tag,this.createRectTween,this.flightShuttleBuilder,this.placeholderBuilder,this.transitionOnUserGestures = false,required this.child,}) : assert(tag != null),assert(transitionOnUserGestures != null),assert(child != null),super(key: key);
required this.tag : 不能为空 , 用于 关联两个界面的 Hero 组件 , 两个 Hero 组件有关联关系 , 则设置相同的 tag 字符串 ;
this.createRectTween : 可以为空 , 用于 定义 Hero 组件的边界 , 以及定义 Hero 组件在界面切换时 , 从 源界面的起始位置 到 目的界面的最终位置 , 动画执行的变化过程 ;
required this.child : 不能为空 , 普通的 Widget 组件 , Hero 动画作用的组件 ;
Hero 动画可以实现径向动画 , 径向动画指的是组件形状可变的动画 , 如圆形变方形 , 方形变三角形 ;
Hero 径向动画 与 普通动画的区别就是是否设置了 createRectTween 参数 ;
一、圆形方形组件
圆形方形变化的组件 : 该组件可以根据不同的参数实现圆形到方形的变化 , 或方形到圆形的变化 ;
/// Hero 组件 , 径向动画扩展
/// 该组件主要用于裁剪组件用的
class OvalRectWidget extends StatelessWidget {/// 这里的裁剪大小 clipRectSize 最大半径 / 2 的开方值 再乘以 2const OvalRectWidget({Key key, this.maxRadius, this.child}): clipRectSize = 2.0 * (maxRadius / math.sqrt2),super(key: key);// 最大半径值final double maxRadius;/// 该值需要动态计算final clipRectSize;final Widget child;/// 这里特别注意该圆形裁剪组件/// 如果整个组件的宽高都是 maxRadius ,/// 内部的方形组件宽高是 2.0 * (maxRadius / math.sqrt2)/// 并且该方形组件居中显示/// 那么该方形组件的四个顶点正好处于圆形组件的裁剪半径位置/// 也就是方形组件完整显示 , 没有裁剪到@overrideWidget build(BuildContext context) {/// 布局裁剪组件 , 可以将布局裁剪成圆形return ClipOval(/// 可用于约束布局大小的组件/// 这里的居中显示是关键 , 如果不居中显示 , 最终还是圆形child: Center(child: SizedBox(width: clipRectSize,height: clipRectSize,/// 用于裁剪圆角矩形的组件child: ClipRect(child: child,),),),);}
}
组件形状显示分析 :
① 方形裁剪组件 : ClipOval 组件区域是 红色 矩形所在位置 , 其裁剪区域是蓝色组件位置 , 如果正好有个方形的组件 ClipRect 处于下面橙色区域内 , 那么该方形组件正好躲过了被外围红色区域 ClipOval 裁剪的操作 ; 显示的仍然是方形的组件 ;
② 圆形裁剪组件 : 如果 ClipOval 圆形裁剪组件 ( 红色 ) 与 ClipRect 方形的裁剪组件 ( 橙色 ) 位置重叠 , 那么该方形的裁剪组件肯定就被裁剪成圆形的了 ;
上面两个组件就是 Hero 径向动画的主要作用组件 , 该动画执行前 , 组件是圆形的 , 执行后组件是方形的 , 这就是改变了外层的 ClipOval 组件的大小 , 导致形状改变 ;
二、创建页面 1 的组件 ( Hero 组件 1 )
页面 1 的 Hero 组件显示的圆形的 , 跳转到页面 2 后 , 相同 tag 的 Hero 组件显示方形 ;
控制 OvalRectWidget 是圆形还是方形 , 主要是控制 OvalRectWidget 组件的宽高 , 这里设置的宽高设置 , 相当于上面的 " ② 圆形裁剪组件 " 情况 , 整个组件被裁剪成圆形的组件 ;
创建页面 1 的组件 :
/// 创建在界面 1 显示的图标 , 点击后跳转到界面 2/// 页面的核心组件是 Hero 组件 , 而且是 3 个Widget _buildFirstPagWidget(BuildContext context, String imageName, String description) {return Container(/// 界面 1 中的显示的 Hero 组件是小图标/// 图标大小就是半径的两倍width: minRadius * 2.0,height: minRadius * 2.0,/// 主界面的核心 Hero 动画child: Hero(/// 这是 Hero 径向动画与标准 Hero 动画的区别/// 如果没有这个动画 , 中间过程会变成椭圆createRectTween: _createRectTween,/// Hero 动画标签tag: imageName,child: OvalRectWidget(maxRadius: maxRadius,/// 最内层显示的是网络图片组件child: ImageWidget(/// 设置网络图片地址imageUrl: imageName,// 设置点击事件onTap: () {/// 点击后跳转到新界面中Navigator.of(context).push(PageRouteBuilder<void>(pageBuilder:(BuildContext context, Animation<double> animation,Animation<double> secondaryAnimation) {// 创建一个 RoutePageBuilderreturn AnimatedBuilder(animation: animation,builder: (context, child) {/// 设置透明度组件return Opacity(/// 当前的透明度值 , 取值 0.0 ~ 1.0opacity: opacityCurve.transform(animation.value),// 主要显示的使用透明度控制的组件// 页面 2 组件child: _buildSecondPageWidget(context, imageName, description),);});}));},),),),);}
三、创建页面 2 的组件 ( Hero 组件 2 )
页面 1 的 Hero 组件显示的圆形的 , 跳转到页面 2 后 , 相同 tag 的 Hero 组件显示方形 ;
控制 OvalRectWidget 是圆形还是方形 , 主要是控制 OvalRectWidget 组件的宽高 , 这里设置的宽高相当于上面的 " ① 方形裁剪组件 " 设置 , 整个组件没有被裁剪到 , 显示的是方形组件 ;
创建页面 2 的组件 :
/// 创建页面 2 , 这是点击后跳转到的页面/// 三个参数分别是 : 上下文 , 图片名称 , 页面描述/// 页面的核心组件是 Hero 组件 , 只有 1 个static Widget _buildSecondPageWidget(BuildContext context, String imageName, String description) {return Container(color: Theme.of(context).canvasColor,child: Center(child: Card(/// 设置卡片布局阴影大小elevation: 8,/// 卡片布局中显示图片和图片的描述child: Column(/// 在主轴方向 , 也就是垂直方向 , 应该占用多少空间/// Colum 主轴方向是垂直方向/// Row 主轴方向是水平方向mainAxisSize: MainAxisSize.min,children: [SizedBox(/// 约束布局大小的组件的宽高定义为最大半径的两倍width: maxRadius * 2,height: maxRadius * 2,/// 核心 Hero 组件child: Hero(/// 创建径向动画/// 如果没有这个动画 , 中间过程会变成椭圆createRectTween: _createRectTween,/// Hero 动画标签 IDtag: imageName,/// Hero 动画作用的组件child: OvalRectWidget(/// 这里的半径设置为最大半径值 ,maxRadius: maxRadius,/// 最内层显示的是网络图片组件child: ImageWidget(imageUrl: imageName,onTap: () {/// 点击后关闭当前页面Navigator.of(context).pop();},),),),),/// 图片描述文本Text(// 设置文本内容description,// 设置文本样式, 粗体style: TextStyle(fontWeight: FontWeight.bold),textScaleFactor: 3.0,),/// 空白间隔 , 无实际意义const SizedBox(height: 16,),],),),),);}
四、完整代码示例
完整代码示例 :
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'dart:math' as math;void main() {runApp(MaterialApp(// 该组件本质是 StatelessWidget 组件子类home: RadialHeroAnimation(),));
}/// Hero 组件 , 跳转前后两个页面都有该组件
class ImageWidget extends StatelessWidget {/// 构造方法const ImageWidget({Key key, this.imageUrl, this.onTap}) : super(key: key);/// Hero 动画之间关联的 ID , 通过该标识/// 标识两个 Hero 组件之间进行动画过渡/// 同时该字符串也是图片的 url 网络地址final String imageUrl;/// 点击后的回调事件final VoidCallback onTap;@overrideWidget build(BuildContext context) {return Material(/// 获取主题颜色 , 并将透明度设置为 0.25color: Colors.green,/// 按钮child: InkWell(/// 按钮点击事件onTap: onTap,child: LayoutBuilder(builder: (BuildContext context, BoxConstraints size) {return Image.network(imageUrl,fit: BoxFit.contain,);},),),);}
}/// Hero 组件 , 径向动画扩展
/// 该组件主要用于裁剪组件用的
class OvalRectWidget extends StatelessWidget {/// 这里的裁剪大小 clipRectSize 最大半径 / 2 的开方值 再乘以 2const OvalRectWidget({Key key, this.maxRadius, this.child}): clipRectSize = 2.0 * (maxRadius / math.sqrt2),super(key: key);// 最大半径值final double maxRadius;/// 该值需要动态计算final clipRectSize;final Widget child;/// 这里特别注意该圆形裁剪组件/// 如果整个组件的宽高都是 maxRadius ,/// 内部的方形组件宽高是 2.0 * (maxRadius / math.sqrt2)/// 并且该方形组件居中显示/// 那么该方形组件的四个顶点正好处于圆形组件的裁剪半径位置/// 也就是方形组件完整显示 , 没有裁剪到@overrideWidget build(BuildContext context) {/// 布局裁剪组件 , 可以将布局裁剪成圆形return ClipOval(/// 可用于约束布局大小的组件/// 这里的居中显示是关键 , 如果不居中显示 , 最终还是圆形child: Center(child: SizedBox(width: clipRectSize,height: clipRectSize,/// 用于裁剪圆角矩形的组件child: ClipRect(child: child,),),),);}
}class RadialHeroAnimation extends StatelessWidget {/// 最小半径/// 使用该半径作为组件大小时 , 组件被裁剪成圆形static const double minRadius = 32.0;/// 最大半径/// 使用该半径作为组件大小时 , 组件被裁剪成方形static const double maxRadius = 128.0;/// 动画差速器static const opacityCurve = Interval(0.0, 0.75, curve: Curves.fastOutSlowIn);/// 创建径向动画static RectTween _createRectTween(Rect begin, Rect end) {/// MaterialRectCenterArcTween 就是从方形到圆形变化的辅助类return MaterialRectCenterArcTween(begin: begin, end: end);}/// 创建页面 2 , 这是点击后跳转到的页面/// 三个参数分别是 : 上下文 , 图片名称 , 页面描述/// 页面的核心组件是 Hero 组件 , 只有 1 个static Widget _buildSecondPageWidget(BuildContext context, String imageName, String description) {return Container(color: Theme.of(context).canvasColor,child: Center(child: Card(/// 设置卡片布局阴影大小elevation: 8,/// 卡片布局中显示图片和图片的描述child: Column(/// 在主轴方向 , 也就是垂直方向 , 应该占用多少空间/// Colum 主轴方向是垂直方向/// Row 主轴方向是水平方向mainAxisSize: MainAxisSize.min,children: [SizedBox(/// 约束布局大小的组件的宽高定义为最大半径的两倍width: maxRadius * 2,height: maxRadius * 2,/// 核心 Hero 组件child: Hero(/// 创建径向动画/// 如果没有这个动画 , 中间过程会变成椭圆createRectTween: _createRectTween,/// Hero 动画标签 IDtag: imageName,/// Hero 动画作用的组件child: OvalRectWidget(/// 这里的半径设置为最大半径值 ,maxRadius: maxRadius,/// 最内层显示的是网络图片组件child: ImageWidget(imageUrl: imageName,onTap: () {/// 点击后关闭当前页面Navigator.of(context).pop();},),),),),/// 图片描述文本Text(// 设置文本内容description,// 设置文本样式, 粗体style: TextStyle(fontWeight: FontWeight.bold),textScaleFactor: 3.0,),/// 空白间隔 , 无实际意义const SizedBox(height: 16,),],),),),);}/// 创建在界面 1 显示的图标 , 点击后跳转到界面 2/// 页面的核心组件是 Hero 组件 , 而且是 3 个Widget _buildFirstPagWidget(BuildContext context, String imageName, String description) {return Container(/// 界面 1 中的显示的 Hero 组件是小图标/// 图标大小就是半径的两倍width: minRadius * 2.0,height: minRadius * 2.0,/// 主界面的核心 Hero 动画child: Hero(/// 这是 Hero 径向动画与标准 Hero 动画的区别/// 如果没有这个动画 , 中间过程会变成椭圆createRectTween: _createRectTween,/// Hero 动画标签tag: imageName,child: OvalRectWidget(maxRadius: maxRadius,/// 最内层显示的是网络图片组件child: ImageWidget(/// 设置网络图片地址imageUrl: imageName,// 设置点击事件onTap: () {/// 点击后跳转到新界面中Navigator.of(context).push(PageRouteBuilder<void>(pageBuilder:(BuildContext context, Animation<double> animation,Animation<double> secondaryAnimation) {// 创建一个 RoutePageBuilderreturn AnimatedBuilder(animation: animation,builder: (context, child) {/// 设置透明度组件return Opacity(/// 当前的透明度值 , 取值 0.0 ~ 1.0opacity: opacityCurve.transform(animation.value),// 主要显示的使用透明度控制的组件// 页面 2 组件child: _buildSecondPageWidget(context, imageName, description),);});}));},),),),);}@overrideWidget build(BuildContext context) {/// 时间膨胀系数 , 用于降低动画运行速度/// 1.0 是标准速度timeDilation = 5.0;/// 主界面显示内容return Scaffold(appBar: AppBar(title: Text("Hero 径向动画演示"),),body: Container(padding: EdgeInsets.all(32),alignment: FractionalOffset.bottomLeft,/// 横向列表显示 3 个图标child: Row(/// 排列方式 : 平分空间mainAxisAlignment: MainAxisAlignment.spaceBetween,children: [_buildFirstPagWidget(context,"https://img-blog.csdnimg.cn/20210330094257242.png", "蜂王"),_buildFirstPagWidget(context,"https://img-blog.csdnimg.cn/20210330093526559.png", "蜜蜂"),_buildFirstPagWidget(context,"https://img-blog.csdnimg.cn/2021033009353553.png", "工蜂"),],),),);}
}
运行效果 :
五、相关资源
参考资料 :
- Flutter 官网 : https://flutter.dev/
- Flutter 插件下载地址 : https://pub.dev/packages
- Flutter 开发文档 : https://flutter.cn/docs ( 强烈推荐 )
- 官方 GitHub 地址 : https://github.com/flutter
- Flutter 中文社区 : https://flutter.cn/
- Flutter 实用教程 : https://flutter.cn/docs/cookbook
- Flutter CodeLab : https://codelabs.flutter-io.cn/
- Dart 中文文档 : https://dart.cn/
- Dart 开发者官网 : https://api.dart.dev/
- Flutter 中文网 : https://flutterchina.club/ , http://flutter.axuer.com/docs/
- Flutter 相关问题 : https://flutterchina.club/faq/ ( 入门阶段推荐看一遍 )
- GitHub 上的 Flutter 开源示例 : https://download.csdn.net/download/han1202012/15989510
- Flutter 实战电子书 : https://book.flutterchina.club/chapter1/
重要的专题 :
- Flutter 动画参考文档 : https://flutterchina.club/animations/
博客源码下载 :
-
GitHub 地址 : https://github.com/han1202012/flutter_animation ( 随博客进度一直更新 , 有可能没有本博客的源码 )
-
博客源码快照 : https://download.csdn.net/download/han1202012/16245277 ( 本篇博客的源码快照 , 可以找到本博客的源码 )