ぽぴなび

知って感動した技術情報・生活情報や買ってよかったものの雑記です。

【Flutter】最上部より少し上までスクロールしたときに閉じるWidgetを作るときの注意点

実装した物

https://user-images.githubusercontent.com/46369142/108667826-5675e380-751d-11eb-8692-123b722338e7.gif

github.com

方針

  • 閉じたいWidgetに画面遷移(またはモーダル表示)する
  • スクロール位置に応じてそのWidgetを閉じる

画面遷移

閉じたいWidget(ソースコード: SliverItemView)を表示する方法としては、以下の3パターンを検討。

  • Navigator.push(context, MaterialPageRoute(fullscreenDialog: true, builder: ...)でページ下部から新しいページが出てくるようなアニメーションで画面遷移
  • showModalBottomSheetでモーダル表示(何故か上部のSafeAreaが無視されるので良くないかも)
  • modal_bottom_sheetパッケージのshowCupertinoModalBottomSheetshowBarModalBottomSheetを使う。
~~
         Card(
            child: ListTile(
              title: Text('Sample2:'),
              subtitle: Text(
                  'modal_bottom_sheetパッケージのshowCupertinoModalBottomSheetをそのまま使った場合'),
              trailing: const Icon(Icons.open_in_new),
              onTap: () {
                showCupertinoModalBottomSheet(
                  context: context,
                  builder: (_) => SliverItemView(),
                );
              },
            ),
          ),
         ~~
         Card(
            child: ListTile(
              title: Text('Sample6:'),
              subtitle: Text('Navigator.pushとfullscreenDialog:trueで画面遷移'),
              trailing: const Icon(Icons.open_in_new),
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => SliverItemView(),
                    fullscreenDialog: true,
                  ),
                );
              },
            ),
          ),
~~

スクロール位置に応じてページを閉じる

  • 現在のスクロール位置はScrollController.offsetプロパティから取得する。
  • 閉じたいWidget(SliverItemView)で、スクロール位置が閾値以下になったらNavigator.popで表示中のWIdgetを閉じるようなlistenerを定義する。

注意点

スクロールイベントは連続して何回も起こるようで、何も工夫しないとlistenerに定義したNavigator.pop(context)が何度も呼ばれてしまい真っ黒な画面にとばされてしまう。そのため今回のコードでは適当なフラグを建てて、条件を満たした最初の1回のみ処理を行うようにしている。

class _SliverItemViewState extends State<SliverItemView> {
  final ScrollController _scrollController = ScrollController();

  // _onScrollChangedListenerが2回以上呼ばれてしまうため、
  // Navigator.popが何回も呼ばれ真っ黒い画面に飛ばされてしまうのを防ぐフラグ
  bool isFirstEvent = true;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScrollChangedListener);
  }

  @override
  void dispose() {
    super.dispose();
    _scrollController.removeListener(_onScrollChangedListener);
  }

  void _onScrollChangedListener() {
    if (isFirstEvent && _scrollController.offset < _dismissThreshold) {
      isFirstEvent = false;
      Navigator.pop(context);
    }
  }
  ~~

その他の注意点

SliverList(またはSliverGrid)の要素数が少ないとスクロールできない

デフォルトではSliverListの要素数が少なくスクロールしなくても全要素見えている場合は、スクロールができない。 これは、CustomScrollViewphysicsプロパティにAlwaysScrollableScrollPhysics()を指定することで解決できる。 (要素数にかかわらずスクロール動作が発生するようになる。)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        controller: _scrollController,
        physics: AlwaysScrollableScrollPhysics(),  // <- 追加
        slivers: [
          SliverAppBar(
            title: Text(this.toStringShort()),
            floating: true,
          ),
          SliverList(
            delegate: _buildSliverChildListDelegate(),
          ),
        ],
      ),
    );
  }