Open6

iOSのLINEの設定画面のような全画面のDialogをFlutterで実装する

YuheiNakasakaYuheiNakasaka

iOSのLINEの設定画面のような全画面のDialogというのはこういうやつ。

YuheiNakasakaYuheiNakasaka

達成したい挙動としては、

  • (1)内部のListを上下スクロール出来る
  • (2)下スワイプ(ドラッグ)でDialogを閉じることが出来る
  • (3)スワイプ(ドラッグ)に合わせてDialogが伸縮出来る

である。

(1)(2)だけであればこんな感じでshowModalBottomSheetとDraggableScrollableSheetを使うだけで大体できる。

showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      builder: (context) {
            return DraggableScrollableSheet();
      }
);

しかし(3)も合わせて実装したい場合は難しくなる。

YuheiNakasakaYuheiNakasaka

(3)を達成するために自分の場合はGestureDetectorをAnimatedBuilderを組み合わせたWidgetを作成した。コードは下記である。このWidgetでDraggableScrollableSheetをラップすると(1),(2)に加えて(3)の上下の伸縮も達成できる。

import 'package:flutter/material.dart';

class SwipableFullModalView extends StatefulWidget {
  const SwipableFullModalView({ this.child});
  final Widget child;

  
  _SwipableFullModalViewState createState() => _SwipableFullModalViewState();
}

class _SwipableFullModalViewState extends State<SwipableFullModalView>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

  double _height;

  
  void initState() {
    super.initState();
    expand();
  }

  Future<void> expand() async {
    _controller = AnimationController(
      duration: const Duration(milliseconds: 200),
      vsync: this,
    );
    final tween = Tween<double>(begin: _controller.value, end: 1);
    _animation = _controller
        .drive(CurveTween(curve: Curves.fastOutSlowIn))
        .drive(tween);
    await _controller.forward();
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    _height ??= MediaQuery.of(context).size.height;
  }

  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        final ratio = _controller.value;
        final top = _height - _height * ratio;
        return Stack(
          children: [
            Positioned(
              top: top,
              left: 0,
              child: child,
            ),
          ],
        );
      },
      child: GestureDetector(
        onVerticalDragUpdate: (details) {
          final delta = -details.primaryDelta;
          _controller.value += delta / _height;
        },
        onVerticalDragEnd: (details) {
          if (_controller.value < 0.9) {
            _controller.value = 0;
          } else {
            _controller.value = 1;
          }
        },
        child: SizedBox(
          width: MediaQuery.of(context).size.width,
          height: MediaQuery.of(context).size.height,
          child: widget.child,
        ),
      ),
    );
  }
}
YuheiNakasakaYuheiNakasaka

なのでまとめると(1)(2)(3)を実装するにはこんな感じのコードを書いた。iOSのネイティブだと標準で実装されてる?もしくは簡単な実装で出来るっぽいが、Flutterだとだいぶ面倒である...

showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      builder: (context) {
            return SwipableFullModalView (
                 child: DraggableScrollableSheet(
                     child: HogeListView() // 内部のスクロール出来るWidgetなど色々実装
                 ),
            );
      }
);
YuheiNakasakaYuheiNakasaka

もっと楽な実装方法があるかもしれないので知ってる人いたら教えて欲しいですわ...

YuheiNakasakaYuheiNakasaka

TwitterでSwipableFullModalViewみたいな層は不要だと気づかされた。

https://twitter.com/razokulover/status/1353926868236439553

元の実装でできないと思い込んでいたのはshowModalBottomSheetのbackgroundColorがColors.whiteになっていて、その状態だとdraggableScrollableSheetでウィジェットが閉じる動きが出来たとしても背景が白なのでDialog全体がスワイプに合わせて動かないものだと思い込んでしまっていた。

しかしshowModalBottomSheetbackgroundColorColors.transparentであればそもそも問題ないという感じである。

問題の見定めを誤ったのであった...