Po czym poznać, że przewijanie nie jest potrzebne w SingleChildScrollView, gdy ekran jest tworzony po raz pierwszy?
Mam SingleChildScrollView
. Czasami child
jest dłuższy niż ekran, w takim przypadku SingleChildScrollView
umożliwia przewijanie. Ale czasami child
jest 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, child
gdy rozmiar SingleChildScrollView
jest 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 listener
zrobić, ale listener
nie 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 _scrollController
operatora 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 Column
z Text
widgetami 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
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;
});
}}
});
}
- Importuj bibliotekę Flutter harmonogramu:
import 'package:flutter/scheduler.dart';
- Utwórz flagę logiczną wewnątrz obiektu stanu, ale poza
build
metodą, aby śledzić, czybuild
została jeszcze wywołana:
bool buildCalledYet = false;
- Dodaj na początku
build
metody:
if (!firstBuild) {
firstBuild = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
atBottom = !(_scrollController.position.maxScrollExtent > 0);
});
});
}
(Flaga boolowska zapobiega build
wielokrotnemu 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