引言

Hero动画是Flutter中最具特色的动画效果之一,它允许元素在页面之间平滑过渡。通过Hero动画,我们可以创建令人印象深刻的用户体验,使页面切换更加流畅和直观。

一、Hero动画基础

1.1 什么是Hero动画

Hero动画是一种共享元素过渡效果,允许同一个Widget在两个页面之间平滑移动和变形。

// 源页面
Hero(
  tag: 'image-hero',
  child: Image.network('https://example.com/image.jpg'),
)

// 目标页面
Hero(
  tag: 'image-hero',
  child: Image.network('https://example.com/image.jpg'),
)

1.2 核心概念

概念 说明
Hero 共享元素Widget
tag 唯一标识符,用于匹配两个页面的Hero
flightShuttleBuilder 自定义过渡期间的Widget
placeholderBuilder 源页面的占位符
transitionOnUserGestures 是否响应用户手势

1.3 基本用法

// 页面1
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: GestureDetector(
        onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const DetailPage())),
        child: Hero(
          tag: 'avatar',
          child: CircleAvatar(
            backgroundImage: const NetworkImage('https://example.com/avatar.jpg'),
            radius: 50,
          ),
        ),
      ),
    );
  }
}

// 页面2
class DetailPage extends StatelessWidget {
  const DetailPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Detail')),
      body: Center(
        child: Hero(
          tag: 'avatar',
          child: CircleAvatar(
            backgroundImage: const NetworkImage('https://example.com/avatar.jpg'),
            radius: 100,
          ),
        ),
      ),
    );
  }
}

二、高级Hero动画

2.1 自定义飞行路径

Hero(
  tag: 'custom-hero',
  flightShuttleBuilder: (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
    return ScaleTransition(
      scale: animation.drive(
        Tween<double>(begin: 1.0, end: 1.5).chain(CurveTween(curve: Curves.easeOut)),
      ),
      child: const Icon(Icons.star, size: 100, color: Colors.yellow),
    );
  },
  child: const Icon(Icons.star, size: 50, color: Colors.yellow),
)

2.2 形状变化动画

// 圆形到矩形的过渡
Hero(
  tag: 'shape-transition',
  child: Container(
    width: 100,
    height: 100,
    decoration: const BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(50),
    ),
  ),
)

// 目标页面
Hero(
  tag: 'shape-transition',
  child: Container(
    width: 300,
    height: 200,
    decoration: const BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(16),
    ),
  ),
)

2.3 渐变背景过渡

Hero(
  tag: 'gradient-hero',
  child: Container(
    width: 150,
    height: 150,
    decoration: const BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.pink, Colors.purple],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(20),
    ),
  ),
)

三、复杂场景

3.1 多个Hero动画

// 源页面
Row(
  children: [
    Hero(tag: 'image', child: Image.network('https://example.com/img.jpg')),
    Hero(tag: 'title', child: const Text('Title')),
  ],
)

// 目标页面
Column(
  children: [
    Hero(tag: 'image', child: Image.network('https://example.com/img.jpg')),
    Hero(tag: 'title', child: const Text('Title')),
  ],
)

3.2 嵌套Hero动画

Hero(
  tag: 'outer',
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
    child: Hero(
      tag: 'inner',
      child: Container(
        width: 100,
        height: 100,
        color: Colors.white,
      ),
    ),
  ),
)

3.3 自定义过渡动画

Hero(
  tag: 'custom-transition',
  createRectTween: (begin, end) {
    return CustomRectTween(begin: begin!, end: end!);
  },
  child: const Icon(Icons.flight),
)

class CustomRectTween extends RectTween {
  CustomRectTween({required super.begin, required super.end});

  @override
  Rect lerp(double t) {
    final curvedT = Curves.easeInOut.transform(t);
    return Rect.lerp(begin, end, curvedT)!;
  }
}

四、实战案例

4.1 图片画廊

class GalleryPage extends StatelessWidget {
  const GalleryPage({super.key});

