持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情
概述
Hero指的是可以在路由(页面)之间“飞行”的widget,简单来说Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。
你可能多次看到过 hero 动画。例如,一个路由中显示待售商品的缩略图列表,选择一个条目会将其跳转到一个新路由,新路由中包含该商品的详细信息和“购买”按钮。 在Flutter中将图片从一个路由“飞”到另一个路由称为hero动画,尽管相同的动作有时也称为 共享元素转换。下面我们通过一个示例来体验一下hero 动画。
Hero动画的基本结构
在不同路由中使用两个 hero widget,但使用匹配的标签来实现动画。
导航器管理包含应用程序路由的栈。
从导航器栈中推入或弹出路由会触发动画。
Flutter框架会计算一个补间矩形 ,用于定义在从源路由“飞行”到目标路由时 hero 的边界。在“飞行”过程中, hero 会移动到应用程序上的一个叠加层,以便它出现在两个页面之上。
Hero示例基本用法
假设有两个路由A和B,他们的内容交互如下:
A:包含一个用户头像,圆形,点击后跳到B路由,可以查看大图。
B:显示用户头像原图,矩形;
在AB两个路由之间跳转的时候,用户头像会逐渐过渡到目标路由页的头像上,接下来我们先看看代码,然后再解析: A路由
import 'package:flutter/material.dart';
class HeroAnimationRoute extends StatelessWidget {
const HeroAnimationRoute({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
padding: EdgeInsets.only(top: 100),
alignment: Alignment.topCenter,
child: Column(
children: <Widget>[
InkWell(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: ClipOval(
child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimages.liqucn.com%2Fimg%2Fh23%2Fh09%2Fimg_localize_d7f901059b0334898457e6775f8bd43f_400x400.png&refer=http%3A%2F%2Fimages.liqucn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1669164092&t=14d13e98a830d9a0d856ca295516c8cb',
width: 100.0,
),
),
),
onTap: () {
//打开B路由
Navigator.push(context, PageRouteBuilder(
pageBuilder: (
BuildContext context,
animation,
secondaryAnimation,
) {
return FadeTransition(
opacity: animation,
child: Scaffold(
appBar: AppBar(
title: Text("原图"),
),
body: HeroAnimationRouteB(),
),
);
},
));
},
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text("点击头像"),
)
],
),
),
);
}
}
B路由
class HeroAnimationRouteB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimages.liqucn.com%2Fimg%2Fh23%2Fh09%2Fimg_localize_d7f901059b0334898457e6775f8bd43f_400x400.png&refer=http%3A%2F%2Fimages.liqucn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1669164092&t=14d13e98a830d9a0d856ca295516c8cb')
));
}
}
运行效果:
实现Hero动画只需要用Hero Widget将要共享的Widget包装起来,并提供一个相同的tag即可。中间的过渡帧都是Flutter Framework自动完成的.
Hero自定义实战应用
需要实现的效果:
1 首先是页面的主体
在这里使用的是Scaffold脚手架来构建,代码如下:
class HeroListPage extends StatefulWidget {
const HeroListPage({Key? key}) : super(key: key);
@override
State<HeroListPage> createState() => _HeroListPageState();
}
class _HeroListPageState extends State<HeroListPage> {
var listImages = [
'http://cools.qctt.cn/1666242448385.jpeg?imageView2/1/w/800/h/450',
'http://cools.qctt.cn/1666183905385.jpeg?imageView2/1/w/800/h/450'
];
var titles =['嗨!Car|更帅更猛 更大更强,全新宝马X1能否再创辉煌 ',
'嗨!Car|纯电领域能否维持超豪华品牌荣光?劳斯莱斯闪灵给你答案 '];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[200],
appBar: getAppBar("每日新闻"),
//主体页面
body: buildListBodyWidget(),
);
}
Widget buildListBodyWidget(){
return ListView(
children: [
InkWell(
//点击跳转
onTap: (){
openPageFunction(listImages[0], titles[0]);
},
child: Container(
padding: EdgeInsets.all(10),
color: Colors.white,
child: Row(//主轴方向开始对齐 在这里是左对齐
mainAxisAlignment: MainAxisAlignment.start,
//交叉轴上开始对齐 在这里是顶部对齐
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//左侧的图片
buildLeftImage(listImages[0]),
//右侧的文本区域
buildRightTextArea(titles[0])],
),
),
),
InkWell(
//点击跳转
onTap: (){
openPageFunction(listImages[1], titles[1]);
},
child: Container(
padding: EdgeInsets.all(10),
color: Colors.white,
child: Row(//主轴方向开始对齐 在这里是左对齐
mainAxisAlignment: MainAxisAlignment.start,
//交叉轴上开始对齐 在这里是顶部对齐
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//左侧的图片
buildLeftImage(listImages[1]),
//右侧的文本区域
buildRightTextArea(titles[1])],
),
),
),
],
);
}
...
}
2 页面的主体就是这里显示的图文,使用Row来将图片与文本区域左右排列,代码如下:
///左侧的图片区域
Container buildLeftImage(imageurl) {
return Container(
margin: EdgeInsets.only(right: 12),
child: Hero(
tag: imageurl,
child: Image.network(imageurl,width: 95,height:95,fit: BoxFit.fill),
// child: Image.asset(
// "images/banner3.webp",
// width: 96,
// fit: BoxFit.fill,
// height: 96,
// ),
),
);
}
///右侧的文本区域
Expanded buildRightTextArea(titile) {
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"优美的应用",
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 3,
style: TextStyle(fontSize: 16),
),
Text(
titile,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 3,
style: TextStyle(fontSize: 14, color: Colors.black38),
)
],
),
);
}
3 自定义透明过度动画路由
Hero达成两个页面之间共享元素的连动效果,但是页面的切换效果造成碍眼的体验,配合一个透明过度,达成舒适的体验,代码如下:
//自定义动画路由过度效果
void openPageFunction(imageurl,title){
Navigator.of(context).push(
PageRouteBuilder(pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation){
return DetailsHeroPage(imageurl,title);
},
//打开新的页面用时
transitionDuration: Duration(milliseconds: 1800),
//关半页用时
reverseTransitionDuration: Duration(milliseconds: 1800),
//过渡动画构建
transitionsBuilder: (BuildContext context,Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,){
//渐变过渡动画
return FadeTransition(
// 透明度从 0.0-1.0
opacity: Tween(begin: 0.0,end: 1.0).animate(CurvedAnimation(
parent: animation,
//动画曲线规则,这里使用的是先快后慢
curve: Curves.fastOutSlowIn
)
),
child: child,
);
}
),
);
}
4 最后就是点击图文信息打开的详情页面
//路由B页面详情页
class DetailsHeroPage extends StatelessWidget {
//页面创建时候接受参数
final String imageurl;
final String title;
DetailsHeroPage( this.imageurl, this.title);
@override
Widget build(BuildContext context) {
return Scaffold(
//背景透明
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("精彩人生"),
),
body: buildCurrentWidget(context),
);
}
Widget buildCurrentWidget(BuildContext context) {
return Container(
color: Colors.white,
padding: EdgeInsets.all(8),
margin: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//图片区域
buildHero(context),
SizedBox(
width: 22,
),
//文字区域
buildTextContainer(),
],
),
);
}
///图片区域
Hero buildHero(BuildContext context) {
return Hero(
tag: imageurl,
child: Material(
color: Colors.blue,
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Image.network(imageurl,fit: BoxFit.fill),
),
),
);
}
Container buildTextContainer() {
return Container(
child: Text(
title,
softWrap: true,
overflow: TextOverflow.ellipsis,
maxLines: 3,
style: TextStyle(fontSize: 16),
),
);
}
}
Flutter Hero动画 让你的APP页面切换充满动效 不一样的体验 优美的视觉。
总结:
Hero指的是可以在路由(页面)之间“飞行”的widget。
使用Flutter的Hero widget创建hero动画。
将 hero从一个路由飞到另一个路由。
将 hero 的形状从圆形转换为矩形,同时将其从一个路由飞到另一个路由的过程中进行动画处理。
Flutter中的Hero widget实现了通常称为 共享元素转换 或 共享元素动画的动画风格。