Como saber se a rolagem não é necessária em um SingleChildScrollView quando a tela é construída pela primeira vez?
Eu tenho um SingleChildScrollView
. Às vezes, child
é mais longo do que a tela, caso em que SingleChildScrollView
permite rolar. Mas também às vezes child
é menor do que a tela, caso em que nenhuma rolagem é necessária.
Estou tentando adicionar uma seta na parte inferior da tela que indica ao usuário que ele pode / deve rolar para baixo para ver o resto do conteúdo. Eu implementei isso com sucesso, exceto no caso em que o child
do SingleChildScrollView
é mais curto do que a tela. Neste caso, nenhuma rolagem é necessária, então eu gostaria de não mostrar a seta.
Tentei fazer um listener
para fazer isso, mas o listener
não é ativado até que você comece a rolar, e neste caso você não pode rolar.
Também tentei acessar as propriedades de _scrollController
no operador ternário que mostra a seta, mas uma exceção é lançada:ScrollController not attached to any scroll views.
Aqui está um aplicativo de amostra completo que mostra o que estou fazendo, então você pode simplesmente copiar e colar se quiser que ele seja executado. Substituí todo o conteúdo por um Column
de Text
widgets para simplificar:
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,
),
),
),
],
);
}
}
Respostas
Você precisa verificar se o _scrollController está anexado a uma visualização de rolagem usando sua propriedade hasClients primeiro.
@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;
});
}
});
}
mudar para:
@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;
});
}}
});
}
- Importe a biblioteca Flutter do planejador:
import 'package:flutter/scheduler.dart';
- Crie um sinalizador booleano dentro do objeto de estado, mas fora do
build
método para rastrear sebuild
já foi chamado:
bool buildCalledYet = false;
- Adicione o seguinte no início do
build
método:
if (!firstBuild) {
firstBuild = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
atBottom = !(_scrollController.position.maxScrollExtent > 0);
});
});
}
(O sinalizador booleano evita que esse código build
seja chamado repetidamente.)
Aqui está o código completo do aplicativo de amostra que implementa esta solução:
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,
),
),
),
],
);
}
}
Encontrei essa solução em outra questão do Stack Overflow: Determinar a altura do widget de rolagem