I created a quiz-application in Flutter. Before the quiz starts, a player will get on the start-page of a specific quiz. This page contains a header with title and background-image, and a body with the description of a quiz and a "start quiz"-button. Sometimes the description is very long, or the players fontSize is larger then usual, so I've put the description inside a scrollable container using SingleChildScrollView.
Everything works great and I already tested this with a large group. But there were some remarks that some people didn't read the whole description because they didn't know they could scroll. So my solution is ofcourse a visible scrollbar, so the users intuitively gets the feeling they can scroll inside the description. (btw, this is also the case where the question is a bit too long, or overflowing in case of a larger fontsize).
I tried to implement the scrollbar-widget around my SingleChildScrollView to make it obvious that it's a scrollable container and there is more information available, but the track doesn't start from the top of the scrollable container, instead, it starts somewhere around the middle of this. (also the case with the questions and answers, where I have a list of answers inside a scrollable Column).
Here is my simplified code (Scrollbar indicated with an arrow "--->"):
import 'package:Client/hive/quiz.dart' as quizmodel;
import 'package:Client/screens/play/quiz/quiz_question_page.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:iconsax/iconsax.dart';
import 'package:Client/constants.dart';
import 'package:shared_preferences/shared_preferences.dart';
class QuizStartPage2 extends StatefulWidget {
const QuizStartPage2({super.key, required this.quiz});
final quizmodel.Quiz quiz;
@override
State<QuizStartPage2> createState() => _QuizStartPage2State();
}
class _QuizStartPage2State extends State<QuizStartPage2> {
final ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: const Color.fromARGB(255, 245, 245, 245),
appBar: AppBar(
toolbarHeight: 80,
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.center,
end: Alignment.bottomCenter,
colors: <Color>[Colors.black, Colors.transparent]),
),
),
backgroundColor: Colors.transparent,
elevation: 0,
leadingWidth: 56 + defaultPadding,
centerTitle: true,
title: AutoSizeText(
"QuizTitle",
maxLines: 2,
minFontSize: 16,
style: GoogleFonts.dmSans(
fontSize: 30,
height: 1,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: <Shadow>[
Shadow(
offset: Offset(2.0, 4.0),
blurRadius: 10.0,
color: Color.fromARGB(30, 0, 0, 0),
),
],
),
),
leading: Padding(
padding: const EdgeInsets.only(
left: defaultPadding,
top: defaultPadding * 0.5,
bottom: defaultPadding * 0.5),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(
Iconsax.arrow_left_2,
color: Colors.black,
),
),
),
),
),
extendBodyBehindAppBar: true,
body: Stack(
children: [
Container(
height: size.height * 0.5,
width: size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black, Colors.black.withOpacity(0.65)],
),
),
child: Image(
image: NetworkImage(widget.quiz.image),
fit: BoxFit.cover,
alignment: Alignment.center,
errorBuilder: (BuildContext context, Object exception,
StackTrace? stackTrace) {
// Return an empty container when an error occurs (e.g., invalid image)
return Container(
color: Colors.blueGrey,
);
},
)),
Container(
height: size.height * 0.5,
width: size.width,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black, Colors.black.withOpacity(0.65)],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: size.height * 0.6,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(defaultBorderRadius * 3)),
color: Colors.white,
),
child: Padding(
padding: const EdgeInsets.all(defaultPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
----> child: Scrollbar(
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: scrollController,
physics: ScrollPhysics(),
padding: EdgeInsets.all(0),
child: AutoSizeText(
"This is the description of the quiz, I made it extra long to see the effect of the scrollbar it is currently having. This is not the behaviour I wanted to have and instead I want the scrollbar to start at the top of the scrollable-container. This is the description of the quiz, I made it extra long to see the effect of the scrollbar it is currently having. This is not the behaviour I wanted to have and instead I want the scrollbar to start at the top of the scrollable-container. Lorem Ipsum solor set amet Lorem Ipsum solor set amet Lorem Ipsum solor set amet .",
minFontSize: 12,
style: GoogleFonts.dmSans(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.start,
),
),
),
),
SafeArea(
top: false,
child: GestureDetector(
onTap: startQuiz,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(defaultBorderRadius * 3)),
color: Colors.black,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: defaultPadding * 3,
vertical: defaultPadding * 1.5),
child: Text("Start Quiz",
textAlign: TextAlign.center,
style: GoogleFonts.dmSans(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: Colors.white)),
)),
),
),
],
),
)),
),
],
),
);
}
_setBeginTime() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('started_at', DateTime.now().toString());
/* print("Quiz started at: ${prefs.getString('started_at')}"); */
}
Future<void> startQuiz() async {
_setBeginTime();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuizQuestionPage(quiz: widget.quiz),
),
);
}
}
Screenshot: notice scrollbar not beginning at the top
I have tried to implement the RawScrollbar, but it still gives the same result. The scrollbar doesn't start from the top of the container.
I also tried to work with "Slivers" but I'm don't have a good example to work with. If someone can provide me a simple example with a scrollbar around a scrollable container with text, I can work with this as well.
For anyone who is visiting this in the future: I found a fix. Thanks to this thread: https://stackoverflow.com/a/64405574/17211590.
use MediaQuery.removePadding widget with removeTop: true
Use with Scrollbar
Also works with RawScrollbar
For some reason flutter adds a top-padding, and in my case it was gigantic relative to the scrollable text. This is also not visible in when using the DevTools!