Wie können Sie feststellen, ob in einer SingleChildScrollView beim ersten Erstellen des Bildschirms kein Bildlauf erforderlich ist?

Nov 25 2020

Ich habe eine SingleChildScrollView. Manchmal childist es länger als der Bildschirm. In diesem Fall SingleChildScrollViewkönnen Sie einen Bildlauf durchführen. Aber manchmal childist es auch kürzer als der Bildschirm. In diesem Fall ist kein Scrollen erforderlich.

Ich versuche, am unteren Bildschirmrand einen Pfeil hinzuzufügen, der den Benutzer darauf hinweist, dass er nach unten scrollen kann / sollte, um den Rest des Inhalts zu sehen. Ich implementiert diese erfolgreich , außer in dem Fall , in dem die childvon der SingleChildScrollViewkürzer ist als der Bildschirm ist. In diesem Fall ist kein Bildlauf erforderlich, daher möchte ich den Pfeil überhaupt nicht anzeigen.

Ich habe versucht, dies listenerzu tun, aber das listenerwird erst aktiviert, wenn Sie mit dem Scrollen beginnen. In diesem Fall können Sie nicht scrollen.

Ich habe auch versucht, auf die Eigenschaften von _scrollControllerim ternären Operator zuzugreifen, der den Pfeil anzeigt, aber eine Ausnahme wird ausgelöst:ScrollController not attached to any scroll views.

Hier ist eine vollständige Beispiel-App, die zeigt, was ich tue. Sie können sie also einfach kopieren und einfügen, wenn Sie möchten, dass sie ausgeführt wird. Ich ersetzen alle Inhalte mit einem Columnvon TextWidgets für Einfachheit:

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,
                  ),
                ),
              ),
      ],
    );
  }
}

Antworten

VinayHP Nov 25 2020 at 19:15

Sie müssen zuerst mithilfe der Eigenschaft hasClients überprüfen, ob der _scrollController an eine Bildlaufansicht angehängt ist.

@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;
        });
      }
    });
  }

ändern:

@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. Importieren Sie die Scheduler Flutter-Bibliothek:
import 'package:flutter/scheduler.dart';
  1. Erstellen Sie ein boolesches Flag innerhalb des Statusobjekts, jedoch außerhalb der buildMethode, um zu verfolgen, ob buildes noch aufgerufen wurde:
bool buildCalledYet = false;
  1. Fügen Sie am Anfang der buildMethode Folgendes hinzu :
if (!firstBuild) {
      firstBuild = true;
      SchedulerBinding.instance.addPostFrameCallback((_) {
        setState(() {
          atBottom = !(_scrollController.position.maxScrollExtent > 0);
        });
      });
    }

(Das boolesche Flag verhindert, dass dieser Code immer buildwieder aufgerufen wird.)

Hier ist der vollständige Code der Beispiel-App, die diese Lösung implementiert:

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,
                  ),
                ),
              ),
      ],
    );
  }
}

Ich habe diese Lösung bei einer anderen Frage zum Stapelüberlauf gefunden: Bestimmen der Höhe des Bildlauf-Widgets