Flutter CupertinoAppでドロワーを表示する

Flutter CupertinoAppでドロワーを表示する

flutter-cupertiono-drawer

はじめに

MaterialAppでは、Scaffoldのdrawerを使って簡単にドロワーを表示することができます。
しかし、CupertinoAppのCupertinoPageScaffold等にはdrawerは用意されていません(Issue #51315でもFlutterでは提供しない旨コメントされています)。
この記事ではCupertinoAppでドロワーを表示する方法について説明します。

前提

  • この記事のサンプルはFlutter 3.3.8で動かしました。
  • MaterialAppではなくCupertinoAppでドロワーを表示する方法です。MaterialAppで表示したい場合、Scaffoldのdrawerを使うことで簡単に表示できます。Add a drawer to a screenをご参照ください。
  • こちらのStackOverflowではStackとAnimatedPositionedを使った方法が紹介されています。この記事ではこの方法ではなくNavigator.push()して表示する方法を説明します。
  • この記事で説明する方法は、ボタンをタップした時にドロワーを開く方法です。画面をスワイプ・ドラッグして開く方法ではありません。画面をスワイプ・ドラッグして開く方法鵜を知りたい場合、当ブログのFlutterで画面をドラッグ・スワイプで開くをご参照ください。

ポイント

  • ドロワー画面のWidgetをNavigator.push()する際に、CupertinoPageRouteではなくPageRouteBuilderを渡します。
  • PageRouteBuilderのtransitionBuilderで、AnimatedWidgetを返すようにすることで、任意のアニメーションでドロワー画面を開けるようになります。
    • 今回、AnimatedWidgetとして、SlideTransitionを返しています。これによって、左から右にスライドする動きを実現しています。

コード全体

import 'package:flutter/cupertino.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const CupertinoApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        leading: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () => _showDrawer(context),
          alignment: Alignment.center,
          child: const Text('Open'),
        ),
        middle: Text(widget.title),
      ),
      child: const SizedBox(),
    );
  }

  Future<void> _showDrawer(BuildContext context) {
    return Navigator.push(
      context,
      PageRouteBuilder(
        transitionDuration: const Duration(milliseconds: 200),
        reverseTransitionDuration: const Duration(milliseconds: 200),
        pageBuilder: (context, animation, secondaryAnimation) => const _DrawerPage(),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const begin = Offset(-1.0, 0.0);
          const end = Offset.zero;
          const curve = Curves.easeInOut;

          var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

          return SlideTransition(
            position: animation.drive(tween),
            child: child,
          );
        },
      ),
    );
  }
}

class _DrawerPage extends StatelessWidget {
  const _DrawerPage({super.key});
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      backgroundColor: const Color(0x00000000),
      child: Stack(
        children: [
          Container(color: const Color(0xFF0000FF)),
          Positioned(
            top: 16,
            right: 16,
            child: SafeArea(
              child: CupertinoButton(
                onPressed: () => Navigator.pop(context),
                child: const Text(
                  'Close',
                  style: TextStyle(color: Color(0xFFFFFFFF)),
                ),
              ),
            ),
          )
        ],
      ),
    );
  }
}

解説

Navigator.push()にPageRouteBuilderを渡す

  • 通常Navigator.push()する際はCupertinoPageRouteを渡すことが多いと思いますが、以下のコードの(1)の通りPageRouteBuilderを渡します。

    • PageRouteBuilderは、CupertinoPageRouteやMaterialPageRouteと同様にPageRouteのサブクラスです。
    • PageRouteBuilderは、PageRouteのサブクラスを作らなくてもそのcallbackを実装することによってPageRouteを定義できるようにしたユーティリティクラスです。

      The PageRouteBuilder subclass provides a way to create a PageRoute using callbacks rather than by defining a new class via subclassing.
      PageRoute

  • PageRouteBuilderのpageBuilderで開きたいドロワー画面のWidget(_DrawerPage)を返すようにしています。

// ...省略...
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        leading: CupertinoButton(
          padding: EdgeInsets.zero,
          onPressed: () => _showDrawer(context),
          alignment: Alignment.center,
          child: const Text('Open'),
        ),
        middle: Text(widget.title),
      ),
      child: const SizedBox(),
    );
  }

  Future<void> _showDrawer(BuildContext context) {
    return Navigator.push(
      context,
      // (1) CupertinoPageRouteではなく、PageRouteBuilderを渡す。
      PageRouteBuilder(
        transitionDuration: const Duration(milliseconds: 200),
        reverseTransitionDuration: const Duration(milliseconds: 200),
        pageBuilder: (context, animation, secondaryAnimation) => const _DrawerPage(),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const begin = Offset(-1.0, 0.0);
          const end = Offset.zero;
          const curve = Curves.easeInOut;

          var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));

          // (2) CupertinoPageRouteではなく、PageRouteBuilderを渡す。
          return SlideTransition(
            position: animation.drive(tween),
            child: child,
          );
        },
      ),
    );
  }
}
// ...省略...

PageRouteBuilderのtransitionBuilderでSlideTransitionを返す。

  • 上記コードの(2)の部分では、今回スライドでドロワーを表示したいのでSlideTransitionを使っています。SlideTransitionはAnimatedWidgetのサブクラスで、position:にAnimationを渡す必要があります。
  • transitionBuilderで渡ってきたanimationをSlideTransitionに渡しています。そのままanimationを渡すこともできますが、緩急をつけるためCurveTweenをかませています。これにより、一定速度のアニメーションが緩急がついたアニメーションに変換されます。
  • このコードは、Flutter公式のAnimate a page route transitionを参考にしています。この公式ドキュメントは下から上にスライドして表示されます(この記事では左から右にスライドして表示されます)。

最後に

最後までお読みくださりありがとうございます。
今回ドロワーをスライドアニメーション付きでpushする方法を説明しました。
今回の方法は、フェードイン・フェードアウトアニメーション付きでpushするときなどにも応用できると思います。
ぜひ使ってみてください。

コメント

タイトルとURLをコピーしました