Reputation: 151
I have a SingleChildScrollView
. Sometimes its child
is longer than the screen, in which case the SingleChildScrollView
lets you scroll. But also sometimes its child
is shorter than the screen, in which case no scrolling is needed.
I am trying to add an arrow to the bottom of the screen which hints to the user that they can/should scroll down to see the rest of the contents. I successfully implemented this except in the case where the child
of the SingleChildScrollView
is shorter than the screen. In this case no scrolling is needed, so I would like to not show the arrow at all.
I've tried making a listener
to do this, but the listener
isn't activated until you start scrolling, and in this case you can't scroll.
I've also tried accessing the properties of the _scrollController
in the ternary operator which shows the arrow, but an exception is thrown: ScrollController not attached to any scroll views.
Here is a complete sample app showing what I'm doing, so you can just copy and paste it if you want to see it run. I replaced all of the contents with a Column
of Text
widgets for simplicity:
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,
),
),
),
],
);
}
}
Upvotes: 3
Views: 1947
Reputation: 151
import 'package:flutter/scheduler.dart';
build
method to track whether build
has been called yet:bool buildCalledYet = false;
build
method:if (!firstBuild) {
firstBuild = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
atBottom = !(_scrollController.position.maxScrollExtent > 0);
});
});
}
(The boolean flag prevents this code from causing build
to be called over and over again.)
Here's the complete code of the sample app implementing this 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,
),
),
),
],
);
}
}
I found this solution on another Stack Overflow question: Determine Scroll Widget height
Upvotes: 5