进阶实战 Flutter for OpenHarmony:Hero 动画转场系统 - 页面过渡动画实现

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


一、Hero 动画系统架构深度解析

在现代移动应用中,页面转场动画是提升用户体验的重要手段。从简单的共享元素过渡到复杂的多元素联动,Flutter 提供了 Hero 组件来实现各种转场动画效果。理解这套架构的底层原理,是构建高性能转场系统的基础。

📱 1.1 Flutter Hero 架构

Flutter 的 Hero 动画系统由多个核心层次组成,每一层都有其特定的职责:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      应用层 (Application Layer)                  │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Hero, HeroController, HeroMode...                      │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                   │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              动画层 (Animation Layer)                     │    │
│  │  AnimationController, Tween, CurvedAnimation...         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                   │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              转场层 (Transition Layer)                    │    │
│  │  HeroFlightShader, HeroFlight, RectTween...             │    │
│  └─────────────────────────────────────────────────────────┘    │
│                              │                                   │
│                              ▼                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │              导航层 (Navigation Layer)                    │    │
│  │  Navigator, Route, PageRoute, MaterialPageRoute...      │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

🔬 1.2 Hero 核心组件详解

Flutter Hero 动画系统的核心组件包括以下几个部分:

Hero(英雄组件)

Hero 是实现共享元素转场的核心组件,通过 tag 标识匹配的元素。

dart 复制代码
Hero(
  tag: 'image-hero',
  child: Image.network('https://example.com/image.jpg'),
)

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

HeroController(英雄控制器)

HeroController 用于管理 Hero 动画的生命周期。

dart 复制代码
MaterialApp(
  navigatorObservers: [
    HeroController(),
  ],
)

HeroFlight(英雄飞行)

HeroFlight 管理两个 Hero 之间的过渡动画。

dart 复制代码
class CustomHeroFlight {
  void start() {
    // 开始飞行动画
  }

  void end() {
    // 结束飞行动画
  }
}

🎯 1.3 Hero 动画设计原则

设计优秀的 Hero 动画需要遵循以下原则:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Hero 动画设计原则                         │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐   │
│  │  1. 视觉连续性 - 元素在页面间平滑过渡                │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  2. 时间协调 - 动画时长与页面转场协调                │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  3. 元素匹配 - 使用唯一的 tag 标识                  │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  4. 性能优化 - 避免复杂的 Hero 动画                  │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  5. 上下文相关 - 动画与内容相关联                    │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Hero 动画类型对比:

类型 特点 适用场景
基础 Hero 简单位置过渡 图片详情页
自定义 Hero 自定义动画效果 品牌定制
多元素 Hero 多个元素联动 列表详情页
复杂转场 组合多种效果 特色页面
双向 Hero 支持返回动画 详情页面

二、基础 Hero 动画实现

基础 Hero 动画包括简单共享元素、图片详情转场和卡片展开动画。这些是构建复杂转场系统的基础。

👆 2.1 简单共享元素动画

简单共享元素动画是最基础的 Hero 动画形式,实现元素在页面间的平滑过渡。

dart 复制代码
import 'package:flutter/material.dart';

