Flutter block scroll of PageView inside another PageView - flutter

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);
}
)
}
)
);
}
}

Related

Flutter resizeToAvoidBottomInset true not working with Expanded ListView

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: [....],
);
}

Unable to scroll ListView even with ScrollPhysics

I have a screen where I need to search for a term from the Appbar, and the area below shows a Card with selections from the displayed list, and the area below that will show all the results returned, within a scrollable list.
The problem is that although the items returned are placed in a ListView.builder and ScrollPhysics is on, the list is not scrollable. If I click on the Card and try to drag, it scrolls for a bit. But one cannot drag by clicking on the list, or items in it.
import '...';
class DiagnosisAdd extends StatefulWidget {
#override
_DiagnosisAddState createState() => _DiagnosisAddState();
}
class _DiagnosisAddState extends State<DiagnosisAdd> {
TextField searchBar;
TextEditingController searchTextController;
Network connection;
List<ICDCode> DiagnosisList;
List<ICDCode> selectedDiagnoses;
#override
void initState() {
connection = Network();
DiagnosisList = [];
selectedDiagnoses = [];
// searchBar = A widget
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: searchBar,
),
body: ListView(
physics: AlwaysScrollableScrollPhysics(),
shrinkWrap: true,
children: [
Card(
child: ListTile(
title: Text("Selected Diagnoses"),
subtitle: Wrap(
children: List.generate(
selectedDiagnoses.length,
(index) => Text(selectedDiagnoses[index].disease),
growable: true,
),
),
),
),
ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: DiagnosisList.length,
itemBuilder: (BuildContext context, int position) {
ICDCode codeDiagnosis = DiagnosisList[position];
return RaisedButton(
child:
Text('${codeDiagnosis.code}, ${codeDiagnosis.disease}'),
onPressed: () {});
},
)
],
),
);
}
Future searchDiagnosis(String text) async {
if (text.length < 3) {
return false;
}
var response = await connection.searchICDbyDisease(
searchString: text,
);
final jsonResponse = await json.decode(response);
List<ICDCode> diagnosis_list =
await jsonResponse.map<ICDCode>((i) => ICDCode.fromJson(i)).toList();
setState(() {
DiagnosisList = diagnosis_list;
});
}
}
You can't scroll your ListView because you have another ListView.builder() inside that ListView that can be scrolled. You would have to make your ListView.builder() unscrollable:
ListView.builder(
physics: NeverScrollableScrollPhysics(),
)
You cannot have two nested widgets that can scroll together at the same time. You would have to disable the nested widget from scrolling so that its the ListView that you scroll instead of ListView.builder()
ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: DiagnosisList.length,
itemBuilder: (BuildContext context, int position) {
ICDCode codeDiagnosis = DiagnosisList[position];
return RaisedButton(
child:
Text('${codeDiagnosis.code}, ${codeDiagnosis.disease}'),
onPressed: () {});
},
)
],
),
);

Flutter listview builder Scroll controller listener not firing inside list view?

I have a listview builder widget inside another list view. Inner listview listener is not firing when scrolling position reaches to its end.
initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.maxScrollExtent ==
_scrollController.position.pixels) {function();}
}
Container(
child: Listview(
children: <Widget>[
Container(),
ListView.builder(
controller: _scrollController,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return Container();
},
),
]
)
)
You might have SingleChildScrollView attached before any widget :
so attach _scrollController to singleChildScrollView not listview
body: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
_chips(),
SizedBox(
height: 10,
),
_slider(),
_showGrid(),
],
),
),
the list view must scroll otherwise it won't work. Not only you have to remove the NeverScrollableScrollPhysics() but also add that list view into some container and set its height smaller then overall height of your ListView. Then the listView begin to scroll and the function will be triggered
ScrollController _scrollController = ScrollController();
List<int> list = [1, 2, 3, 4, 5];
initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.maxScrollExtent ==
_scrollController.position.pixels) {
print('firing');
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ControlBar(
title: Text('Home'),
),
),
body: ListView(
children: <Widget>[
Container(
height: 150,
child: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(list[index].toString()));
},
),
),
],
),
);
}

