我和谷歌共成长 - Flutter 中的 Hero Widget:实用指南(2022 年)

守护奶酪 2022-08-22 21:36:58

 

本文是关于 Flutter 中的 Hero 小部件和动画。我们将看一眼这些东西的本质,然后研究一个在实践中使用它们的完整示例。

简单来说,Hero动画是一个页面(屏幕)的元素在用户导航时飞到另一个页面的动画。要设置Hero动画,您需要在 2 个页面上放置 2 个Hero小部件,如下所示:

// implement a Hero widget on source page
InkWell(
         // navigate to the detail page on tap
         onTap: () {
                  Navigator.of(context).push(/* destination route */);
         },
         // implement the Hero widget
         child: Hero(
                  tag: 'id-123',
                  child: /* ... */,
         ),
);

和这个:

 

// implement the corresponding Hero widget on destination page
Hero(
        tag: 'id-123',
        child: /* ... */,
),

2 个不同页面上的 2 个Hero小部件链接在一起,仅仅是因为它们具有相同的标签。这个非常重要。标签可以是任意 Dart 对象,如字符串、数字等。

Hero 小部件的构造函数:

Hero({
  Key? key, 
  required Object tag, 
  CreateRectTween? createRectTween, 
  HeroFlightShuttleBuilder? flightShuttleBuilder, 
  HeroPlaceholderBuilder? placeholderBuilder, 
  bool transitionOnUserGestures = false, 
  required Widget child
})

有 2 个属性不能忽略:

  • tag:Hero的标识符。出发页面上的Hero小部件的标签和目的地页面上的Hero小部件的标签必须相同。

  • child : 在过渡期间从一个页面飞到另一个页面的小部件

一堵文字墙可能很无聊,让你感到困惑。继续下一部分,通过编写一个工作演示应用程序来更好地理解。

 

完整示例

应用预览

我们要构建的演示项目包含 2 个页面:

 

 

  • 主屏幕:显示缩略图网格。您可以通过点击缩略图导航到详细信息页面。

  • 详细信息页面:显示有关图像的详细信息,包括全尺寸图像和标题。您可以通过按后退按钮或关闭按钮返回主页(这两个按钮做同样的事情但有不同的实现)

 

下面的动画 GIF 截图清楚地描绘了我们的最终产品。在页面之间导航时将您的意图转向动画(图像被轻轻放大/缩小):

编码

以下是main.dart中的完整源代码,并在注释中进行了解释。为简单起见,我将所有内容都放在一个文件中。但是,您可以通过将每个屏幕放在一个单独的文件中来保持代码的良好组织(这不是必须做的,但被认为是很好的做法)。

 

// main.dart
import 'package:flutter/material.dart';
​
void main() {
  runApp(const MyApp());
}
​
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Remove the debug banner
      debugShowCheckedModeBanner: false,
      title: 'KindaCode.com',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const KindaCodeDemo(),
    );
  }
}
​
// implement the main page
class KindaCodeDemo extends StatefulWidget {
  const KindaCodeDemo({Key? key}) : super(key: key);
​
  @override
  State<KindaCodeDemo> createState() => _KindaCodeDemoState();
}
​
class _KindaCodeDemoState extends State<KindaCodeDemo> {
  // dummy data to feed the grid view
  // list of images
  final List<Map<String, dynamic>> _images = [
    {
      'id': 'i1',
      'title': 'One',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/1.png'
    },
    {
      'id': 'i2',
      'title': 'Two',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/2.png'
    },
    {
      'id': 'i3',
      'title': 'Three',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/3.jpg'
    },
    {
      'id': 'i4',
      'title': 'Four',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/4.jpg'
    },
    {
      'id': 'i5',
      'title': 'Five',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/5.jpg'
    },
    {
      'id': 'i6',
      'title': 'Six',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/6.jpg'
    }
  ];
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('KindaCode.com')),
      body: Container(
        padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
        // implement the grid view
        child: GridView.builder(
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 200,
                childAspectRatio: 3 / 2,
                crossAxisSpacing: 20,
                mainAxisSpacing: 20),
            itemCount: _images.length,
            itemBuilder: (BuildContext ctx, index) {
              return InkWell(
                key: ValueKey(_images[index]['id']),
                // navigate to the detail page on tap
                onTap: () {
                  Navigator.of(context).push(MaterialPageRoute(
                      // pass the image data to the detail page
                      builder: (context) => DetailPage(_images[index])));
                },
                // implement the Hero widget
                child: Hero(
                  // important: the tag
                  tag: _images[index]['id'],
                  // display the image
                  child: Image.network(
                    _images[index]['url'],
                    fit: BoxFit.cover,
                  ),
                ),
              );
            }),
      ),
    );
  }
}
​
// detail page
class DetailPage extends StatelessWidget {
  // get the image data that was passed from the main page
  final Map image;
  const DetailPage(this.image, {Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      // implement the app bar
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        title: Text(image['title']),
        actions: [
          // this button does the same thing as the back button
          // I put it here to show you how to go back to the previous screen by implementing your own code
          IconButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              icon: const Icon(Icons.close))
        ],
      ),
      body: Center(
        // implement the Hero widget
        child: Hero(
          // important: the tag
          tag: image['id'],
          child: Image.network(image['url']),
        ),
      ),
    );
  }
}

结论

您已经了解了有关 Hero 小部件的基础知识,这并不难实现,但会产生丰硕的成果。同样,这可能是多余的,但重要的是要记住您必须为 Hero 小部件设置标签。

...全文
201 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

1,471

社区成员

发帖
与我相关
我的任务
社区描述
加入“谷歌开发者”社区,一起“共码未来。
android 企业社区
社区管理员
  • 谷歌开发者
  • 开发者大赛发布
  • 活动通知
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