Comment savoir si le défilement n'est pas nécessaire dans un SingleChildScrollView lorsque l'écran est créé pour la première fois?
J'ai un SingleChildScrollView
. Parfois, il child
est plus long que l'écran, auquel cas le SingleChildScrollView
vous permet de faire défiler. Mais aussi parfois, il child
est plus court que l'écran, auquel cas aucun défilement n'est nécessaire.
J'essaie d'ajouter une flèche au bas de l'écran qui indique à l'utilisateur qu'il peut / doit faire défiler vers le bas pour voir le reste du contenu. J'ai implémenté cela avec succès sauf dans le cas où le child
de SingleChildScrollView
est plus court que l'écran. Dans ce cas, aucun défilement n'est nécessaire, je ne voudrais donc pas du tout afficher la flèche.
J'ai essayé de faire une listener
pour faire cela, mais le listener
n'est pas activé tant que vous ne commencez pas à faire défiler, et dans ce cas, vous ne pouvez pas faire défiler.
J'ai également essayé d'accéder aux propriétés de l' _scrollController
opérateur ternaire qui montre la flèche, mais une exception est levée:ScrollController not attached to any scroll views.
Voici un exemple d'application complet montrant ce que je fais, vous pouvez donc simplement le copier et le coller si vous voulez le voir fonctionner. J'ai remplacé tout le contenu par un Column
des Text
widgets pour plus de simplicité:
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,
),
),
),
],
);
}
}
Réponses
Vous devez vérifier si le _scrollController est attaché à une vue de défilement en utilisant d'abord sa propriété 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;
});
}
});
}
changer en:
@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;
});
}}
});
}
- Importez la bibliothèque Flutter du planificateur:
import 'package:flutter/scheduler.dart';
- Créez un indicateur booléen à l'intérieur de l'objet d'état mais en dehors de la
build
méthode pour savoir si ellebuild
a déjà été appelée:
bool buildCalledYet = false;
- Ajoutez ce qui suit au début de la
build
méthode:
if (!firstBuild) {
firstBuild = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
atBottom = !(_scrollController.position.maxScrollExtent > 0);
});
});
}
(L'indicateur booléen empêche ce code de provoquer build
d'être appelé encore et encore.)
Voici le code complet de l'exemple d'application implémentant cette solution:
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,
),
),
),
],
);
}
}
J'ai trouvé cette solution sur une autre question de Stack Overflow: Déterminer la hauteur du widget de défilement