Determine Scroll Widget height - flutter

I would like to determine if a 'scrollable' widget indeed needs to scroll. I would ultimately like to show something like 'Scroll for more'. How do I achieve this?
I could use a combination of LayoutBuilder and a ScrollController. However, ScrollController gives me maxScrollExtent and minScrollExtent only after any event - like say for example user trying to scroll
I would like to know during 'build' so that I can determine to show 'Scroll for more' or not depending on the screen dimensions
class _MyHomePageState extends State<MyHomePage> {
int value = 0;
String text = 'You have pushed the button 0 times\n';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 9,
child: SingleChildScrollView(
child: Text(text),
),
),
Expanded(
flex: 1,
child: Text('Scroll for more')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
value += 1;
text = text + 'You have pushed the button $value times \n';
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
I would like to dynamically display
Text('Scroll for more'),
only if the SingleChildScrollView needs to be scrolled to view the entire content. Please note, I am just giving the above code as example.
In my real case, I have a StreamBuilder inside a SingleChildScrollView and I cannot determine how much of data is going to be flowing in from the stream. I also do not have any button or tap or any gesture to access the controller and setState
Thanks in advance for any help

class _MyHomePageState extends State<MyHomePage> {
bool showMore = false;
final scrollController = ScrollController();
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
showMore = scrollController.position.maxScrollExtent > 0;
});
});
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 9,
child: SingleChildScrollView(
controller: scrollController,
child: SizedBox(height: 650, child: Text('blah')),
),
),
if (showMore) Expanded(flex: 1, child: Text('Scroll for more')),
],
),
),
);
}
}

Related

Register tap on empty space around widget

