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

Flutter中如何为ShellRoute自定义路由切换动画(如SlideTransition)

解决GoRouter ShellRoute嵌套路由切换动画不自然问题

问题背景

使用GoRouter 13.0.0的ShellRoute结合BottomNavigationBar实现嵌套路由时,遇到以下问题:

  • 路由切换动画生硬不自然
  • 尝试为ShellRoute添加SlideTransition动画未生效
  • 期望点击BottomNavigationBar右侧项时显示右滑进入动画,左侧项时显示左滑进入动画
  • Android与iOS默认路由动画存在差异

问题原因

你之前尝试在ShellRoute的builder中添加动画,但ShellRoute本身作为容器,其内部子路由的切换动画才是需要自定义的核心。直接修改ShellRoute的builder无法影响内部页面的过渡效果,因为内部导航器的页面过渡由子路由的pageBuilder控制,而非Shell容器。

解决方案

1. 记录路由切换方向

在BottomNavigationBar的点击事件中,计算当前路由索引与目标索引的差值,判断切换方向(左滑/右滑),并通过路由的extra参数传递方向信息。

2. 自定义子路由的页面过渡

将ShellRoute内的GoRoute从builder改为pageBuilder,在pageBuilder中根据传递的方向参数,自定义SlideTransition动画,同时可兼容平台默认差异。

修改后的代码

routes.dart

import 'package:baro_project/screens/calendar_screen.dart';
import 'package:baro_project/screens/info_screen.dart';
import 'package:baro_project/screens/youtube_screen.dart';
import 'package:baro_project/widgets/nav_bar.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'screens/login_screen.dart';
import 'screens/main_screen.dart';
import 'screens/noti_screen.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'Root');
final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'Shell');

// 定义切换方向枚举
enum SlideDirection { left, right }

final router = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/login',
  routes: <RouteBase>[
    GoRoute(
      path: '/login',
      builder: (context, state) => const Login(),
    ),
    GoRoute(
      path: '/notification',
      builder: (context, state) => const NotificationManager(),
    ),
    ShellRoute(
      navigatorKey: _shellNavigatorKey,
      builder: (context, state, child) => NavBarWidget(child: child),
      routes: <RouteBase>[
        GoRoute(
          path: '/main',
          pageBuilder: (context, state) => _buildSlideTransitionPage(
            context,
            state,
            const MainScreen(),
            state.extra as SlideDirection?,
          ),
        ),
        GoRoute(
          path: '/calendar',
          pageBuilder: (context, state) => _buildSlideTransitionPage(
            context,
            state,
            const CalendarScreen(),
            state.extra as SlideDirection?,
          ),
        ),
        GoRoute(
          path: '/youtube',
          pageBuilder: (context, state) => _buildSlideTransitionPage(
            context,
            state,
            const YoutubeScreen(),
            state.extra as SlideDirection?,
          ),
        ),
        GoRoute(
          path: '/information',
          pageBuilder: (context, state) => _buildSlideTransitionPage(
            context,
            state,
            const InformationScreen(),
            state.extra as SlideDirection?,
          ),
        ),
      ],
    ),
  ],
);

// 自定义滑动过渡页面
CustomTransitionPage _buildSlideTransitionPage(
  BuildContext context,
  GoRouterState state,
  Widget child,
  SlideDirection? direction,
) {
  // 默认方向:如果没有传递方向,根据平台设置默认动画
  final beginOffset = switch (direction) {
    SlideDirection.left => const Offset(-1.0, 0.0),
    SlideDirection.right => const Offset(1.0, 0.0),
    _ => Theme.of(context).platform == TargetPlatform.iOS
        ? const Offset(1.0, 0.0)
        : const Offset(0.0, 0.3),
  };

  return CustomTransitionPage(
    key: state.pageKey,
    child: child,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return SlideTransition(
        position: Tween<Offset>(
          begin: beginOffset,
          end: Offset.zero,
        ).animate(CurvedAnimation(
          parent: animation,
          curve: Curves.easeInOut,
        )),
        child: child,
      );
    },
  );
}

nav_bar.dart

import 'package:baro_project/widgets/app_bar.dart';
import 'package:baro_project/widgets/custom_drawer.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:baro_project/routes.dart'; // 引入定义的SlideDirection

class NavBarWidget extends StatelessWidget {
  const NavBarWidget({super.key, required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: customAppBar(context),
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        unselectedItemColor: Colors.grey,
        selectedItemColor: Colors.black,
        type: BottomNavigationBarType.fixed,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: FaIcon(FontAwesomeIcons.house, size: 15.0),
            label: 'Main',
          ),
          BottomNavigationBarItem(
            icon: FaIcon(FontAwesomeIcons.calendar, size: 15.0),
            label: 'Calendar',
          ),
          BottomNavigationBarItem(
            icon: FaIcon(FontAwesomeIcons.youtube, size: 15.0),
            label: 'Youtube',
          ),
          BottomNavigationBarItem(
            icon: FaIcon(FontAwesomeIcons.user, size: 15.0),
            label: 'Information',
          ),
        ],
        onTap: (index) => _onItemTapped(index, context),
        currentIndex: _calculateIndex(context),
      ),
      endDrawer: customDrawer(context),
    );
  }

  void _onItemTapped(int index, BuildContext context) {
    final currentIndex = _calculateIndex(context);
    SlideDirection? direction;
    
    // 判断切换方向:目标索引大于当前索引则右滑进入,反之左滑进入
    if (index > currentIndex) {
      direction = SlideDirection.right;
    } else if (index < currentIndex) {
      direction = SlideDirection.left;
    }

    switch (index) {
      case 0:
        GoRouter.of(context).go('/main', extra: direction);
        break;
      case 1:
        GoRouter.of(context).go('/calendar', extra: direction);
        break;
      case 2:
        GoRouter.of(context).go('/youtube', extra: direction);
        break;
      case 3:
        GoRouter.of(context).go('/information', extra: direction);
        break;
    }
  }

  int _calculateIndex(BuildContext context) {
    final currentLocation = GoRouterState.of(context).uri.toString();
    if (currentLocation == '/main') {
      return 0;
    }
    if (currentLocation == '/calendar') {
      return 1;
    }
    if (currentLocation == '/youtube') {
      return 2;
    }
    if (currentLocation == '/information') {
      return 3;
    }
    return 0;
  }
}

说明

  • 通过CustomTransitionPage替代默认页面,完全控制过渡动画
  • 在BottomNavigationBar点击时计算方向,通过extra参数传递给路由
  • 动画方向逻辑:点击右侧项(索引更大)时,新页面从右侧滑入;点击左侧项(索引更小)时,新页面从左侧滑入
  • 兼容平台差异:如果未传递方向(如首次进入),根据平台使用默认偏移量

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 23:54:50