You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

页面转场时如何将Widget动画至新位置?——仿Google应用搜索栏转场动画实现咨询

实现搜索栏跨页面平滑过渡的方案(仿Google应用效果)

刚好做过类似的需求,这种跨页面的共享元素位置/尺寸过渡,用Flutter的Hero动画+自定义过渡逻辑就能完美实现,比单独用AnimatedPositioned或者普通路由动画更贴合场景。下面是具体的实现思路和代码示例:

核心思路

我们要让同一个搜索栏组件在两个页面间"无缝移动":

  1. 用Hero组件标记两个页面的搜索栏,让Flutter识别这是共享元素
  2. 自定义flightShuttleBuilder,在过渡过程中动态调整搜索栏的位置、尺寸
  3. 配合页面背景的淡入淡出动画,让整体效果更自然

步骤1:封装可动画的搜索栏组件

先写一个能根据动画值调整自身布局的搜索栏,它会根据传入的动画参数,从首页的中央状态平滑过渡到搜索页的顶部状态:

class AnimatedSearchBar extends StatelessWidget {
  final Animation<double> animation;
  final VoidCallback onTap;
  final bool isHomePage;

  const AnimatedSearchBar({
    super.key,
    required this.animation,
    required this.onTap,
    required this.isHomePage,
  });

  @override
  Widget build(BuildContext context) {
    final screenSize = MediaQuery.of(context).size;
    final statusBarHeight = MediaQuery.of(context).padding.top;

    // 定义首页和搜索页的搜索栏参数
    final homeState = {
      'top': screenSize.height / 2 - 25, // 垂直居中(搜索栏高度50)
      'width': screenSize.width * 0.8,    // 宽度占屏幕80%
      'borderRadius': 25.0,
    };
    final searchPageState = {
      'top': statusBarHeight + 16, // 状态栏下方+16px间距
      'width': screenSize.width - 32, // 左右各留16px边距
      'borderRadius': 12.0,
    };

    // 通过动画插值计算当前状态
    final currentTop = Tween<double>(
      begin: homeState['top'],
      end: searchPageState['top'],
    ).animate(animation).value;
    final currentWidth = Tween<double>(
      begin: homeState['width'],
      end: searchPageState['width'],
    ).animate(animation).value;
    final currentRadius = Tween<double>(
      begin: homeState['borderRadius'],
      end: searchPageState['borderRadius'],
    ).animate(animation).value;

    return Positioned(
      top: currentTop,
      left: (screenSize.width - currentWidth) / 2,
      child: SizedBox(
        width: currentWidth,
        height: 50,
        child: TextField(
          decoration: InputDecoration(
            hintText: '搜索',
            filled: true,
            fillColor: Colors.grey[200],
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(currentRadius),
              borderSide: BorderSide.none,
            ),
            prefixIcon: const Icon(Icons.search),
          ),
          onTap: onTap,
          enabled: !isHomePage || animation.status == AnimationStatus.completed,
        ),
      ),
    );
  }
}

步骤2:首页(含中央搜索栏)

用Hero包裹搜索栏,指定唯一的tag,同时在flightShuttleBuilder中返回过渡中的动态搜索栏:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Stack(
        children: [
          Hero(
            tag: 'search_bar',
            flightShuttleBuilder: (flightContext, animation, flightDirection, fromHero, toHero) {
              // 过渡过程中使用动画驱动搜索栏变化
              return AnimatedSearchBar(
                animation: animation,
                onTap: () {},
                isHomePage: flightDirection == HeroFlightDirection.push,
              );
            },
            child: AnimatedSearchBar(
              animation: const AlwaysStoppedAnimation(0), // 初始状态(首页)
              onTap: () {
                // 跳转到搜索页,用自定义路由实现背景过渡
                Navigator.push(
                  context,
                  PageRouteBuilder(
                    pageBuilder: (context, anim, secondaryAnim) => const SearchPage(),
                    transitionsBuilder: (context, anim, secondaryAnim, child) {
                      // 首页淡出 + 搜索页淡入
                      final fadeOut = Tween<double>(begin: 1, end: 0).animate(
                        CurvedAnimation(parent: anim, curve: const Interval(0, 0.5)),
                      );
                      final fadeIn = Tween<double>(begin: 0, end: 1).animate(
                        CurvedAnimation(parent: anim, curve: const Interval(0.5, 1)),
                      );

                      return Stack(
                        children: [
                          FadeTransition(opacity: fadeOut, child: const HomePage()),
                          FadeTransition(opacity: fadeIn, child: child),
                        ],
                      );
                    },
                  ),
                );
              },
              isHomePage: true,
            ),
          ),
          // 首页其他内容(比如logo、快捷入口)
          const Center(
            child: Text('首页内容'),
          ),
        ],
      ),
    );
  }
}

步骤3:搜索页(含顶部搜索栏)

同样用Hero包裹搜索栏,初始状态为搜索页的顶部样式:

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

  @override
  Widget build(BuildContext context) {
    final pageAnim = ModalRoute.of(context)!.animation!;

    return Scaffold(
      backgroundColor: Colors.white,
      body: Stack(
        children: [
          Hero(
            tag: 'search_bar',
            child: AnimatedSearchBar(
              animation: const AlwaysStoppedAnimation(1), // 目标状态(搜索页)
              onTap: () {},
              isHomePage: false,
            ),
          ),
          // 搜索结果区域:等过渡完成后淡入
          FadeTransition(
            opacity: Tween<double>(begin: 0, end: 1).animate(
              CurvedAnimation(parent: pageAnim, curve: const Interval(0.6, 1)),
            ),
            child: Positioned(
              top: MediaQuery.of(context).padding.top + 16 + 50 + 16,
              left: 16,
              right: 16,
              child: ListView.builder(
                itemCount: 20,
                itemBuilder: (context, index) => ListTile(title: Text('搜索结果 $index')),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

关键细节说明

  1. Hero的核心作用:让Flutter识别两个页面的搜索栏是同一个元素,自动处理过渡时的"飞行"逻辑,我们只需要自定义飞行过程中的样式
  2. 动画曲线优化:可以把CurvedAnimation的曲线换成Curves.fastOutSlowIn,这是Material Design推荐的过渡曲线,和Google应用的动画节奏更匹配
  3. 交互控制:过渡过程中禁用搜索栏的点击(enabled属性),避免用户在动画未完成时误操作
  4. 适配不同设备:用MediaQuery获取状态栏高度和屏幕尺寸,确保在各种设备上的位置都准确

这种方案完全符合你要的效果:点击中央搜索栏,它会平滑移动到顶部,同时页面背景淡入淡出,和Google应用的搜索转场一模一样。

内容的提问来源于stack exchange,提问作者camelCaseIsGoodPractice

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 16:37:32