Working on a flutter web project. I have a row which has 3 widgets:
From left to right:
Sidebar
Sidebar content
body
Widget _buildBody() {
final screenwidth = MediaQuery.of(context).size.width;
editpanel = screenwidth * 0.3;
final editor = ViewProvider.of(context).isEditPanelOpen
? (screenwidth - sidebar - editpanel)
: (screenwidth - sidebar);
final ViewProvider viewProvider = Provider.of<ViewProvider>(context);
return Row(
Sidebar()
_loadSidebarContent(bloc.editPanelIndex),
_sidebarHandler(viewProvider),
Center(
child: SizedBox(
width: editor * 0.8,
child: Center(
child: MyWidget(),
),
),
),
],
);
}
I need to register tap if user taps on anything except the Appbar, Sidebar, Sidebarcontent, on MyWidget.
So I wrapped the entire scaffold with gesture detector and tried using IgnorePointer for the specific widgets.
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
.. call some specific function
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: Size(
MediaQuery.of(context).size.width,
height + 80,
),
child: IgnorePointer(
child: Appbar(),
ignoring: true,
),
),
body: _buildBody(),
),
);
}
Issue is: MyWidget is getting ignored all the time. I don't want to fire the specificFunc() when user taps on any of the: Appbar, Sidebar, Sidebarcontent, or MyWidget.
Basically if user taps the white space around MyWidget specificFunction will be called
Wrap the whole Scaffold widget with GestureDector is not a good idea.
Instead wrap the container (white space around your button) with the detector and supply the button as a child.
In the following sample, the amber area is your white one. Tapping the amber area, and the button produces a separate log.
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
if (kDebugMode) {
print('Amber area tapped!');
}
},
child: Container(
color: Colors.amber,
width: 400,
height: 400,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all<Color>(Colors.blue),
backgroundColor: MaterialStateProperty.all<Color>(
Colors.white)),
onPressed: () {
if (kDebugMode) {
print('Button clicked.');
}
},
child: const Text('A Button'),
),
],
)),
),
)
],
),
),
);
}
}
You could use a stack (https://api.flutter.dev/flutter/widgets/Stack-class.html) and wrap the widget at the very bottom of the stack with a gesture detector.
To position the other widgets correctly, you could use the Positioned widget.
Instead of ignorePointer you should be using AbsorbPointer which will absorb the pointer and not pass it to the content below it

Flutter scrollable layout with dynamic child

I want to create a generic Layout which accepts a child Widget as a parameter, that lays out the content as follows:
I have an AppBar at the Top, a Title (headline), and below that the Content (could be anything). At the bottom, I have a Column with a few buttons. If the content is too big for the screen, all those widgets, except the AppBar, are scrollable. If the content fits the screen, the title and content should be aligned at the top, and the buttons at the bottom.
To showcase what I mean, I created a drawing:
It is easy to create to scrollable content functionality. But I struggle with laying out the content so that the buttons are aligned at the bottom, if the content does NOT need to be scrollable.
It is important to say that I don't know the height of the content widget or the buttons. They are dynamic and can change their height. Also, the title is optional and can have two different sizes.
What I tried is the following:
import 'package:flutter/material.dart';
class BaseScreen extends StatelessWidget {
final String? title;
final bool bigHeader;
final Widget child;
final Widget bottomButtons;
const BaseScreen({
Key? key,
required this.child,
required this.bottomButtons,
this.bigHeader = true,
this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final AppBar appBar = AppBar(
title: Text("AppBar"),
);
double minChildHeight = MediaQuery.of(context).size.height -
MediaQuery.of(context).viewInsets.bottom -
MediaQuery.of(context).viewInsets.top -
MediaQuery.of(context).viewPadding.bottom -
MediaQuery.of(context).viewPadding.top -
appBar.preferredSize.height;
if (title != null) {
minChildHeight -= 20;
if (bigHeader) {
minChildHeight -= bigHeaderStyle.fontSize!;
} else {
minChildHeight -= smallHeaderStyle.fontSize!;
}
}
final Widget content = Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title != null)
Text(
title!,
style: bigHeader ? bigHeaderStyle : smallHeaderStyle,
textAlign: TextAlign.center,
),
if (title != null)
const SizedBox(
height: 20,
),
ConstrainedBox(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
child,
bottomButtons,
],
),
constraints: BoxConstraints(
minHeight: minChildHeight,
),
),
],
);
return Scaffold(
appBar: appBar,
body: SingleChildScrollView(
child: content,
),
);
}
TextStyle get bigHeaderStyle {
return TextStyle(fontSize: 20);
}
TextStyle get smallHeaderStyle {
return TextStyle(fontSize: 16);
}
}
The scrolling effects work perfectly, but the Buttons are not aligned at the bottom. Instead, they are aligned directly below the content. Does anyone know how I can fix this?
DartPad you can check here
customscrollview tutorial
Scaffold(
// bottomNavigationBar: ,
appBar: AppBar(
title: Text(" App Bar title ${widgets.length}"),
),
//============
body: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
// controller: _mycontroller,
children: [
title,
...contents,
// ---------------------This give Expansion and button get down --------
Expanded(
child: Container(),
),
// ---------------------This give Expansion and button get down --------
Buttons
],
),
)
],
))
We can Achieve with the help of CustomScrollView widget and Expanded widget.here Expanded widget just expand between the widget
Sample Code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(debugShowCheckedModeBanner: false, home: MyApp()),
);
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var widgets = [];
var _mycontroller = ScrollController();
#override
Widget build(BuildContext context) {
var title = Center(
child: Text(
"Scrollable title ${widgets.length}",
style: TextStyle(fontSize: 30),
));
var contents = [
...widgets,
];
var Buttons = Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 100,
child: ElevatedButton(
onPressed: () {
setState(() {
widgets.add(Container(
height: 100,
child: ListTile(
title: Text(widgets.length.toString()),
subtitle: Text("Contents BTN1"),
),
));
});
// _mycontroller.jumpTo(widgets.length * 100);
},
child: Text("BTN1"),
),
),
)),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 100,
child: ElevatedButton(
onPressed: () {
setState(() {
if (widgets.length > 0) {
widgets.removeLast();
}
});
// _mycontroller.jumpTo(widgets.length * 100);
},
child: Text("BTN2"),
),
),
))
],
);
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SafeArea(
child: Scaffold(
// bottomNavigationBar: ,
appBar: AppBar(
title: Text(" App Bar title ${widgets.length}"),
),
body: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
// controller: _mycontroller,
children: [
title,
...contents,
Expanded(
child: Container(),
),
Buttons
],
),
)
],
)),
),
);
}
}
Try this:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BaseScreen(
bottomButtons: [
ElevatedButton(onPressed: () {}, child: const Text('Button 1')),
ElevatedButton(onPressed: () {}, child: const Text('Button 2')),
],
content: Container(
color: Colors.lightGreen,
height: 200,
),
title: 'Title',
),
);
}
}
class BaseScreen extends StatelessWidget {
final bool bigHeader;
final List<Widget> bottomButtons;
final String? title;
final Widget content;
const BaseScreen({
this.bigHeader = true,
required this.bottomButtons,
required this.content,
this.title,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('AppBar'),
),
body: CustomScrollView(
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Column(
children: [
if (title != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
title!,
style: bigHeader ? _bigHeaderStyle : _smallHeaderStyle,
textAlign: TextAlign.center,
),
),
content,
const Spacer(),
...bottomButtons,
],
),
),
],
),
);
}
TextStyle get _bigHeaderStyle => const TextStyle(fontSize: 20);
TextStyle get _smallHeaderStyle => const TextStyle(fontSize: 16);
}
Screenshots:
without_scrolling
scrolled_up
scrolled_down

