I am working on flutter project. I want to get content size of horizontal listview. When i click on option in list view , i want to check that option is in proper bound of screen or out of bound. If it is out of bound, then how to move in of bound?
Please suggest and help me to sort out
Thanks in advance
You can use ScrollController to get the size of listView. If it is on Column widget, wrap with Expanded widget to get available space.
class _TDState extends State<XT> {
late final ScrollController controller = ScrollController()
..addListener(() {
print(controller.position.maxScrollExtent);
});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: controller,
itemCount: 44,
itemBuilder: (context, index) => Text("item $index"),
))
],
),
);
}
}
Related
Nested scrolling?
I have three vertical pages in a PageView that I want to be able to flip between.
The pages consists of scrollable ListViews.
When a page is in focus the displayed list should be vertically scrollable, but when the list is scrolled to either end I want the pageView scrolling to take over the scroll behaviour and handle the page flipping to next page (like a web page with scrollable elements).
Example with scrolling lists below.
If the list scrolling is disabled the page flipping works.
How can I make both work?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: VerticalPageView(),
);
}
}
class VerticalPageView extends StatelessWidget {
VerticalPageView({Key key}) : super(key: key);
final PageController pageController = PageController();
final ScrollController scrollController = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: PageView(
controller: pageController,
pageSnapping: true,
scrollDirection: Axis.vertical,
children: <Widget>[
Container(
color: Colors.pinkAccent,
child: ListView.builder(
controller: scrollController,
itemCount: 100,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Text('page 0 item $index');
},
),
),
Container(
color: Colors.lightBlue,
child: ListView.builder(
controller: scrollController,
itemCount: 100,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Text('page 1 item $index');
},
),
),
Container(
color: Colors.lightGreen,
child: ListView.builder(
controller: scrollController,
itemCount: 100,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Text('page 2 item $index');
},
),
),
],
),
),
);
}
}
I think what you are trying to achieve is not impossible but needs a lot of study and care.
I have been trying to use several NotificationListener<ScrollNotification>'s to adapt the reaction depending on the position of the scroll but did not get anywhere.
Give a look to the Animation sample home.dart file in the Gallery App. It is full of insight in this regard.
The problem with this approach is basically what you say. When you disable programmatically the scrolling when reaching the end of the list to enable page scrolling, then you can switch to the other page but can't scroll the list in the other direction any more.
So, either you scroll the list or you scroll the pages, but not both.
Maybe you could add a GestureDetector() above everything and check on every drag update what is your situation below to configure accordingly the different scrollers.
Anyway, in case it is helpful to you I let you here an alternate solution using a CustomScrollView and SliverList's.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: VerticalPageView(),
);
}
}
class VerticalPageView extends StatelessWidget {
final ScrollController _scrollController = ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.pinkAccent,
child: Text('page 0 item $index'),
);
},
childCount: 100,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.lightBlue,
child: Text('page 1 item $index'),
);
},
childCount: 100,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.lightGreen,
child: Text('page 2 item $index'),
);
},
childCount: 100,
),
),
],
),
);
}
}
I made an app to display a list of hospitals under a state.
This is the Main.dart
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'package:emas_app/model/accounts_model.dart';
Future<String> _loadAsset() async{
return await rootBundle.loadString('Assets/accounts.json');
}
Future<Accounts> loadAccounts() async{
final response = await _loadAsset();
final jsonResponse = json.decode(response);
Accounts accounts = new Accounts.fromJson(jsonResponse);
return accounts;
}
class ProviderList extends StatefulWidget {
#override
ListState createState() {
return new ListState();
}
}
class ListState extends State<ProviderList> {
#override
Widget build(BuildContext context) {
Widget newbody = new ExpansionTile(
title: new Text("State Name"),
children: <Widget>[
new FutureBuilder<Accounts>(
future: loadAccounts(),
builder: (context, snapshot){
if(snapshot.hasData){
return new ListView.builder(
itemCount: snapshot.data.accountinfo.length,
itemBuilder: (context, index){
String username = snapshot.data.accountinfo[index].name;
String address = snapshot.data.accountinfo[index].street;
String lat = snapshot.data.accountinfo[index].coordinates.lat;
String lng = snapshot.data.accountinfo[index].coordinates.lng;
return new ListTile(
title: new Text(username),
trailing: new Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new IconButton(
icon: Icon(Icons.info),
onPressed: null
),
new IconButton(
icon: Icon(Icons.directions),
onPressed: null
)
],
)
);
});
}else{
return new Center(
child: new CircularProgressIndicator(),
);
}
})
]);
return new Scaffold(
appBar: new AppBar(title: new Text("Providers")),
body: newbody
);
}
}
This is the output where it shows the expanded ExpansionTile is blank:
And this is the error caught:
I/flutter ( 6305): Vertical viewport was given unbounded height.
I/flutter ( 6305): Viewports expand in the scrolling direction to fill their container.In this case, a vertical
I/flutter ( 6305): viewport was given an unlimited amount of vertical space in which to expand. This situation
I/flutter ( 6305): typically happens when a scrollable widget is nested inside another scrollable widget.
I have tried every possible solution I could find in Stack Overflow by wrapping the ListView.builder with Expanded or Flexible but its not working. Any ideas on how I could go about fixing this issue?
There are two solutions:
Use shrinkWrap property of the ListView
new ListView.builder(
shrinkWrap: true,
itemCount: ...
Reason:
In your case, ListView is inside an ExpansionTile. ExpansionTile will show expand and show how many ever children are there. ListView will expand to the maximum size in scrollDirection as it is placed in ExpansionTile(unbounded constraints widget). As per docs,
If the scroll view does not shrink wrap, then the scroll view will expand
to the maximum allowed size in the [scrollDirection]. If the scroll view
has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
be true.
Use SizedBox for giving fixed height for ListView.
SizedBox(height: 200.0, child: new ListView.builder(...))
The issue:
I have 2 tabs using Default Tabs Controller, like so:
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
drawer: Menu(),
appBar: AppBar(
title: Container(
child: Text('Dashboard'),
),
bottom: TabBar(
tabs: <Widget>[
Container(
padding: EdgeInsets.all(8.0),
child: Text('Deals'),
),
Container(
padding: EdgeInsets.all(8.0),
child: Text('Viewer'),
),
],
),
),
body: TabBarView(
children: <Widget>[
DealList(),
ViewersPage(),
],
),
),
);
}
}
The DealList() is a StatefulWidget which is built like this:
Widget build(BuildContext context) {
return FutureBuilder(
future: this.loadDeals(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
print('Has error: ${snapshot.hasError}');
print('Has data: ${snapshot.hasData}');
print('Snapshot data: ${snapshot.data}');
return snapshot.connectionState == ConnectionState.done
? RefreshIndicator(
onRefresh: showSomething,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: snapshot.data['deals'].length,
itemBuilder: (context, index) {
final Map deal = snapshot.data['deals'][index];
print('A Deal: ${deal}');
return _getDealItem(deal, context);
},
),
)
: Center(
child: CircularProgressIndicator(),
);
},
);
}
}
With the above, here's what happens whenever I switch back to the DealList() tab: It reloads.
Is there a way to prevent re-run of the FutureBuilder when done once? (the plan is for user to use the RefreshIndicator to reload. So changing tabs should not trigger anything, unless explicitly done so by user.)
There are two issues here, the first:
When the TabController switches tabs, it unloads the old widget tree to save memory. If you want to change this behavior, you need to mixin AutomaticKeepAliveClientMixin to your tab widget's state.
class _DealListState extends State<DealList> with AutomaticKeepAliveClientMixin<DealList> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context); // need to call super method.
return /* ... */
}
}
The second issue is in your use of the FutureBuilder -
If you provide a new Future to a FutureBuilder, it can't tell that the results would be the same as the last time, so it has to rebuild. (Remember that Flutter may call your build method up to once a frame).
return FutureBuilder(
future: this.loadDeals(), // Creates a new future on every build invocation.
/* ... */
);
Instead, you want to assign the future to a member on your State class in initState, and then pass this value to the FutureBuilder. The ensures that the future is the same on subsequent rebuilds. If you want to force the State to reload the deals, you can always create a method which reassigns the _loadingDeals member and calls setState.
Future<...> _loadingDeals;
#override
void initState() {
_loadingDeals = loadDeals(); // only create the future once.
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context); // because we use the keep alive mixin.
return new FutureBuilder(future: _loadingDeals, /* ... */);
}
I'm currently developing a reader and using PageView to slide the page of images. How do I make the next page preload so that the user can slide to next page without waiting for the page to load? I don't want to download all the pages first because it will load the server and freezes my app. I just want to download just next one or two pages when the user browsing on current page.
Here is the excerpt of my code.
PageController _controller;
ZoomableImage nextPage;
Widget _loadImage(int index) {
ImageProvider image = new CachedNetworkImageProvider("https://example.com/${bookId}/${index+1}.jpg}");
ZoomableImage zoomed = new ZoomableImage(
image,
placeholder: new Center(
child: CupertinoActivityIndicator(),
),
);
return zoomed;
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Container(
child: PageView.builder(
physics: new AlwaysScrollableScrollPhysics(),
controller: _controller,
itemCount: book.numPages,
itemBuilder: (BuildContext context, int index) {
return index == 0 || index == 1 ? _loadImage(index) : nextPage;
},
onPageChanged: (int index) {
nextPage = _loadImage(index+1);
},
),
),
);
}
Thank you!
Simple! Just set allowImplicitScrolling: true, // in PageView.builder
I ended up using FutureBuilder and CachedNetworkImageProvider from the package cached_network_image to prefetch all the images. Here is my solution:
PageController _controller;
ZoomableImage currPage, nextPage;
Future<List<CachedNetworkImageProvider>> _loadAllImages(Book book) async {
List<CachedNetworkImageProvider> cachedImages = [];
for(int i=0;i<book.numPages;i++) {
var configuration = createLocalImageConfiguration(context);
cachedImages.add(new CachedNetworkImageProvider("https://example.com/${bookId}/${index+1}.jpg}")..resolve(configuration));
}
return cachedImages;
}
FutureBuilder<List<CachedNetworkImageProvider>> _futurePages(Book book) {
return new FutureBuilder(
future: _loadAllImages(book),
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.hasData) {
return new Container(
child: PageView.builder(
physics: new AlwaysScrollableScrollPhysics(),
controller: _controller,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
ImageProvider image = snapshot.data[index];
return new ZoomableImage(
image,
placeholder: new Center(
child: CupertinoActivityIndicator(),
),
);
},
onPageChanged: (int index) {},
),
);
} else if(!snapshot.hasData) return new Center(child: CupertinoActivityIndicator());
},
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: _futurePages(widget.book),
);
}
As people mentioned before the cached_network_image library is a solution, but not perfect for my situation. There are a full page PageView(fit width and height) in my project, when I try previous code my PageView will show a blank page first, then show the image.
I start read PageView source code, finally I find a way to fit my personal requirement. The basic idea is change PageView source code's cacheExtent
This is description about how cacheExtent works:
The viewport has an area before and after the visible area to cache items that are about to become visible when the user scrolls.
Items that fall in this cache area are laid out even though they are not (yet) visible on screen. The cacheExtent describes how many pixels the cache area extends before the leading edge and after the trailing edge of the viewport.
Change flutter's source code directly is a bad idea so I create a new PrelodPageView widget and use it at specific place when I need preload function.
Edit:
I add one more parameter preloadPagesCount for preload multiple pages automatically.
https://pub.dartlang.org/packages/preload_page_view
I'm having a hard time understanding how to best create a scrollable container for the body that holds inside children that by default are scrollable as well.
In this case the grid shouldn't scroll but it's the entire page that should scroll so you are able to see more of the elements inside the grid. So basically the whole content should move vertically with the addition of the ListView moving horizontally (but that works fine already)
I had it working but it was using a bunch of "silver" widget, and I'm hoping there's a better solution that works without using all those extra widgets.
Thanks
Here's my code so far:
class GenresAndMoodsPage extends AbstractPage {
#override
String getTitle() => 'Genres & Moods';
#override
int getPageBottomBarIndex() => BottomBarItems.Browse.index;
static const kListHeight = 150.0;
Widget _buildHorizontalList() => SizedBox(
height: kListHeight,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 20,
itemBuilder: (_, index) =>
CTile(heading: 'Hip Hop', subheading: '623 Beats'),
),
);
Widget _buildGrid() => GridView.count(
crossAxisCount: 2,
crossAxisSpacing: LayoutSpacing.sm,
mainAxisSpacing: LayoutSpacing.sm,
children: List.generate(10, (index) {
return CTile(
padding: false,
heading: 'Kevin Gates Type Beat',
subheading: '623 FOLLOWERS',
width: double.infinity,
);
}),
);
#override
Widget buildBody(_) {
return ListView(children: [
CSectionHeading('Popular Genres & Moods'),
_buildHorizontalList(),
CSectionHeading('All Genres & Moods'),
_buildGrid(),
]);
}
}
The result should be something like this
Create List with Horizontal Scroll direction and called it as a child for Vertical Scroll direction.
body: new ListView.builder(itemBuilder: (context, index){
return new HorizList();
})
class HorizList extends StatelessWidget{
#override
Widget build(BuildContext context) {
return new Container(
height: 100.0,
child: new ListView.builder(itemBuilder: (context, index){
return new Card(child: new Container(width: 80.0,
child: new Text('Hello'),alignment: Alignment.center,));
}, scrollDirection: Axis.horizontal,),
);
}
}
As we want Popular Genres & Moods section also to scroll, we should not using nestedScroll. In above example GridView is nested inside `ListView. Because of which when we scroll, only the GridView will scroll.
I used Only one ListView to achieve the similar screen.
Number of children = (AllGenresAndMoodsCount/2) + 1
divide by 2 as we are having 2 elements per row
+1 for the first element which is horizontal scroll view.
Please refer the code:
import 'package:flutter/material.dart';
void main() {
runApp(new Home());
}
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
var image = new Image.network("http://www.gstatic.com/webp/gallery/1.jpg");
var container = new Container(
child: image,
padding: EdgeInsets.only(left: 5.0, right: 5.0, top: 5.0, bottom: 5.0),
width: 200.0,
height: 200.0,
);
return MaterialApp(
title: "Scroller",
home: Scaffold(
body: Center(
child: new ListView.builder(
itemBuilder: (context, index) {
if (index == 0) { //first row is horizontal scroll
var singleChildScrollView = SingleChildScrollView(
child: Row(
children: <Widget>[
container,
container,
container,
],
),
scrollDirection: Axis.horizontal);
return singleChildScrollView;
} else {
return new Row(
children: <Widget>[container, container],
);
}
},
itemCount: 10, // 9 rows of AllGenresAndMoods + 1 row of PopularGenresAndMoods
)),
),
);
}
}