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

はじめに
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するときなどにも応用できると思います。
ぜひ使ってみてください。
  
  
  
  
コメント