iT邦幫忙

0

【Flutter】MaterialPageRoute 與 CupertinoPageRoute 混合使用的問題

  • 分享至 

  • xImage
  •  

在 Flutter 中,如果當前頁面應用的是 MaterialPageRoute,想要在切換時應用 iOS 風格的切換動畫,最簡單的方法就是切換時直接使用 CupertinoPageRoute。

https://book.flutterchina.club/chapter9/route_transition.html

但是眼尖的我發現了一個奇怪的現象,當使用 MaterialPageRoute 進入頁面 A,然後從頁面 A 使用 CupertinoPageRoute 進入頁面 B 時,頁面 A 的離開動畫仍然使用 MaterialPageRoute 的動畫,而不是預期的 iOS 風格動畫。

我在看很多人的教學文章都沒有提到這點,這讓我非常困惑。

原因分析:
查看 MaterialPageRoute 的原始碼會發現,MaterialPageRoute 的 buildTransitions 方法依賴 Theme.of(context).pageTransitionsTheme。

@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
  final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
  return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}

也就是說:

  • 頁面的進入和離開動畫是成對的,由該頁面被推入時使用的 Route 類型決定。
  • 全局 pageTransitionsTheme 設置會影響所有使用 MaterialPageRoute 的頁面轉換。
theme: ThemeData(
  pageTransitionsTheme: PageTransitionsTheme(
    builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
      TargetPlatform.values,
      value: (dynamic _) => const ZoomPageTransitionsBuilder(),
    ),
  ),
),
  • 如果沒有特別設定 ThemeData 中的 pageTransitionsTheme,在 Android 會使用 ZoomPageTransitionsBuilder、在 iOS 會使用 CupertinoPageTransitionsBuilder。

那意思不就是說,如果我要隨時切換 MaterialPageRoute 的進入與離開動畫,自己定義路由動畫不就是不可避免的? 😵


解決方案:

  1. 使用 Riverpod 的 StateNotifierProvider 來管理轉場動畫風格。
class PageTransitionNotifier extends StateNotifier<PageTransitionsBuilder> {
  PageTransitionNotifier() : super(const FadeUpwardsPageTransitionsBuilder());

  void setCupertinoStyle() {
    state = const CupertinoPageTransitionsBuilder();
  }

  void setZoomStyle() {
    state = const ZoomPageTransitionsBuilder();
  }
}

final pageTransitionProvider =
    StateNotifierProvider<PageTransitionNotifier, PageTransitionsBuilder>(
        (ref) {
  return PageTransitionNotifier();
});
  1. 創建自定義的 CustomMaterialPageRoute,覆寫 buildTransitions 方法。
class CustomMaterialPageRoute<T> extends MaterialPageRoute<T> {
  CustomMaterialPageRoute({
    required super.builder,
    super.settings,
    super.maintainState,
    super.fullscreenDialog,
  });

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return Consumer(
      builder: (context, ref, _) {
        final pageTransitionsBuilder = ref.watch(pageTransitionProvider);
        return pageTransitionsBuilder.buildTransitions<T>(
          this,
          context,
          animation,
          secondaryAnimation,
          child,
        );
      },
    );
  }
}
  1. 在需要切換動畫風格時,透過 Provider 來控制。
ref.read(pageTransitionProvider.notifier).setCupertinoStyle();

Navigator.push(
  context,
  CustomMaterialPageRoute(
    builder: (context) => const SecondScreen(
      content: 'Cupertino Transition',
    ),
  ),
);
  • MaterialPageRoute 的 iOS 切換動畫不如直接使用 CupertinoPageRoute 來的流暢,可以把上面的 CustomMaterialPageRoute 改成 CupertinoPageRoute,但要如何控制 CupertinoPageRoute 的離開動畫,這又是另一個問題了⋯⋯

這個解法要注意把初始頁面也放在 CustomMaterialPageRoute 中,否則初始頁面還是會受到 MaterialApp 中 ThemeData 的影響。

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      initialRoute: '/', // 設置初始路由
      onGenerateRoute: (settings) {
        if (settings.name == '/') {
          return CustomMaterialPageRoute(
            builder: (context) => const HomeScreen(),
          );
        }
        return null;
      },
    );
  }
}

以上解法是自己想出來的,歡迎各路大神討論。


然後就在 2024 年 12 月 Flutter 發佈了 3.27 更新, 其中就有提到 Mixing Route Transitions,看到當下我整個傻眼,後來就知道這是一個存在已久的已知問題了,但我想說這個框架都推出這麼久了直到現在才修復是正常的嗎?😂

難怪有人要推出 Flutter 分叉專案 Flock

回歸正題,從 GitHub PR #150031 的討論和程式碼對比,可以看出這次改動的主要差異:

  1. 主要目的是允許在同一個應用中混合使用不同的路由轉場動畫。例如可以同時使用 Material 的縮放轉場和 Cupertino 的滑動轉場。
  2. 核心改動是 MaterialPageRoute 和 CupertinoPageRoute 都引入了 DelegatedTransition 機制:
  • 當一個新路由被推入時,它可以告訴下面的路由如何執行退出動畫。
  • 下層路由會使用上層路由提供的 delegatedTransition 來替代自己默認的次要轉場動畫。
  1. 具體實現(以 MaterialPageRoute 為例):
  • 在 3.27 版本中,主要新增了 delegatedTransition 相關的功能:
mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
  // 3.27 新增
  @override
  DelegatedTransitionBuilder? get delegatedTransition => _delegatedTransition;

  static Widget? _delegatedTransition(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, bool allowSnapshotting, Widget? child) {
    final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
    final TargetPlatform platform = Theme.of(context).platform;
    final DelegatedTransitionBuilder? themeDelegatedTransition = theme.delegatedTransition(platform);
    return themeDelegatedTransition != null ? themeDelegatedTransition(context, animation, secondaryAnimation, allowSnapshotting, child) : null;
  }
}
  • canTransitionTo 方法的邏輯也有所改變:
// 3.24 版本
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
  return (nextRoute is MaterialRouteTransitionMixin && !nextRoute.fullscreenDialog)
    || (nextRoute is CupertinoRouteTransitionMixin && !nextRoute.fullscreenDialog);
}

// 3.27 版本
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
  final bool nextRouteIsNotFullscreen = (nextRoute is! PageRoute<T>) || !nextRoute.fullscreenDialog;
  final bool nextRouteHasDelegatedTransition = nextRoute is ModalRoute<T>
    && nextRoute.delegatedTransition != null;
  return nextRouteIsNotFullscreen &&
    ((nextRoute is MaterialRouteTransitionMixin) || nextRouteHasDelegatedTransition);
}

具體內部做了什麼就不深究了,總之把 Flutter 更新到 3.27 來解決這個問題是最佳解。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言