/// 简单共享元素动画示例
class SimpleHeroDemo extends StatelessWidget {
  const SimpleHeroDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('简单 Hero 动画')),
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => const DetailPage(),
              ),
            );
          },
          child: Hero(
            tag: 'simple-hero',
            child: Container(
              width: 150,
              height: 150,
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(12),
                boxShadow: [
                  BoxShadow(
                    color: Colors.blue.withOpacity(0.3),
                    blurRadius: 10,
                    offset: const Offset(0, 5),
                  ),
                ],
              ),
              child: const Center(
                child: Text(
                  '点击查看详情',
                  style: TextStyle(color: Colors.white, fontSize: 16),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('详情页')),
      body: Center(
        child: Hero(
          tag: 'simple-hero',
          child: Container(
            width: 300,
            height: 300,
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(24),
              boxShadow: [
                BoxShadow(
                  color: Colors.blue.withOpacity(0.5),
                  blurRadius: 20,
                  offset: const Offset(0, 10),
                ),
              ],
            ),
            child: const Center(
              child: Text(
                '详情内容',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

🔄 2.2 图片详情转场

图片详情转场是 Hero 动画最常见的应用场景。

dart 复制代码
/// 图片详情转场示例
class ImageHeroDemo extends StatelessWidget {
  const ImageHeroDemo({super.key});

  final List<String> images = const [
    'https://picsum.photos/seed/1/400/300',
    'https://picsum.photos/seed/2/400/300',
    'https://picsum.photos/seed/3/400/300',
    'https://picsum.photos/seed/4/400/300',
    'https://picsum.photos/seed/5/400/300',
    'https://picsum.photos/seed/6/400/300',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('图片 Hero 动画')),
      body: GridView.builder(
        padding: const EdgeInsets.all(8),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 1.2,
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
        ),
        itemCount: images.length,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ImageDetailPage(
                    imageUrl: images[index],
                    tag: 'image-$index',
                  ),
                ),
              );
            },
            child: Hero(
              tag: 'image-$index',
              child: ClipRRect(
                borderRadius: BorderRadius.circular(12),
                child: Image.network(
                  images[index],
                  fit: BoxFit.cover,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class ImageDetailPage extends StatelessWidget {
  final String imageUrl;
  final String tag;

  const ImageDetailPage({
    super.key,
    required this.imageUrl,
    required this.tag,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: GestureDetector(
        onTap: () => Navigator.pop(context),
        child: Center(
          child: Hero(
            tag: tag,
            child: Image.network(
              imageUrl,
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}

🌊 2.3 卡片展开动画

卡片展开动画实现从列表卡片到详情页的平滑过渡。

dart 复制代码
/// 卡片展开动画示例
class CardHeroDemo extends StatelessWidget {
  const CardHeroDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('卡片 Hero 动画')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 10,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => CardDetailPage(index: index),
                ),
              );
            },
            child: Hero(
              tag: 'card-$index',
              child: Card(
                margin: const EdgeInsets.only(bottom: 16),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(16),
                ),
                child: Container(
                  height: 120,
                  padding: const EdgeInsets.all(16),
                  child: Row(
                    children: [
                      Container(
                        width: 80,
                        height: 80,
                        decoration: BoxDecoration(
                          color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2),
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Icon(
                          Icons.star,
                          color: Colors.primaries[index % Colors.primaries.length],
                          size: 40,
                        ),
                      ),
                      const SizedBox(width: 16),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              '卡片标题 ${index + 1}',
                              style: const TextStyle(
                                fontSize: 16,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const SizedBox(height: 4),
                            Text(
                              '这是卡片的描述信息',
                              style: TextStyle(
                                color: Colors.grey[600],
                              ),
                            ),
                          ],
                        ),
                      ),
                      const Icon(Icons.chevron_right),
                    ],
                  ),
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class CardDetailPage extends StatelessWidget {
  final int index;

  const CardDetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 200,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text('卡片 ${index + 1}'),
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Colors.primaries[index % Colors.primaries.length],
                      Colors.primaries[(index + 1) % Colors.primaries.length],
                    ],
                  ),
                ),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Hero(
                    tag: 'card-$index',
                    flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
                      return AnimatedBuilder(
                        animation: animation,
                        builder: (context, child) {
                          return Container(
                            decoration: BoxDecoration(
                              color: Colors.primaries[index % Colors.primaries.length].withOpacity(animation.value),
                              borderRadius: BorderRadius.circular(16),
                            ),
                          );
                        },
                      );
                    },
                    child: Container(
                      width: double.infinity,
                      height: 150,
                      decoration: BoxDecoration(
                        color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2),
                        borderRadius: BorderRadius.circular(16),
                      ),
                      child: Center(
                        child: Icon(
                          Icons.star,
                          color: Colors.primaries[index % Colors.primaries.length],
                          size: 60,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 24),
                  const Text(
                    '详情内容',
                    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 16),
                  Text(
                    '这是卡片 ${index + 1} 的详细内容。Hero 动画让页面之间的过渡更加自然流畅。',
                    style: TextStyle(color: Colors.grey[600]),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

三、高级 Hero 动画实现

高级 Hero 动画包括自定义飞行动画、多元素联动、复杂转场效果和双向 Hero 动画。

📊 3.1 自定义飞行动画

自定义飞行动画通过 flightShuttleBuilder 实现独特的过渡效果。

dart 复制代码
/// 自定义飞行动画示例
class CustomFlightDemo extends StatelessWidget {
  const CustomFlightDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('自定义飞行动画')),
      body: Center(
        child: GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => const CustomFlightDetailPage(),
              ),
            );
          },
          child: Hero(
            tag: 'custom-flight',
            flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
              return AnimatedBuilder(
                animation: animation,
                builder: (context, child) {
                  return Transform(
                    transform: Matrix4.identity()
                      ..setEntry(3, 2, 0.001)
                      ..rotateY(animation.value * 3.14159),
                    alignment: Alignment.center,
                    child: Container(
                      decoration: BoxDecoration(
                        color: Color.lerp(Colors.blue, Colors.purple, animation.value),
                        borderRadius: BorderRadius.circular(12 + animation.value * 12),
                        boxShadow: [
                          BoxShadow(
                            color: Color.lerp(Colors.blue, Colors.purple, animation.value)!.withOpacity(0.5),
                            blurRadius: 10 + animation.value * 20,
                            offset: Offset(0, 5 + animation.value * 10),
                          ),
                        ],
                      ),
                    ),
                  );
                },
              );
            },
            child: Container(
              width: 150,
              height: 150,
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Center(
                child: Text('自定义飞行', style: TextStyle(color: Colors.white)),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('详情页')),
      body: Center(
        child: Hero(
          tag: 'custom-flight',
          flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
            return AnimatedBuilder(
              animation: animation,
              builder: (context, child) {
                return Transform(
                  transform: Matrix4.identity()
                    ..setEntry(3, 2, 0.001)
                    ..rotateY(animation.value * 3.14159),
                  alignment: Alignment.center,
                  child: Container(
                    decoration: BoxDecoration(
                      color: Color.lerp(Colors.purple, Colors.blue, animation.value),
                      borderRadius: BorderRadius.circular(24 - animation.value * 12),
                    ),
                  ),
                );
              },
            );
          },
          child: Container(
            width: 300,
            height: 300,
            decoration: BoxDecoration(
              color: Colors.purple,
              borderRadius: BorderRadius.circular(24),
            ),
            child: const Center(
              child: Text('飞行完成', style: TextStyle(color: Colors.white, fontSize: 24)),
            ),
          ),
        ),
      ),
    );
  }
}

📝 3.2 多元素联动动画

多元素联动动画实现多个 Hero 元素的协同过渡。

dart 复制代码
/// 多元素联动动画示例
class MultiHeroDemo extends StatelessWidget {
  const MultiHeroDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('多元素联动')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 5,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => MultiHeroDetailPage(index: index),
                ),
              );
            },
            child: Container(
              margin: const EdgeInsets.only(bottom: 16),
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(16),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.1),
                    blurRadius: 10,
                  ),
                ],
              ),
              child: Row(
                children: [
                  Hero(
                    tag: 'avatar-$index',
                    child: CircleAvatar(
                      radius: 30,
                      backgroundColor: Colors.primaries[index % Colors.primaries.length],
                      child: Text('${index + 1}'),
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Hero(
                          tag: 'title-$index',
                          flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
                            return DefaultTextStyle(
                              style: TextStyle(
                                fontSize: 14 + animation.value * 10,
                                fontWeight: FontWeight.bold,
                                color: Colors.black,
                              ),
                              child: toHeroContext.widget,
                            );
                          },
                          child: Text(
                            '用户 ${index + 1}',
                            style: const TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                        const SizedBox(height: 4),
                        Hero(
                          tag: 'subtitle-$index',
                          child: Text(
                            '这是用户的描述信息',
                            style: TextStyle(color: Colors.grey[600]),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

class MultiHeroDetailPage extends StatelessWidget {
  final int index;

  const MultiHeroDetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 250,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Colors.primaries[index % Colors.primaries.length],
                      Colors.primaries[(index + 1) % Colors.primaries.length],
                    ],
                  ),
                ),
                child: Center(
                  child: Hero(
                    tag: 'avatar-$index',
                    child: CircleAvatar(
                      radius: 50,
                      backgroundColor: Colors.white,
                      child: Text(
                        '${index + 1}',
                        style: const TextStyle(fontSize: 32),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Hero(
                    tag: 'title-$index',
                    child: const Text(
                      '',
                      style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                    ),
                  ),
                  Text(
                    '用户 ${index + 1}',
                    style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  Hero(
                    tag: 'subtitle-$index',
                    child: Text(
                      '',
                      style: TextStyle(color: Colors.grey[600]),
                    ),
                  ),
                  Text(
                    '这是用户的详细描述信息',
                    style: TextStyle(color: Colors.grey[600]),
                  ),
                  const SizedBox(height: 24),
                  const Text(
                    '详细内容',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 8),
                  const Text(
                    '这是用户的详细内容。多元素 Hero 动画让页面之间的过渡更加自然流畅,多个元素协同运动,提供更好的视觉体验。',
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

🔄 3.3 复杂转场效果

复杂转场效果组合多种动画实现独特的过渡体验。

dart 复制代码
/// 复杂转场效果示例
class ComplexTransitionDemo extends StatelessWidget {
  const ComplexTransitionDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('复杂转场效果')),
      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 0.8,
          crossAxisSpacing: 16,
          mainAxisSpacing: 16,
        ),
        itemCount: 6,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                PageRouteBuilder(
                  pageBuilder: (context, animation, secondaryAnimation) {
                    return ComplexDetailPage(index: index);
                  },
                  transitionsBuilder: (context, animation, secondaryAnimation, child) {
                    return FadeTransition(
                      opacity: animation,
                      child: ScaleTransition(
                        scale: Tween<double>(begin: 0.8, end: 1.0).animate(
                          CurvedAnimation(parent: animation, curve: Curves.easeOut),
                        ),
                        child: child,
                      ),
                    );
                  },
                  transitionDuration: const Duration(milliseconds: 500),
                ),
              );
            },
            child: Hero(
              tag: 'complex-$index',
              child: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Colors.primaries[index % Colors.primaries.length],
                      Colors.primaries[(index + 1) % Colors.primaries.length],
                    ],
                  ),
                  borderRadius: BorderRadius.circular(16),
                  boxShadow: [
                    BoxShadow(
                      color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.3),
                      blurRadius: 10,
                      offset: const Offset(0, 5),
                    ),
                  ],
                ),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(
                      Icons.star,
                      size: 40,
                      color: Colors.white.withOpacity(0.8),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      '项目 ${index + 1}',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

class ComplexDetailPage extends StatelessWidget {
  final int index;

  const ComplexDetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Hero(
            tag: 'complex-$index',
            child: Container(
              width: double.infinity,
              height: 300,
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                  colors: [
                    Colors.primaries[index % Colors.primaries.length],
                    Colors.primaries[(index + 1) % Colors.primaries.length],
                  ],
                ),
              ),
            ),
          ),
          SafeArea(
            child: Column(
              children: [
                AppBar(
                  backgroundColor: Colors.transparent,
                  elevation: 0,
                  leading: IconButton(
                    icon: const Icon(Icons.arrow_back, color: Colors.white),
                    onPressed: () => Navigator.pop(context),
                  ),
                ),
                const SizedBox(height: 100),
                Expanded(
                  child: Container(
                    decoration: const BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
                    ),
                    child: Padding(
                      padding: const EdgeInsets.all(24),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            '项目 ${index + 1}',
                            style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(height: 8),
                          Text(
                            '这是项目的详细描述',
                            style: TextStyle(color: Colors.grey[600]),
                          ),
                          const SizedBox(height: 24),
                          const Text(
                            '详细内容',
                            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                          ),
                          const SizedBox(height: 8),
                          const Expanded(
                            child: Text(
                              '复杂转场效果结合了 Hero 动画和自定义页面转场,提供更加丰富的视觉体验。',
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

四、完整示例:Hero 动画转场系统

下面是一个完整的 Hero 动画转场系统示例:

dart 复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HeroHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('🦸 Hero 动画转场系统')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _buildSectionCard(context, title: '简单 Hero', description: '基础共享元素', icon: Icons.star, color: Colors.blue, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SimpleHeroDemo()))),
          _buildSectionCard(context, title: '图片详情', description: '图片放大转场', icon: Icons.image, color: Colors.teal, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ImageHeroDemo()))),
          _buildSectionCard(context, title: '卡片展开', description: '列表卡片转场', icon: Icons.view_agenda, color: Colors.green, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CardHeroDemo()))),
          _buildSectionCard(context, title: '自定义飞行', description: '自定义过渡效果', icon: Icons.flight, color: Colors.purple, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CustomFlightDemo()))),
          _buildSectionCard(context, title: '多元素联动', description: '多个 Hero 协同', icon: Icons.people, color: Colors.orange, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const MultiHeroDemo()))),
          _buildSectionCard(context, title: '复杂转场', description: '组合动画效果', icon: Icons.auto_awesome, color: Colors.pink, onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ComplexTransitionDemo()))),
        ],
      ),
    );
  }

  Widget _buildSectionCard(BuildContext context, {required String title, required String description, required IconData icon, required Color color, required VoidCallback onTap}) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Container(width: 56, height: 56, decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)), child: Icon(icon, color: color, size: 28)),
              const SizedBox(width: 16),
              Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(description, style: TextStyle(fontSize: 13, color: Colors.grey[600]))])),
              Icon(Icons.chevron_right, color: Colors.grey[400]),
            ],
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('简单 Hero 动画')),
      body: Center(
        child: GestureDetector(
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const SimpleDetailPage())),
          child: Hero(
            tag: 'simple-hero',
            child: Container(
              width: 150,
              height: 150,
              decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(12)),
              child: const Center(child: Text('点击查看详情', style: TextStyle(color: Colors.white, fontSize: 16))),
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('详情页')),
      body: Center(
        child: Hero(
          tag: 'simple-hero',
          child: Container(
            width: 300,
            height: 300,
            decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(24)),
            child: const Center(child: Text('详情内容', style: TextStyle(color: Colors.white, fontSize: 24))),
          ),
        ),
      ),
    );
  }
}

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

  final List<String> images = const [
    'https://picsum.photos/seed/1/400/300',
    'https://picsum.photos/seed/2/400/300',
    'https://picsum.photos/seed/3/400/300',
    'https://picsum.photos/seed/4/400/300',
    'https://picsum.photos/seed/5/400/300',
    'https://picsum.photos/seed/6/400/300',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('图片 Hero 动画')),
      body: GridView.builder(
        padding: const EdgeInsets.all(8),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 1.2, crossAxisSpacing: 8, mainAxisSpacing: 8),
        itemCount: images.length,
        itemBuilder: (context, index) => GestureDetector(
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => ImageDetailPage(imageUrl: images[index], tag: 'image-$index'))),
          child: Hero(tag: 'image-$index', child: ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.network(images[index], fit: BoxFit.cover))),
        ),
      ),
    );
  }
}