How to make a list of widgets mutate state based on activity on any one widget?

I have a GridView Widget which has a list of Custom Buttons. I want to freeze/disable all the remaining buttons in the list if one of the button in the list is clicked.
How can this be done? I had some ideas around perhaps making a Stateful List Widget that holds the state of whats clicked. But i do not know how to mutate that state. Or broadcast it to all the other objects to manipulate its state. Some guidance would be helpful.
#override
Widget build(BuildContext context) {
final title = 'Grid List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: <Widget>[
Center(
child: Container(
height: 120.0,
width: 120.0,
color: Colors.blue[50],
child: Align(
alignment: Alignment.topRight,
child: FlutterLogo(
size: 60,
),
),
),
),
new Expanded(
child: GridView.count(
// shrinkWrap: false,
// Create a grid with 2 columns. If you change the scrollDirection to
// horizontal, this produces 2 rows.
childAspectRatio: 1.2,
crossAxisCount: 2,
// Generate 100 widgets that display their index in the List.
children: <Center> [
Center(child: Option("Option 1")),
Center(child: Option("Option 2")),
Center(child: Option("Option 3")),
Center(child: Option("Option 4"))
],
)
)
],
),
),
);
}
}
You should employ a state management solution such as riverpod (pub.dev) (docs).
This isn't a scalable example but should be enough to get you started.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final disabledButtonsProvider = StateProvider<List<bool>>((_) => List.generate(4, (_) => false));
class ButtonGrid extends ConsumerWidget {
const ButtonGrid({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final disabledButtons = watch(disabledButtonsProvider).state;
return Scaffold(
body: GridView.count(
crossAxisCount: 2,
children: List.generate(
4,
(index) => ElevatedButton(
onPressed: disabledButtons[index]
? null
: () => context.read(disabledButtonsProvider).state =
List.generate(4, (i) => i != index),
child: Text(index.toString()),
),
),
),
);
}
}

Flutter: Button should output an widget but doesn't

This is my function:
Widget RandomSkill(Widget SkillJonglieren) {
return Widget SkillJonglieren;
This is the stateless Widget "SkillJonglierten"
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Column(
children: <Widget>[
Text('Jonglieren'),
Image.asset('images/Jonglieren.jpg')
],
),
),
);
}
}
This is the button
body: Align(alignment: Alignment.bottomCenter,
child: RaisedButton(
onPressed: RandomSkill,
child: Text('Zufälliger Skill')
),
));
I just want, that when i press the button that this widget is shown
You need a StatefulWidget. In its State class, you need to remember whether the SkillJonglierten is shown or not. If it's shown, you need to show it in your build() function. Otherwise, you should not show it in your build() function. So, this becomes: https://codepen.io/gazialankus/pen/rNxgBgx
class _MyHomePageState extends State<MyHomePage> {
bool isShown;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('Show'),
onPressed: () {
setState(() {
isShown = true;
});
}
),
if (isShown == true) SkillJonglieren()
],
),
),
);
}
}

