跨页面切换的动效 Widget (Hero animations)

在页面跳转过程中给用户加以引导是非常有用的。实现引导的一种通用做法是在页面切换时为某个组件加上转场动画,从而在两个页面间建立视觉上的锚定关联。

It’s often helpful to guide users through an app as they navigate from screen to screen. A common technique to lead users through an app is to animate a widget from one screen to the next. This creates a visual anchor connecting the two screens.

在 Flutter 中,可以通过 Hero 组件实现页面切换时组件的转场动画。

This recipe uses the following steps: 这个教程将包含以下步骤:

Use the Hero widget to animate a widget from one screen to the next.

  1. 创建两个页面,展示相同的图片

    Create two screens showing the same image.

  2. 在第一个页面中加入 Hero 组件

    Add a Hero widget to the first screen.

  3. 在第二个页面中加入 Hero 组件

    Add a Hero widget to the second screen.

1. 创建两个页面,展示相同的图片

1. Create two screens showing the same image

在这个示例中,将在两个页面中展示相同的图片。当用户在第一个页面点击图片,会通过一个转场动画切换到第二个页面。现在,我们将会创建页面的视觉结构,并在后续步骤中处理动画。

In this example, display the same image on both screens. Animate the image from the first screen to the second screen when the user taps the image. For now, create the visual structure; handle animations in the next steps.

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Screen'),
      ),
      body: GestureDetector(
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return DetailScreen();
          }));
        },
        child: Image.network(
          'https://picsum.photos/250?image=9',
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: () {
          Navigator.pop(context);
        },
        child: Center(
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
      ),
    );
  }
}

2. 在第一个页面中加入 Hero 组件

2. Add a Hero widget to the first screen

为了通过动画在两个页面间建立联系,需要把每个页面的 Image 组件都包裹进 Hero 组件里面。Hero 组件有两个参数:

To connect the two screens together with an animation, wrap the Image widget on both screens in a Hero widget. The Hero widget requires two arguments:

`tag`

作为 `Hero` 组件的标识,在这两个页面中必须相同。

An object that identifies the `Hero`. It must be the same on both screens.

`child`

在两个屏幕直接跨越的那个 widget。

The widget to animate across screens.

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);

3. 在第二个页面中加入 Hero 组件

3. Add a Hero widget to the second screen

为了完善与第一个页面的关联,同样需要把第二个页面中的 Image 组件包裹进 Hero 组件里面。它的 tag 也必须和第一个页面相同。

To complete the connection with the first screen, wrap the Image on the second screen with a Hero widget that has the same tag as the Hero in the first screen.

Hero 组件被应用到第二个页面后,页面的转场动画就生效了。

After applying the Hero widget to the second screen, the animation between screens just works.

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);

完整样例

Complete example

import 'package:flutter/material.dart';

void main() => runApp(HeroApp());

class HeroApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Transition Demo',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Screen'),
      ),
      body: GestureDetector(
        child: Hero(
          tag: 'imageHero',
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return DetailScreen();
          }));
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        child: Center(
          child: Hero(
            tag: 'imageHero',
            child: Image.network(
              'https://picsum.photos/250?image=9',
            ),
          ),
        ),
        onTap: () {
          Navigator.pop(context);
        },
      ),
    );
  }
}

Hero demo