class ImageDetailPage extends StatelessWidget {
  final String imageUrl;
  final String tag;

  const ImageDetailPage({super.key, required this.imageUrl, required this.tag});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(backgroundColor: Colors.transparent, elevation: 0, iconTheme: const IconThemeData(color: Colors.white)),
      body: GestureDetector(
        onTap: () => Navigator.pop(context),
        child: Center(child: Hero(tag: tag, child: Image.network(imageUrl, fit: BoxFit.contain))),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('卡片 Hero 动画')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 10,
        itemBuilder: (context, index) => GestureDetector(
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => CardDetailPage(index: index))),
          child: Hero(
            tag: 'card-$index',
            child: Card(
              margin: const EdgeInsets.only(bottom: 16),
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
              child: Container(
                height: 120,
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    Container(width: 80, height: 80, decoration: BoxDecoration(color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2), borderRadius: BorderRadius.circular(12))),
                    const SizedBox(width: 16),
                    Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [Text('卡片标题 ${index + 1}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text('这是卡片的描述信息', style: TextStyle(color: Colors.grey[600]))])),
                    const Icon(Icons.chevron_right),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class CardDetailPage extends StatelessWidget {
  final int index;

  const CardDetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 200,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text('卡片 ${index + 1}'),
              background: Container(decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]]))),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Hero(
                    tag: 'card-$index',
                    child: Container(
                      width: double.infinity,
                      height: 150,
                      decoration: BoxDecoration(color: Colors.primaries[index % Colors.primaries.length].withOpacity(0.2), borderRadius: BorderRadius.circular(16)),
                    ),
                  ),
                  const SizedBox(height: 24),
                  const Text('详情内容', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 16),
                  Text('这是卡片 ${index + 1} 的详细内容。Hero 动画让页面之间的过渡更加自然流畅。', style: TextStyle(color: Colors.grey[600])),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('自定义飞行动画')),
      body: Center(
        child: GestureDetector(
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const CustomFlightDetailPage())),
          child: Hero(
            tag: 'custom-flight',
            flightShuttleBuilder: (flightContext, animation, flightDirection, fromHeroContext, toHeroContext) => AnimatedBuilder(
              animation: animation,
              builder: (context, child) => Transform(
                transform: Matrix4.identity()..setEntry(3, 2, 0.001)..rotateY(animation.value * 3.14159),
                alignment: Alignment.center,
                child: Container(decoration: BoxDecoration(color: Color.lerp(Colors.blue, Colors.purple, animation.value), borderRadius: BorderRadius.circular(12 + animation.value * 12))),
              ),
            ),
            child: Container(width: 150, height: 150, decoration: BoxDecoration(color: Colors.blue, borderRadius: BorderRadius.circular(12)), child: const Center(child: Text('自定义飞行', style: TextStyle(color: Colors.white)))),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('详情页')),
      body: Center(
        child: Hero(
          tag: 'custom-flight',
          child: Container(width: 300, height: 300, decoration: BoxDecoration(color: Colors.purple, borderRadius: BorderRadius.circular(24)), child: const Center(child: Text('飞行完成', style: TextStyle(color: Colors.white, fontSize: 24)))),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('多元素联动')),
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 5,
        itemBuilder: (context, index) => GestureDetector(
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => MultiHeroDetailPage(index: index))),
          child: Container(
            margin: const EdgeInsets.only(bottom: 16),
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10)]),
            child: Row(
              children: [
                Hero(tag: 'avatar-$index', child: CircleAvatar(radius: 30, backgroundColor: Colors.primaries[index % Colors.primaries.length], child: Text('${index + 1}'))),
                const SizedBox(width: 16),
                Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Hero(tag: 'title-$index', child: Text('用户 ${index + 1}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold))), const SizedBox(height: 4), Hero(tag: 'subtitle-$index', child: Text('这是用户的描述信息', style: TextStyle(color: Colors.grey[600])))])),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class MultiHeroDetailPage extends StatelessWidget {
  final int index;

  const MultiHeroDetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 250,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]])),
                child: Center(child: Hero(tag: 'avatar-$index', child: CircleAvatar(radius: 50, backgroundColor: Colors.white, child: Text('${index + 1}', style: const TextStyle(fontSize: 32))))),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('用户 ${index + 1}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  Text('这是用户的详细描述信息', style: TextStyle(color: Colors.grey[600])),
                  const SizedBox(height: 24),
                  const Text('详细内容', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 8),
                  const Text('多元素 Hero 动画让页面之间的过渡更加自然流畅,多个元素协同运动,提供更好的视觉体验。'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('复杂转场效果')),
      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 0.8, crossAxisSpacing: 16, mainAxisSpacing: 16),
        itemCount: 6,
        itemBuilder: (context, index) => GestureDetector(
          onTap: () => Navigator.push(
            context,
            PageRouteBuilder(
              pageBuilder: (context, animation, secondaryAnimation) => ComplexDetailPage(index: index),
              transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: ScaleTransition(scale: Tween<double>(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)), child: child)),
              transitionDuration: const Duration(milliseconds: 500),
            ),
          ),
          child: Hero(
            tag: 'complex-$index',
            child: Container(
              decoration: BoxDecoration(
                gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]]),
                borderRadius: BorderRadius.circular(16),
              ),
              child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.star, size: 40, color: Colors.white.withOpacity(0.8)), const SizedBox(height: 8), Text('项目 ${index + 1}', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold))]),
            ),
          ),
        ),
      ),
    );
  }
}

