画面が最初に作成されたときに、SingleChildScrollViewでスクロールが不要かどうかをどのように判断しますか?

Nov 25 2020

私は持っていSingleChildScrollViewます。時には、そのはchild、その場合には、画面よりも長いSingleChildScrollViewあなたがスクロールすることができます。ただしchild、画面よりも短い場合もあります。その場合、スクロールは必要ありません。

画面の下部に矢印を追加しようとしています。これは、ユーザーが下にスクロールして残りのコンテンツを表示できる/すべきであることをユーザーに示唆しています。私は成功した場合を除き、これを実装childするにはSingleChildScrollView、画面よりも短くなっています。この場合、スクロールは必要ないので、矢印はまったく表示しません。

これを実行しようとしましたlistenerが、listenerスクロールを開始するまではアクティブにならず、この場合はスクロールできません。

_scrollController矢印を示す三項演算子ののプロパティにもアクセスしようとしましたが、例外がスローされます。ScrollController not attached to any scroll views.

これは私が行っていることを示す完全なサンプルアプリです。実行を確認したい場合は、コピーして貼り付けるだけです。私はすべての内容を置き換えるColumnText簡略化のためのウィジェット:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) =>
      MaterialApp(home: Scaffold(body: MyScreen()));
}

class MyScreen extends StatefulWidget {
  @override
  _MyScreenState createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
  ScrollController _scrollController = ScrollController();
  bool atBottom = false;

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

    // Activated when you get to the bottom:
    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter == 0) {
        setState(() {
          atBottom = true;
        });
      }
    });

    // Activated as soon as you start scrolling back up after getting to the bottom:
    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter > 0 && atBottom) {
        setState(() {
          atBottom = false;
        });
      }
    });

    // I want this to activate if you are at the top of the screen and there is
    // no scrolling to do, i.e. the widget being displayed fits in the screen:
    _scrollController.addListener(() {
      if (_scrollController.offset == 0 &&
          _scrollController.position.extentAfter == 0) {
        setState(() {
          atBottom = false;
        });
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        SingleChildScrollView(
          controller: _scrollController,
          scrollDirection: Axis.vertical,
          child: Container(
            width: MediaQuery.of(context).size.width,
            child: Column(
              children: [
                for (int i = 0; i < 100; i++)
                  Text(
                    i.toString(),
                  ),
              ],
            ),
          ),
        ),
        atBottom
            ? Container()
            : Positioned(
                bottom: 10,
                right: 10,
                child: Container(
                  child: Icon(
                    Icons.arrow_circle_down,
                  ),
                ),
              ),
      ],
    );
  }
}

回答

VinayHP Nov 25 2020 at 19:15

最初にhasClientsプロパティを使用して、_scrollControllerがスクロールビューにアタッチされているかどうかを確認する必要があります。

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

    // Activated when you get to the bottom:
    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter == 0) {
        setState(() {
          atBottom = true;
        });
      }
    });

    // Activated as soon as you start scrolling back up after getting to the bottom:
    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter > 0 && atBottom) {
        setState(() {
          atBottom = false;
        });
      }
    });

    // I want this to activate if you are at the top of the screen and there is
    // no scrolling to do, i.e. the widget being displayed fits in the screen:
    _scrollController.addListener(() {
      if (_scrollController.offset == 0 &&
          _scrollController.position.extentAfter == 0) {
        setState(() {
          atBottom = false;
        });
      }
    });
  }

への変更:

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

  // Activated when you get to the bottom:
  _scrollController.addListener(() {
    if (_scrollController.hasClients){ // Like this
      if (_scrollController.position.extentAfter == 0) {
      setState(() {
        atBottom = true;
      });
    }}
  });

  // Activated as soon as you start scrolling back up after getting to the bottom:
  _scrollController.addListener(() {
    if (_scrollController.hasClients){ // Like this
    if (_scrollController.position.extentAfter > 0 && atBottom) {
      setState(() {
        atBottom = false;
      });
    }}
  });

  // I want this to activate if you are at the top of the screen and there is
  // no scrolling to do, i.e. the widget being displayed fits in the screen:
  _scrollController.addListener(() {
    if (_scrollController.hasClients){ // Like this
    if (_scrollController.offset == 0 &&
        _scrollController.position.extentAfter == 0) {
      setState(() {
        atBottom = false;
      });
    }}
  });
}
MichaelRodeman Nov 25 2020 at 22:49
  1. スケジューラーのFlutterライブラリーをインポートします。
import 'package:flutter/scheduler.dart';
  1. 状態オブジェクトの内部でbuildメソッドの外部にブールフラグを作成して、buildまだ呼び出されているかどうかを追跡します。
bool buildCalledYet = false;
  1. buildメソッドの最初に以下を追加します。
if (!firstBuild) {
      firstBuild = true;
      SchedulerBinding.instance.addPostFrameCallback((_) {
        setState(() {
          atBottom = !(_scrollController.position.maxScrollExtent > 0);
        });
      });
    }

(ブールフラグは、このコードがbuild何度も呼び出されるのを防ぎます。)

このソリューションを実装するサンプルアプリの完全なコードは次のとおりです。

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) =>
      MaterialApp(home: Scaffold(body: MyScreen()));
}

class MyScreen extends StatefulWidget {
  @override
  _MyScreenState createState() => _MyScreenState();
}

class _MyScreenState extends State<MyScreen> {
  ScrollController _scrollController = ScrollController();
  bool atBottom = false;
  // ======= new code =======
  bool buildCalledYet = false;
  // ========================

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

    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter == 0) {
        setState(() {
          atBottom = true;
        });
      }
    });

    _scrollController.addListener(() {
      if (_scrollController.position.extentAfter > 0 && atBottom) {
        setState(() {
          atBottom = false;
        });
      }
    });

    // ======= The third listener is not needed. =======
  }

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

  @override
  Widget build(BuildContext context) {
    // =========================== new code ===========================
    if (!buildCalledYet) {
      buildCalledYet = true;
      SchedulerBinding.instance.addPostFrameCallback((_) {
        setState(() {
          atBottom = !(_scrollController.position.maxScrollExtent > 0);
        });
      });
    }
    // ================================================================

    return Stack(
      children: [
        SingleChildScrollView(
          controller: _scrollController,
          scrollDirection: Axis.vertical,
          child: Container(
            width: MediaQuery.of(context).size.width,
            child: Column(
              children: [
                for (int i = 0; i < 100; i++)
                  Text(
                    i.toString(),
                  ),
              ],
            ),
          ),
        ),
        atBottom
            ? Container()
            : Positioned(
                bottom: 10,
                right: 10,
                child: Container(
                  child: Icon(
                    Icons.arrow_circle_down,
                  ),
                ),
              ),
      ],
    );
  }
}

私は別のスタックオーバーフローの質問でこの解決策を見つけました:スクロールウィジェットの高さを決定する