flutter - chat's input box keyboard pushing message-list upwards

I have a fairly simple flutter app. It has a chat feature.
However, I have a problem with the chat feature.
It's made up of a widget does Scaffold and in it SingleChildScrollView - which has a message-list (container) and input-area (container). Code is attached.
Problem is: if I click on the input box, the keyboard opens and it pushes the message-list.
Pushing the message-list is an acceptable thing if you are already at the bottom of the message-list.
However, if the user scrolled up and saw some old messages, I don't want the message-list widget to be pushed up.
Also, I don't want the message-list to be pushed up if I have only a handful of messages (because that just makes the messages disappear when keyboard opens, and then I need to go and scroll to the messages that have been pushed [user is left with 0 visible messages until they scroll]).
I tried different approaches - like
resizeToAvoidBottomInset: false
But nothing seems to work for me, and this seems like it should be a straightforward behavior (for example, whatsapp act like the desired behavior).
My only option I fear is to listen to keyboard opening event, but I was hoping for a more elegant solution.
Here's my code:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
);
Widget buildInputArea(BuildContext context) {
return Container(
height: height * 0.1,
width: width,
child: Row(
children: <Widget>[
buildChatInput(),
buildSendButton(context),
],
),
);
}
Widget buildMessageList() {
return Container(
height: height * 0.8,
width: width,
child: ListView.builder(
controller: scrollController,
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
return buildSingleMessage(index);
},
),
);
}
This seems to work for me:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildMessageList(),
buildInputArea(context),
],
),
),
);
}
Widget buildInputArea(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: TextField(),
),
RaisedButton(
onPressed: null,
child: Icon(Icons.send),
),
],
);
}
Widget buildMessageList() {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: 50,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(index.toString()),
),
);
},
),
);
}
}
I think the problem is that you are using fixed sizes for all widgets. In this case it is better to use Expanded for the ListView and removing the SingleChildScrollView. That way the whole Column won't scroll, but only the ListView.
Try to use Stack:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(),
],
),
),
Positioned(
bottom: 0.0,
child: buildInputArea(context),
),
],
),
);
}
Setting resizeToAvoidBottomInset property to false in your Scaffold should work.
You can use NotificationListener to listen to scroll notifications to detect that user is at the bottom of the message-list. If you are at the bottom you can then set resizeToAvoidBottomInset to true.
Something like this should work
final resizeToAvoidBottomInset = true;
_onScrollNotification (BuildContext context, ScrollNotification scrollNotification) {
if (scrollNotification is ScrollUpdateNotification) {
// detect scroll position here
// and set resizeToAvoidBottomInset to false if needed
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: this.resizeToAvoidBottomInset,
body: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
return _onScrollNotification(context, scrollNotification);
},
child: SingleChildScrollView(
child: Column(
children: <Widget>[
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
),
);
}
this is technically already answered, and the answer is almost correct. However, I have found a better solution to this. Previously the author mentions that he wants to have a similar experience to WhatsApp. By using the previous solution, the listview would not be able to scrolldown to maxExtent when the sent button is pressed. To fix this I implemented Flex instead of Expanded, and use a singlechildscrollview for the input area
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
TextEditingController _textcontroller=TextEditingController();
List<String> messages=[];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: messages.length,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
print("From listviewbuilder: ${messages[index]}");
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(messages[index])
),
);
},
),
),
SingleChildScrollView(
child: Row(
children: <Widget>[
Expanded(
child: TextField(controller: _textcontroller),
),
RaisedButton(
onPressed: (){
Timer(Duration(milliseconds: 100), () {
_controller.animateTo(
_controller.position.maxScrollExtent,
//scroll the listview to the very bottom everytime the user inputs a message
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
});
setState(() {
messages.add(_textcontroller.text);
});
print(messages);
},
child: Icon(Icons.send),
),
],
),
),
],
),
),
);
}
}
It's better to use flex because expanded as the documentation says, expands over available space, whereas flex would resize to the appropriate proportion. This way if you are going for the "WhatsApp experience" in which the listview scrolls down once you sent a message. The listview would resize when the keyboard pops up and you will get to the bottom, instead of it not going fully to the bottom.