class ComplexDetailPage extends StatelessWidget {
  final int index;

  const ComplexDetailPage({super.key, required this.index});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Hero(tag: 'complex-$index', child: Container(width: double.infinity, height: 300, decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.primaries[index % Colors.primaries.length], Colors.primaries[(index + 1) % Colors.primaries.length]])))),
          SafeArea(
            child: Column(
              children: [
                AppBar(backgroundColor: Colors.transparent, elevation: 0, leading: IconButton(icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context))),
                const SizedBox(height: 100),
                Expanded(
                  child: Container(
                    decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
                    child: Padding(
                      padding: const EdgeInsets.all(24),
                      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text('项目 ${index + 1}', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text('这是项目的详细描述', style: TextStyle(color: Colors.grey[600])), const SizedBox(height: 24), const Text('详细内容', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), const Expanded(child: Text('复杂转场效果结合了 Hero 动画和自定义页面转场,提供更加丰富的视觉体验。'))]),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

五、最佳实践与性能优化

🎨 5.1 性能优化建议

  1. 避免过多 Hero 元素:每个页面限制 Hero 元素数量
  2. 使用唯一的 tag:确保 Hero tag 在页面间唯一
  3. 优化动画时长:动画时长控制在 300-500ms
  4. 避免复杂布局:Hero 元素内部布局尽量简单

🔧 5.2 Hero 动画调试

dart 复制代码
MaterialApp(
  debugShowCheckedModeBanner: false,
  navigatorObservers: [
    HeroController(),
  ],
)

📱 5.3 OpenHarmony 适配

在 OpenHarmony 平台上,需要注意:

  • 处理手势冲突
  • 适配不同屏幕尺寸
  • 优化动画性能

六、总结

本文详细介绍了 Flutter for OpenHarmony 的 Hero 动画转场系统,包括:

组件类型 核心技术 应用场景
简单 Hero Hero + tag 基础共享元素
图片详情 Hero + Image 图片放大转场
卡片展开 Hero + Card 列表详情页
自定义飞行 flightShuttleBuilder 品牌定制
多元素联动 多个 Hero + tag 复杂页面
复杂转场 Hero + PageRouteBuilder 特色页面

参考资料


💡 提示:Hero 动画是提升应用用户体验的重要手段,合理使用可以让页面转场更加自然流畅。建议根据具体场景选择合适的 Hero 动画类型,并注意性能优化和动画时长控制。

相关推荐
2601_949593652 小时前
进阶实战 Flutter for OpenHarmony:ValueNotifier 组件实战 - 轻量级状态管理系统
flutter
忙碌5442 小时前
2026年Flutter 3.16全栈实战:从UI到后端的一体化开发革命
flutter·ui
九狼JIULANG2 小时前
Flutter Riverpod + MVI 状态管理实现的提示词优化器
flutter
lili-felicity3 小时前
进阶实战 Flutter for OpenHarmony:NestedScrollView 嵌套滚动系统 - 复杂滚动交互实现
flutter
lili-felicity3 小时前
进阶实战 Flutter for OpenHarmony:PageView 无限轮播系统 - 轮播交互优化实现
flutter·交互
lili-felicity3 小时前
进阶实战 Flutter for OpenHarmony:flutter_slidable 第三方库实战 - 列表滑动
flutter
啥都想学点4 小时前
第1天:搭建 flutter 和 Android 环境
android·flutter
蓝帆傲亦4 小时前
Vue.js 大数据处理全景解析:从加载策略到渲染优化的完全手册
前端·vue.js·flutter
九狼4 小时前
Flutter Riverpod + MVI 状态管理实现的提示词优化器
前端·flutter·github