  @override
  Widget build(BuildContext context) {
    final images = [
      'https://example.com/img1.jpg',
      'https://example.com/img2.jpg',
      'https://example.com/img3.jpg',
    ];

    return GridView.builder(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 10,
        mainAxisSpacing: 10,
      ),
      itemCount: images.length,
      itemBuilder: (context, index) {
        return GestureDetector(
          onTap: () => Navigator.push(
            context,
            PageRouteBuilder(
              pageBuilder: (_, __, ___) => DetailImagePage(url: images[index]),
              transitionsBuilder: (_, animation, __, child) {
                return FadeTransition(opacity: animation, child: child);
              },
            ),
          ),
          child: Hero(
            tag: images[index],
            child: Image.network(images[index], fit: BoxFit.cover),
          ),
        );
      },
    );
  }
}

4.2 卡片展开效果

class CardHero extends StatelessWidget {
  const CardHero({super.key});

  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: 'card',
      child: Material(
        elevation: 4,
        borderRadius: BorderRadius.circular(16),
        child: Container(
          width: 200,
          height: 150,
          padding: const EdgeInsets.all(16),
          child: const Column(
            children: [
              Text('Card Title', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
              SizedBox(height: 8),
              Text('Card description text'),
            ],
          ),
        ),
      ),
    );
  }
}

4.3 圆形头像到全屏

// 源页面
Hero(
  tag: 'profile',
  child: const CircleAvatar(
    backgroundImage: NetworkImage('https://example.com/profile.jpg'),
    radius: 30,
  ),
)

// 目标页面
Hero(
  tag: 'profile',
  child: Container(
    width: MediaQuery.of(context).size.width,
    height: MediaQuery.of(context).size.height * 0.5,
    decoration: const BoxDecoration(
      image: DecorationImage(
        image: NetworkImage('https://example.com/profile.jpg'),
        fit: BoxFit.cover,
      ),
    ),
  ),
)

五、性能优化

5.1 使用RepaintBoundary

Hero(
  tag: 'optimized-hero',
  child: RepaintBoundary(
    child: Image.network('https://example.com/image.jpg'),
  ),
)

5.2 避免复杂Widget

// 避免:复杂Widget作为Hero child
Hero(
  tag: 'bad-hero',
  child: Container(
    child: Column(
      children: [/* 复杂内容 */],
    ),
  ),
)

// 推荐:使用简单Widget
Hero(
  tag: 'good-hero',
  child: Image.network('https://example.com/image.jpg'),
)

5.3 使用transitionOnUserGestures

Hero(
  tag: 'gesture-hero',
  transitionOnUserGestures: true,
  child: const Icon(Icons.share),
)

六、最佳实践

6.1 标签命名规范

// 推荐:使用有意义的唯一标签
Hero(tag: 'product-image-${product.id}', child: ...)

// 避免:重复或无意义的标签
Hero(tag: 'image', child: ...)

6.2 过渡一致性

// 保持源和目标的内容一致
Hero(tag: 'avatar', child: Image.network(url))
// 目标页面也使用相同的图片
Hero(tag: 'avatar', child: Image.network(url))

6.3 测试动画效果

void main() {
  testWidgets('Hero animation works', (tester) async {
    await tester.pumpWidget(MaterialApp(home: const HomePage()));
    
    await tester.tap(find.byType(Hero));
    await tester.pumpAndSettle();
    
    expect(find.byType(DetailPage), findsOneWidget);
  });
}

七、总结

Hero动画是Flutter中强大的过渡效果,能够创建流畅的页面切换体验。通过合理使用Hero动画,可以提升应用的用户体验。

关键要点:

  • 使用唯一的tag标识Hero
  • 保持源和目标Widget的一致性
  • 使用flightShuttleBuilder自定义过渡
  • 考虑性能优化
  • 测试动画效果

掌握Hero动画,将使你的Flutter应用更加生动和专业。

更多推荐