LoginSignup
2
0

はじめに

ショッピングサイト等でよくある、商品の絞り込み毎に左→右へ画面遷移を繰り返すあのUIを、BottomSheetで実現したい。

イメージ参考:zozotownアプリ カテゴリー検索

スクリーンショット 2024-01-04 21.33.31.png

完成図

キャプチャ.gif

① フローティングボタンタップで[絞り込み画面]表示
② [絞り込み画面]内のリストタップでシートを開いたまま[カテゴリー画面]を開く
③ [カテゴリー画面]の<ボタンタップで[絞り込み画面]に戻る
④ [カテゴリー画面]の×ボタンタップでシート自体pop

BottomSheetを表示する

以下より実装方法を記載していきます。
まず① フローティングボタンタップで[絞り込み画面]表示 の処理
BottomSheetをPushします。


  Future<void> _incrementCounter() async {
    await Navigator.of(context).push(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        barrierColor: Colors.black.withOpacity(0.6),
        pageBuilder: (_, __, ___) => const BottomSheetView(),
        transitionDuration: const Duration(milliseconds: 500),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const Offset begin = Offset(0.0, 1.0); // 下から上にSheetを表示
          const Offset end = Offset.zero;
          final Animatable<Offset> tween = Tween(begin: begin, end: end).chain(CurveTween(curve: Curves.easeInOut));
          final Animation<Offset> offsetAnimation = animation.drive(tween);
          return SlideTransition(
            position: offsetAnimation,
            child: child,
          );
        },
      ),
    );
  }

② [絞り込み画面]内のリストタップでシートを開いたまま[カテゴリー画面]を開く の処理

ここで[カテゴリー画面]をpush時にboolを引数で返せるように設定するawait Navigator.of(context).push<bool?>
③④でのpop処理時にtrueorfalseを渡すことでbottomSheet自体を閉じるか、一つ前の画面に戻るかを判定できる。

もちろん、bool以外を返却地に設定し、pop処理時に任意の値を渡すことも可能


  Future<void> _onTapCategoryButton() async {
    final backButton = await Navigator.of(context).push<bool?>(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        pageBuilder: (context, animation, secondaryAnimation) =>
            const CategoryBottomSheetView(),
      ),
    );
    if (backButton == null || backButton == false) {
      if (mounted) Navigator.of(context).pop();
    }
  }
}

③ [カテゴリー画面]の<ボタンタップで[絞り込み画面]に戻る の処理

  void _onPressedBackButton() {
    Navigator.of(context).pop(true);
  }

④ [カテゴリー画面]の×ボタンタップでシート自体pop

  void _onPressedCloseButton() {
    Navigator.of(context).pop(false);
  }

コード全体

main.dart

import 'package:flutter/material.dart';
import 'package:test_project/bottomsheet_view.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const 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> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Container(),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.egg_alt_sharp),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  Future<void> _incrementCounter() async {
    await Navigator.of(context).push(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        barrierColor: Colors.black.withOpacity(0.6),
        pageBuilder: (_, __, ___) => const BottomSheetView(),
        transitionDuration: const Duration(milliseconds: 500),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          const Offset begin = Offset(0.0, 1.0); // 下から上
          const Offset end = Offset.zero;
          final Animatable<Offset> tween = Tween(begin: begin, end: end).chain(CurveTween(curve: Curves.easeInOut));
          final Animation<Offset> offsetAnimation = animation.drive(tween);
          return SlideTransition(
            position: offsetAnimation,
            child: child,
          );
        },
      ),
    );
  }
}

bottomsheet_view.dart

import 'package:auto_size_text_plus/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:test_project/categorybottomsheet_view.dart';

class BottomSheetView extends StatefulWidget {
  const BottomSheetView({Key? key}) : super(key: key);

  @override
  _BottomSheetView createState() => _BottomSheetView();
}

class _BottomSheetView extends State<BottomSheetView> {
  _BottomSheetView() {}

