27、Flutter之Hero动画,自定义页面跳转炫酷动效。

1,297 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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')
    ));
  }
}

运行效果:

111.gif

实现Hero动画只需要用Hero Widget将要共享的Widget包装起来,并提供一个相同的tag即可。中间的过渡帧都是Flutter Framework自动完成的.

Hero自定义实战应用

需要实现的效果:

111.gif

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实现了通常称为 共享元素转换 或 共享元素动画的动画风格。