文章目录
- ◯、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 的开方值 再乘以 2
const 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)
/// 并且该方形组件居中显示
/// 那么该方形组件的四个顶点正好处于圆形组件的裁剪半径位置
/// 也就是方形组件完整显示 , 没有裁剪到
@override
Widget 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) {
// 创建一个 RoutePageBuilder
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
/// 设置透明度组件
return Opacity(
/// 当前的透明度值 , 取值 0.0 ~ 1.0
opacity: 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 动画标签 ID
tag: 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;
@override
Widget build(BuildContext context) {
return Material(
/// 获取主题颜色 , 并将透明度设置为 0.25
color: 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 的开方值 再乘以 2
const 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)
/// 并且该方形组件居中显示
/// 那么该方形组件的四个顶点正好处于圆形组件的裁剪半径位置
/// 也就是方形组件完整显示 , 没有裁剪到
@override
Widget 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 动画标签 ID
tag: 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) {
// 创建一个 RoutePageBuilder
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
/// 设置透明度组件
return Opacity(
/// 当前的透明度值 , 取值 0.0 ~ 1.0
opacity: opacityCurve.transform(animation.value),
// 主要显示的使用透明度控制的组件
// 页面 2 组件
child: _buildSecondPageWidget(context, imageName, description),
);
});
}));
},
),
),
),
);
}
@override
Widget 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 ( 本篇博客的源码快照 , 可以找到本博客的源码 )