  @override
  Widget build(final BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.only(
              topRight: Radius.circular(12), topLeft: Radius.circular(12)),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.5),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3), // changes position of shadow
            ),
          ],
        ),
        height: MediaQuery.of(context).size.height * 0.8,
        width: double.maxFinite,
        child: Material(
          type: MaterialType.transparency,
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                //余白
                const Divider(color: Colors.transparent),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    IconButton(
                      icon: const Icon(
                        Icons.arrow_back_ios_rounded,
                        size: 15,
                      ),
                      color: Colors.transparent,
                      onPressed: () {},
                    ),
                    const Padding(
                      padding: EdgeInsets.symmetric(horizontal: 9.0),
                      child: AutoSizeText("絞り込み"),
                    ),
                    //余白
                    IconButton(
                      icon: const Icon(
                        Icons.arrow_back_ios_rounded,
                        size: 15,
                      ),
                      color: Colors.transparent,
                      onPressed: () {},
                    ),
                  ],
                ),
                ListTile(
                  onTap: _onTapCategoryButton,
                  title: const Text(
                    "カテゴリー",
                    style: TextStyle(fontSize: 15),
                  ),
                  trailing: const Icon(
                    Icons.arrow_forward_ios_rounded,
                    color: Colors.grey,
                    size: 15,
                  ),
                  // isThreeLine: true,
                ),
                ListTile(
                  onTap: () {
                    // _onTapCreateReadyMadeClubButton(GolfclubCategoryType.hybridUtility);
                  },
                  title: const Text(
                    "ショップ",
                    style: TextStyle(fontSize: 15),
                  ),
                  trailing: const Icon(
                    Icons.arrow_forward_ios_rounded,
                    color: Colors.grey,
                    size: 15,
                  ),
                  // isThreeLine: true,
                ),
                ListTile(
                  onTap: () {
                    // _onTapCreateReadyMadeClubButton(GolfclubCategoryType.iron);
                  },
                  title: const Text(
                    "オプション",
                    style: TextStyle(fontSize: 15),
                  ),
                  trailing: const Icon(
                    Icons.arrow_forward_ios_rounded,
                    color: Colors.grey,
                    size: 15,
                  ),
                  // isThreeLine: true,
                ),

                const Divider(color: Colors.transparent),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  Future<void> _onTapCategoryButton() async {
    final backButton = await Navigator.of(context).push<bool?>(
      PageRouteBuilder(
        opaque: false,
        barrierDismissible: true,
        pageBuilder: (context, animation, secondaryAnimation) =>
            const CategoryBottomSheetView(),
      ),
    );
    if (backButton == null || backButton == false) {
      if (mounted) Navigator.of(context).pop();
    }
  }
}

categorybottomsheet_view.dart

import 'package:auto_size_text_plus/auto_size_text.dart';
import 'package:flutter/material.dart';

class CategoryBottomSheetView extends StatefulWidget {
  const CategoryBottomSheetView({Key? key}) : super(key: key);

  @override
  _CategoryBottomSheetView createState() => _CategoryBottomSheetView();
}

class _CategoryBottomSheetView extends State<CategoryBottomSheetView> {
  _CategoryBottomSheetView() {}

  @override
  Widget build(final BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.only(
              topRight: Radius.circular(12), topLeft: Radius.circular(12)),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.5),
              spreadRadius: 5,
              blurRadius: 7,
              offset: const Offset(0, 3), // changes position of shadow
            ),
          ],
        ),
        height: MediaQuery.of(context).size.height * 0.8,
        width: double.maxFinite,
        child: Material(
          type: MaterialType.transparency,
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                //余白
                const Divider(color: Colors.transparent),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    IconButton(
                      icon: const Icon(
                        Icons.arrow_back_ios_rounded,
                        size: 15,
                      ),
                      color: Colors.black,
                      onPressed: _onPressedBackButton,
                    ),
                    const Padding(
                      padding: EdgeInsets.symmetric(horizontal: 9.0),
                      child: AutoSizeText("カテゴリー"),
                    ),
                    //中央揃えのため透明アイコンを配置
                    IconButton(
                      icon: const Icon(
                        Icons.close,
                        size: 21,
                      ),
                      color: Colors.black,
                      onPressed: _onPressedCloseButton,
                    ),
                  ],
                ),
                const ListTile(
                  onTap: null,
                  title: Text(
                    "トップス",
                    style: TextStyle(fontSize: 15),
                  ),
                ),
                ListTile(
                  onTap: () {},
                  title: const Text(
                    "ボトムス",
                    style: TextStyle(fontSize: 15),
                  ),
                ),
                const Divider(color: Colors.transparent),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  void _onPressedBackButton() {
    Navigator.of(context).pop(true);
  }

  void _onPressedCloseButton() {
    Navigator.of(context).pop(false);
  }
}

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0