Flutterでスクロールを検知し、ある位置までスクロールしたらWidgetを表示するには

Posted by on Mon, Sep 3, 2018

やりたいこと

Flutterで、下図のように上にスクロールしてある一定の位置まで来たらWidgetを表示させ、下にスクロールしてある一定の位置まで来たら表示したWidgetを消すというのをやりたい。

どうやって?

  1. スクロールを検知するListenerをセットする
  2. そのListenerでスクロール位置を計算してWidgetの表示・非表示を切り替える。

1. スクロールを検知するListenerをセットする

Flutter標準のScrollControllerクラスを使って、ScrollController _scrollController;と変数宣言し、initState()で初期化、dispose()で後処理しておきます。

ScrollController _scrollController;
@override
void initState() {
  super.initState();
  _scrollController = ScrollController();
}
@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}

ListView.builderScrollControllerをセットできるので、先程宣言した_scrollControllerをセットします。

Widget _buildListView() {
  return ListView.builder(
    controller: _scrollController,
    reverse: true,
    itemCount: messages != null ? messages.length : 0,
    itemBuilder: (BuildContext context, int position) {
      return _buildMessageRow(context, position);
    },
  );
}

_scrollControllerにスクロールを検知したときに処理をしたいリスナー(ここでは_scrollListener)を追加します。

@override
void initState() {
  super.initState();
  _scrollController = ScrollController();
  _scrollController.addListener(_scrollListener); // ←追加
}

void _scrollListener() {
  // スクロールを検知したときに呼ばれる      
}

これで、スクロールを検知することができるようになりました。

2. そのListenerでスクロール位置を計算してWidgetの表示・非表示を切り替える。

スクロール位置を計算する方法ですが、先に実装を書くと次のように書けます。

void _scrollListener() {
  double positionRate =
      _scrollController.offset / _scrollController.position.maxScrollExtent;
}

_scrollController.offsetは、現在のスクロール位置を表し、

_scrollController.position.maxScrollExtentは、スクロールできる最大の位置を表します。

offsetmaxScrollExtentもpixel単位で、たとえばですがmaxScrollExtentが1203.45で、offsetが605.2とかの場合は、スクロール位置は代替真ん中あたりにいることになります。(数値は適当です)

ですので、offset割るmaxScrollExtent(offset / maxScrollExtent)としてあげれば、スクロール位置の割合がでますので、その割合が0なら一番下、1なら一番上ということになります(注意:ここではListViewはreverse: trueと逆にしていますので、reverse: falseなら0が一番上1が一番下となります)。

あとはスクロール位置の割合を使って、適当な閾値でWidgetの表示・非表示を切り替えればよいだけです。

void _scrollListener() {
  double positionRate =
      _scrollController.offset / _scrollController.position.maxScrollExtent;
  // ↓追加ここから    
  const threshold = 0.8;
  if (positionRate > threshold) {
    setState(() {
      _isShow = true;
    });  
  }
  if (positionRate < threshold - 0.05) {
    setState(() {
      _isShow = false;
    });  
  }
  // ↑追加ここまで   
}

_isShowはWidgetを切り替えるフラグです。参考までに次のように使っています。

Widget _buildSample() {
  return Container(
    padding: EdgeInsets.all(8.0),
    color: Colors.orange[100],
    child: Text(
        'To see this room\'s full history, upgrade one of our paid plans.',
        style: TextStyle(color: Colors.red),
      ),
  );
}

Widget _buildBody() {
  List<Widget> children = List.from([ _buildListView()]);
  if (_isShow) {
    children.add(_buildSample());
  }
  return Stack(
    children: children,
  );
}

ちなみに、先程の適当な閾値でWidgetの表示・非表示を切り替えているところで、

if (positionRate > threshold) {
    // setState
} else {
   // setState
}

このようにif~else~ではなく

if (positionRate > threshold) {
    // setState
} 
if (positionRate < threshold - 0.05) {
   // setState
}

positionRate < threshold - 0.05としているのは、if~else~のときだとthresholdギリギリの位置でWidgetの表示・非表示がちらつくためです。非表示時は少し余裕を持って非表示にしています。なお0.05という値は適当です。

以上です。

Happy Fluttering!



comments powered by Disqus