Po czym poznać, że przewijanie nie jest potrzebne w SingleChildScrollView, gdy ekran jest tworzony po raz pierwszy?

Nov 25 2020

Mam SingleChildScrollView. Czasami childjest dłuższy niż ekran, w takim przypadku SingleChildScrollViewumożliwia przewijanie. Ale czasami childjest też krótszy niż ekran, w którym to przypadku nie jest potrzebne przewijanie.

Próbuję dodać strzałkę u dołu ekranu, która wskazuje użytkownikowi, że może / powinien przewinąć w dół, aby zobaczyć resztę treści. Udało mi się to zaimplementować, z wyjątkiem przypadku, childgdy rozmiar SingleChildScrollViewjest krótszy niż ekran. W tym przypadku przewijanie nie jest potrzebne, więc nie chciałbym w ogóle pokazywać strzałki.

Próbowałem to listenerzrobić, ale listenernie jest aktywowany, dopóki nie zaczniesz przewijać, aw tym przypadku nie możesz przewijać.

Próbowałem również uzyskać dostęp do właściwości _scrollControlleroperatora trójskładnikowego, który pokazuje strzałkę, ale jest zgłaszany wyjątek:ScrollController not attached to any scroll views.

Oto kompletna przykładowa aplikacja pokazująca, co robię, więc możesz ją po prostu skopiować i wkleić, jeśli chcesz zobaczyć, jak działa. Wymieniłem wszystkie treści z Columnz Textwidgetami dla uproszczenia:

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

Odpowiedzi

VinayHP Nov 25 2020 at 19:15

Musisz najpierw sprawdzić, czy _scrollController jest dołączony do widoku przewijania, używając najpierw jego właściwości hasClients.

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

zmień na:

@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. Importuj bibliotekę Flutter harmonogramu:
import 'package:flutter/scheduler.dart';
  1. Utwórz flagę logiczną wewnątrz obiektu stanu, ale poza buildmetodą, aby śledzić, czy buildzostała jeszcze wywołana:
bool buildCalledYet = false;
  1. Dodaj na początku buildmetody:
if (!firstBuild) {
      firstBuild = true;
      SchedulerBinding.instance.addPostFrameCallback((_) {
        setState(() {
          atBottom = !(_scrollController.position.maxScrollExtent > 0);
        });
      });
    }

(Flaga boolowska zapobiega buildwielokrotnemu wywoływaniu tego kodu ).

Oto pełny kod przykładowej aplikacji implementującej to rozwiązanie:

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

Znalazłem to rozwiązanie w innym pytaniu o przepełnienie stosu: Określ wysokość widżetu przewijania