I got four different Tabs at my Bottom Navigation and one of those tabs (the last one) is a SingleChildScrollView. I want that I can click on the Icons on the Bottom Navigation AND also can scroll trough the App. That works fine, only the last tab makes a lot of trouble: My Problem is that when I scroll down to the SingleChildScrollView (the last tab), I can't scroll out of it to the tab above it anymore. That's what my PageView looks like (it's the Body):
body: PageView(
scrollDirection: Axis.vertical,
onPageChanged: (page) {
setState(
() {
_selectedIndex = page;
},
);
},
controller: _controller,
children: [
Home(),
Games(),
Shop(),
AboutUs(),
],
),
);
}
And that's what my Tab (AboutUs()) looks like:
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.vertical,
physics: BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// In there are many many children (Containers, etc. :))
],
),
);
}
Sorry for my bad explaination I hope someone can help! Thanks already! :)
One possible way is to wrap your SingleChildScrollView with a NotificationListener like so:
class AboutUs extends StatelessWidget {
final PageController pageController;
const AboutUs({Key? key, required this.pageController}) : super(key: key);
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollUpdateNotification>(
onNotification: (notification) {
if (pageController.page == 3) {
if (notification.metrics.pixels < -50) {
pageController.previousPage(duration: const Duration(milliseconds: 200), curve: Curves.ease);
}
}
return false;
},
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
physics: const BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// In there are many many children (Containers, etc. :))
],
),
)
);
}
}
Note that you will need to pass down your PageController to the AboutUs Widget so you can go back up to the previous page.
Related
i have an animatedSwitcher but with effect of swip left and right, i reach that by using Two widget:
a PageView builder with a transparent background and an animatedSwitcher. inside a stack i can swip smoothly between widgets.
the problem is the widgets of animatedSwitcher are scrollable but the PageView builder is above the animatedSwitcher so i cant scroll up and down widget of animatedSwitcher. (are not clickable in general).
the aim is to reach the same result as the gif down below. but without a pageview builder.
any suggestion will be helpful and thanks.
the code:
int selectedPage = 0;
PageController _controller = PageController();
#override
Widget build(BuildContext context) {
List<Widget> _pages = [
Container(color: Colors.blue),
Container(color: Colors.red),
];
return Stack(
alignment: Alignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Container(
key: ValueKey(_pages[selectedPage]),
child: _pages[selectedPage],
),
),
PageView.builder(
onPageChanged: (value) {
setState(() {
selectedPage = value;
});
},
controller: _controller,
physics: const ClampingScrollPhysics(),
itemCount: _pages.length,
itemBuilder: (context, index) {
return Container();
},
),
],
);
}
The keyboard hides my ListView (GroupedListView). I think it's because of the Expanded Widget.
My body:
Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GroupedListView<dynamic, String>(
controller: _scrollController,
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemBuilder: (context, message) {
return ListTile(
title: ChatBubble(message),
);
},
elements: messages,
groupBy: (message) => DateFormat('MMMM dd,yyyy')
.format(message.timestamp.toDate()),
groupSeparatorBuilder: (String groupByValue) =>
getMiddleChatBubble(context, groupByValue),
itemComparator: (item1, item2) =>
item1.timestamp.compareTo(item2.timestamp),
useStickyGroupSeparators: false,
floatingHeader: false,
order: GroupedListOrder.ASC,
),
),
),
WriteMessageBox(
group: group,
groupId: docs[0].id,
tokens: [widget.friendToken])
],
);
Why the resizeToAvoidBottomInset isn't working?
I have opened an issue to the Flutter team
In short: use reversed: true.
What you see is the expected behavior for the following reason:
ListView preserves its scroll offset when something on your screen resizes. This offset is how many pixels the list is scrolled to from the beginning. By default the beginning counts from the top and the list grows to bottom.
If you use reversed: true, the scroll position counts from the bottom, so the bottommost position is 0, and the list grows from bottom to the top. It has many benefits:
The bottommost position of 0 is preserved when the keyboard opens. So does any other position. At any position it just appears that the list shifts to the top, and the last visible element remains the last visible element.
Its easier to sort and paginate messages when you get them from the DB. You just sort by datetime descending and append to the list, no need to reverse the object list before feeding it to the ListView.
It just works with no listeners and the controller manipulations. Declarative solutions are more reliable in general.
The rule of thumb is to reverse the lists that paginate with more items loading at the top.
Here is the example:
import 'package:flutter/material.dart';
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: 30,
reverse: true,
itemBuilder: (context, i) => ListTile(title: Text('Item $i')),
),
),
const TextField(),
],
),
),
);
}
}
As for resizeToAvoidBottomInset, it does its job. The Scaffold is indeed shortened with the keyboard on. So is ListView. So it shows you less items. For non-reversed list, gone are the bottommost.
It looks like you want the GroupedListView to be visible from the last line. The WriteMessageBox is pushed up by the keyboard and obscures the last messages. The most direct solution is to scroll the list to the bottom when the keyboard is visible. That is, when the WriteMessageBox gains focus.
Add a FocusScope to the WriteMessageBox in the build() method. It becomes
FocusScope(
child: Focus(
child: WriteMessageBox(),
onFocusChange: (focused) {
if (focused) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
)
)
Screenshot:
Code:
You can use MediaQueryData to get the height of keyboard, and then scroll the ListView up by that number.
Create this class:
class HandleScrollWidget extends StatefulWidget {
final BuildContext context;
final Widget child;
final ScrollController controller;
HandleScrollWidget(this.context, {required this.controller, required this.child});
#override
_HandleScrollWidgetState createState() => _HandleScrollWidgetState();
}
class _HandleScrollWidgetState extends State<HandleScrollWidget> {
double? _offset;
#override
Widget build(BuildContext context) {
final bottom = MediaQuery.of(widget.context).viewInsets.bottom;
if (bottom == 0) {
_offset = null;
} else if (bottom != 0 && _offset == null) {
_offset = widget.controller.offset;
}
if (bottom > 0) widget.controller.jumpTo(_offset! + bottom);
return widget.child;
}
}
Usage:
final ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('ListView')),
body: HandleScrollWidget(
context,
controller: _controller,
child: Column(
children: [
Expanded(
child: ListView.builder(
controller: _controller,
itemCount: 100,
itemBuilder: (_, i) => ListTile(title: Text('Messages #$i')),
),
),
TextField(decoration: InputDecoration(hintText: 'Write a message')),
],
),
),
);
}
It appears that you are using text fields so it hides data or sometimes it may overflow borders by black and yellow stripes
better to use SingleChildScrollView and for scrolling direction use scrollDirection with parameters Axis.vertical or Axis.horizontal
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child :Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GroupedListView<dynamic, String>(
controller: _scrollController,
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemBuilder: (context, message) {
return ListTile(
title: ChatBubble(message),
);
},
elements: messages,
groupBy: (message) => DateFormat('MMMM dd,yyyy')
.format(message.timestamp.toDate()),
groupSeparatorBuilder: (String groupByValue) =>
getMiddleChatBubble(context, groupByValue),
itemComparator: (item1, item2) =>
item1.timestamp.compareTo(item2.timestamp),
useStickyGroupSeparators: false,
floatingHeader: false,
order: GroupedListOrder.ASC,
),
),
),
WriteMessageBox(
group: group,
groupId: docs[0].id,
tokens: [widget.friendToken])
],
);
);
Please try this solution. Hope it will work for you. Thanks.
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GroupedListView<dynamic, String>(
scrollDirection: Axis.vertical,
shrinkWrap: true,
controller: _scrollController,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemBuilder: (context, message) {
return ListTile(
title: ChatBubble(message),
);
},
elements: messages,
groupBy: (message) =>
DateFormat('MMMM dd,yyyy').format(message.timestamp.toDate()),
groupSeparatorBuilder: (String groupByValue) =>
getMiddleChatBubble(context, groupByValue),
itemComparator: (item1, item2) =>
item1.timestamp.compareTo(item2.timestamp),
useStickyGroupSeparators: false,
floatingHeader: false,
order: GroupedListOrder.ASC,
),
),
),
WriteMessageBox(
group: group, groupId: docs[0].id, tokens: [widget.friendToken])
In short: use reversed: true, jump the scrolling position to 0.
final scrollController = ScrollController();
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
Widget _buildScrollView(){
return SingleChildScrollView(
reverse: true,
controller: scrollController,
child: [....],
);
}
I have the following Widget which displays vertically a list of pictures of some recipes, and when swipe to the right page of each of these recipes, it goes to its description page. The problem here is that when I go to DescriptionScreen, I can scroll down and go to other recipies' RecipeScreen. I want to block that, to allow vertical scroll only when user is on RecipeScreen, else if he is on DescriptionScreen, to be able just to swipe to left and continue scrolling. How it would be possible to achieve that?
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
body: Column(
children: [
Expanded(
child: PageView.builder(
controller: verticalPageController,
scrollDirection: Axis.vertical,
itemCount: recipes.length,
allowImplicitScrolling: true,
itemBuilder: (BuildContext context, int index) {
return PageView(
controller: pageControllers[index],
children: [
RecipeScreen(
recipe: recipes[index],
onRecipeDelete: onRecipeDelete,
),
),
DescriptionScreen(
recipeId: recipes[index].id,
onRecipeSave: onRecipeSave,
),
],
);
},
),
),
SafeArea(
top: false,
child: Container(height: 0),
),
],
),
bottomNavigationBar: BottomBar(
page: currentPage,
),
);
}
I would add a onPageChanged to your horizontal PageViews and when the user goes to the DescriptionScreen, you set the physics of your vertical PageView to NeverScrollableScrollPhysics.
High level code:
class MyStatefulWidgetState extends State<MyStatefulWidget> {
ScrollPhysics physics;
void setScrollPhysics(ScrollPhysics physics) {
setState(() {
this.physics = physics
});
}
Widget build(BuildContext context) {
return Scaffold(
body: //...
PageView.builder(
controller: verticalPageController,
// ...
physics: physics, // this line will enable / disable scroll
itemBuilder: (ctx, index) {
return PageView(
controller: pageControllers[index],
onPageChanged: (page) {
// enable / disable vertical scrolling depending on page
setScrollPhysics(page == 1 ? NeverScrollableScrollPhysics() : null);
}
)
}
)
);
}
}
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.
I want to use a PageView with vertical axis and move between the pages using the mouse-scroll, but when I use the mouse-scroll the page don't scroll... The page only scroll when I click and swipe to up/down.
There is any way to do that?
I want to keep the property pageSnapping: true
The problem:
When I try to mouse-scroll the page, it don't change, it just back to initial offset.
But when I click and swipe works...
class Body extends StatefulWidget {
#override
_BodyState createState() => _BodyState();
}
class _BodyState extends State<Body> {
PageController _controller = PageController(keepPage: true);
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: Sizing.size.height,
width: Sizing.size.width,
child: Stack(
children: <Widget>[
PageView(
scrollDirection: Axis.vertical,
controller: _controller,
children: <Widget>[
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.orange),
],
),
],
),
),
);
}
}
To use the mouse scroll you must disable the movement of the PageView by setting
physics: NeverScrollableScrollPhysics().
Then you have to manually intercept the mouse scroll through a Listener. If you also want to recover the PageView classic movement through swipe you must use a GestureDetector.
Here is an example code:
class _HomepageState extends State<Homepage> {
final PageController pageController = PageController();
// this is like a lock that prevent update the PageView multiple times while is
// scrolling
bool pageIsScrolling = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GestureDetector(
// to detect swipe
onPanUpdate: (details) {
_onScroll(details.delta.dy * -1);
},
child: Listener(
// to detect scroll
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
_onScroll(pointerSignal.scrollDelta.dy);
}
},
child: PageView(
scrollDirection: Axis.vertical,
controller: pageController,
physics: NeverScrollableScrollPhysics(),
pageSnapping: true,
children: [
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.orange),
],
),
),
)),
);
}
void _onScroll(double offset) {
if (pageIsScrolling == false) {
pageIsScrolling = true;
if (offset > 0) {
pageController
.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut)
.then((value) => pageIsScrolling = false);
print('scroll down');
} else {
pageController
.previousPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut)
.then((value) => pageIsScrolling = false);
print('scroll up');
}
}
}
}
To make stuff scrollable in general you can wrap the widget (>>right click on the widget you want to make scrollable>>refactor>>wrap with widget) in a SingleChildScrollView().
Thanks to Luca!
I just modified the entire thing to do the same, what was the question asked for.
Listener(
onPointerMove: (pointerMove) {
if (pointerMove is PointerMoveEvent) {
_onScroll(pointerMove.delta.dy * -1);
print(pointerMove.delta.dy);
}
},
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
_onScroll(pointerSignal.scrollDelta.dy);
print(pointerSignal.scrollDelta.dy);
}
},
child: SingleChildScrollView(
child: Container(
constraints: BoxConstraints(
minHeight: SizeConfig.screenHeight * .9),
width: SizeConfig.screenWidth,
height: SizeConfig.screenWidth / 2,
decoration: BoxDecoration(
image: DecorationImage(
alignment: Alignment.centerRight,
image: AssetImage(
'assets/images/background_circles.png'))),
child: PageView(
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
pageSnapping: true,
scrollDirection: Axis.vertical,
children: [