How do I get my page to scroll using Flutter?

So I'm working on a tarot app and I can't figure out how to make this page scrollable.
I'm currently using the element SingleChildScrollView to wrap my elements.
Right now it scrolls down part way and then gets stuck, and it bounces back up and I can't see the rest of my screen down below.
I'm thinking I should probably use a multi child scroll view widget but not sure how to get that to work.
What I'm looking for is to be able to display a list of elements and have them scroll on the page.
I'm sure I'm doing this wrong if someone could me out that would be awesome! :) Thanks in advance for your help and advice
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
child: getSpread(widget.selected),
),
ListView.builder(
itemCount: widget.selected.length,
itemBuilder: (context, index) {
final int cardNumber = widget.selected[index];
final TarotCard tarotCard = tarotMaster.tarotDeck[cardNumber];
return TarotCardDetails(tarotCard: tarotCard);
},
scrollDirection: Axis.vertical,
shrinkWrap: true,
),
BottomButton(
onTap: () {
Navigator.pushNamed(context, '/home');
print(widget.selected);
},
buttonTitle: 'BACK TO HOME',
),
],
),
),
),
);
}
}
Use physics in ListView.builder() this issue happens because flutter does not know what to scroll because it found two scrollable widget. You can specify a empty ScrollPhysics so flutter will know ListView will not need to be scrolled instead the entire page which is SingleChildScrollView widget items to be scroll
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
child: getSpread(widget.selected),
),
ListView.builder(
physics: ScrollPhysics(),
itemCount: widget.selected.length,
itemBuilder: (context, index) {
final int cardNumber = widget.selected[index];
final TarotCard tarotCard = tarotMaster.tarotDeck[cardNumber];
return TarotCardDetails(tarotCard: tarotCard);
},
scrollDirection: Axis.vertical,
shrinkWrap: true,
),
BottomButton(
onTap: () {
Navigator.pushNamed(context, '/home');
print(widget.selected);
},
buttonTitle: 'BACK TO HOME',
),
],
),
),
),
);
}
}
Hope this will help you.

How to display progress indicator in flutter when a certain condition is true?

I have widget which return CircularProgressIndicator()
it shows on the circular mark upper left of screen.
However I want to put this as overlay and put at the center of screen.
I am checking widget list but I cant find what Widget should I use as overlay.
On which layer should I put this on??
For now my code is like this ,when loading it shows CircularProgressIndicator instead of ListView
However I want to put CircularProgressIndicator() on ListView
Widget build(BuildContext context) {
if(loading) {
return CircularProgressIndicator();
}
return ListView.builder(
controller: _controller,
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index]),
);
},
);
}
Thank you very much for answers.
I solve with stack Widget like this below.
At first I try to use overlay, but I bumped into some errors.
So, I use simply stack.
#override
Widget build(BuildContext context) {
Widget tempWidget = new CircularProgressIndicator();
if(loading) {
tempWidget = new CircularProgressIndicator();
}
else {
tempWidget = new Center();//EmptyWidget
}
return Stack(
children: <Widget>[
ListView.builder(
controller: _controller,
itemCount: articles.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(articles[index].title),
onTap: () => onTapped(context,articles[index].url),
);
},
),
Center(
child: tempWidget
),
]
);
}
Stack(
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
color: Colors.red,
);
},
itemCount: 10,
),
Center(
child: CircularProgressIndicator(),
),
],
)
Stack(
children: <Widget>[
ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
color: Colors.red,
);
},
itemCount: 10,
),
isLoading? Container(child: Center(
child: CircularProgressIndicator(),
)): Container(), //if isLoading flag is true it'll display the progress indicator
],
)
or you can use futureBuilder or streamBuilder when loading data from somewhere and you want to change the ui depending on the state
To overlay or position items on top of each other you would usually use a Stack widget or Overlay as described here. For your usecase I would recommend checking out the modal progress hud package.