SingleChildScrollView not keeping scroll position - flutter

I have a SingleChildScrollView widget in my app that contains a Column as a child.
The Column has many children and the last one in the very bottom of the scrolled screen is a StreamBuilder that I use to change a child Image
The issue is that when I tap on the image, the logic of the StreamBuilder works and the image is changed, but then the SingleChildScrollView scrolls a bit up so that the image is not visible and forces the user to scroll down again to be able to see the new loaded image.
Widget _buildScroll() => SingleChildScrollView(
child: Container(
width: 2080,
child: Column(
children: <Widget>[
_buildTopBar(),
_buildMainContent(),
SizedBox(height: 30),
Container(
child: Image.asset(
"assets/images/chart_legend.png",
width: 300,
),
),
SizedBox(height: 30),
Image.asset("assets/images/road_map.png", width: 600),
StreamBuilder<int>(
initialData: 1,
stream: _compareStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data == 1) {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: GestureDetector(
child: Image.asset("assets/images/compare1.png"),
onTap: () => _compareSubject.add(2),
),
);
} else if (snapshot.data == 2) {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: GestureDetector(
child: Image.asset("assets/images/compare2.png"),
onTap: () => _compareSubject.add(3),
),
);
} else {
return Padding(
padding: const EdgeInsets.only(right: 10),
child: GestureDetector(
child: Image.asset("assets/images/compare3.png"),
onTap: () => _compareSubject.add(1),
),
);
}
}
return Padding(
padding: const EdgeInsets.only(right: 10),
child: GestureDetector(
child: Image.asset("assets/images/compare1.png"),
onTap: () {},
),
);
}),
],
),
),
);
However, even more weird is that, once I have done tap on all images, they will be showed as expected without scrolling up...meaning that, if there is the second time i tap on a image, the second time the image is replaced the scrolling up in not happening.

The problem here is that the size of your children changes and the SingleChildScrollView cannot handle that.
I think there could be two solutions that might work here:
If you know the sizes of your images before they are loaded, you should enforce it using a SizedBox. This way, the scroll position will stay the same:
SizedBox(
width: 300,
height: 120,
child: StreamBuilder<int>(...),
)
Use ensureVisible that you trigger once the stream builder is updated, which lets you control exactly where the image should be displayed.
You would need to assign a ScrollController to your SingleChildScrollView (controller parameter). Then, you also need a GlobalKey for your StreamBuilder that you want to show (key parameter).
If you have saved instances of the two to variables, you will be able to call the following once your image is loaded:
scrollController.position.ensureVisible(
globalKey.currentContext.findRenderObject(),
alignment: 0.5, // Aligns the image in the middle.
duration: const Duration(milliseconds: 120), // So it does not jump.
);

Related

make photo bigger when click on flutter

how to make the picture zoomable when the photo is clicked, here's the code I made to display the image
Column(
children: [
image11 != null
? Image.file(
image11,
width: 50,
height: 50,
fit: BoxFit.cover,
)
: Text(" Bagian Belakang"),
Container(
margin: const EdgeInsets.fromLTRB(14, 5, 0, 0),
child: RaisedButton(
padding: EdgeInsets.all(10),
onPressed: () {
pickImage11(ImageSource.camera);
},
child: Icon(Icons.camera),
),
),
],
),
Taking inspiration from this answer Hero animation with an AlertDialog
You can use a Hero widget combined with a PageRouteBuilder to imitate a zoom feature:
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
barrierDismissible:true,
pageBuilder: (BuildContext context, _, __) {
return Hero(
tag: "zoom",
child: Image.file(...),
);
}
)
);
EDIT: I found barrier dismissable not working and found the solution here: Flutter SizeTransition and PageRouteBuilder not working
You will have to wrap the Hero widget with a defined width and height to get the optimum zoom level.

How to create a form with add more field in flutter using flutter_form_builder?

Flutter Web
So I have a button called add tags which opens up a modal. The Modal has only one text field and two buttons called add another tag and submit.
Now what I want to do is when the user clicks the add another tag button the app will generate another text field.
I've already seen some videos and read the documentation but since I need to work on a modal and the modal has defined size I'm not sure how to handle issues like
What happens if the user adds a lot of tags. How can I make the modal scrollable?
I'm new to flutter_form_builder so I'm not sure if the modal can handle it or not.
Here's my code:
final _formKey = GlobalKey<FormBuilderState>();
Future buildAddTagsForm(BuildContext context,
{Function()? notifyParent}) async {
return await showDialog(
barrierDismissible: false,
barrierColor: Colors.black.withOpacity(0.5),
context: context,
builder: (context) {
var screen = MediaQuery.of(context).size;
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
content: SingleChildScrollView(
child: Container(
height: screen.height / 2,
width: screen.height > 650 ? 600.00 : screen.height * 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FormBuilder(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: Icon(
Icons.cancel_presentation_rounded,
),
),
],
),
SizedBox(
height: 10,
),
FormBuilderTextField(
name: 'Tag Name',
decoration: InputDecoration(labelText: 'Tag name'),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context),
]),
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MaterialButton(
color: CustomColors.buttonColor,
child: Text(
"Add another tag",
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {},
)
],
),
SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MaterialButton(
color: CustomColors.buttonColor,
child: Text(
"Submit",
style: TextStyle(
color: Colors.white,
),
),
onPressed: () {},
)
],
),
],
),
),
),
),
),
);
},
);
},
);
}
I'm assuming by "modal" we're talking about the AlertDialog here:
return AlertDialog(
content: SingleChildScrollView(
By using SingleChildScrollView as the AlertDialog content:, we can have any size / any number of text fields we like in the dialog. If their number are too many for the height of dialog inside our screen, the content will scroll.
Although, its immediate child Container with height prevents the SingleChildScrollView from doing its magic:
return AlertDialog(
content: SingleChildScrollView(
child: Container(
height: screen.height / 2,
I think the above AlertDialog would not scroll because it would never be big enough to need to scroll. Plus, any fields added that combine to be taller than that specified height (screen.height / 2) will cause an overflow warning and be cutoff visually.
So to answer question #1: "What happens if the user adds a lot of tags. How can I make the modal scrollable?"
using SingleChildScrollView is the right idea
lets swap the position of the Container with height and the SingleChildScrollView and this should allow the dialog to grow & scroll as needed as columns in FormBuilder increase
Your question #2: "I'm new to flutter_form_builder so I'm not sure if the modal can handle it or not."
flutter_form_builder shouldn't affect how SingleChildScrollView works
Example
Here's a partial example of an AlertDialog with scroll view content: that can grow in number.
Widget build(BuildContext context) {
return Container(
height: 300,
child: AlertDialog(
content: SingleChildScrollView(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
),
),
),
actions: [
OutlinedButton(
child: Text('Add Row'),
onPressed: _incrementCounter
)
]
),
);
}
The complete example runnable in DartPard is here. (Add a 6 or 7 rows and then scroll the content.)
Warning
There's a gotcha with using the above AlertDialog inside a sized Container. That Container with height is not enough to constrain the AlertDialog size.
Your showDialog builder: (that pushes the AlertDialog into existence) must provide additional constraints in order for the sized Container to have constraints to size itself within. Without these constraints, the AlertDialog will grow until it matches the device viewport size. I believe this is a quirk with how showDialog is written, since I'm guessing it's a modal layer on top of the current stack of routes. (Someone can correct me if I'm wrong.) It's only constraint is the physical device, but nothing else. By wrapping builder:'s output with a constraining widget (such as Center) the output will be able to size itself.
To see this in action, remove the Center widget from the full example above an re-run it. The dialog will grow to fill the screen when adding rows instead of being at max 300px in height.
child: OutlinedButton(
child: Text('Open Dialog'),
onPressed: () => showDialog(
context: context,
builder: (context) => Center(child: MyDialog())
),
)

How to fix a button at bottom of a single child scrollview with a list

I have a SingleChildScrollView and inside it I have a list with some cards, that you can remove ou add more. I need to fix an add button at the bottom of the screen, when the card list is not scrollable yet, but when the card list increase size and the scrollview is able to scroll now (to see all the content), the button must follow the list and not keep fixed at the bottom anymore.
For now, what I did to solve this, was check the scroll view every time that a card is added ou removed, if I checked that the screen is now scrollable or not scrollable I change some properties of my build widget:
SingleChildScrollView(
controller: _scrollController,
physics: AlwaysScrollableScrollPhysics(),
child: Container(
height: isNotScrollable
? _pageSize - (_appBarSize + _notifySize)
: null,
padding: const EdgeInsets.symmetric(
horizontal: Constraints.paddingNormal),
child: Column(
.....
and after the list render I create the button like this
isNotScrollable
? Expanded(
child: Container(),
)
: Container(),
CVButton(
color: Palette.white,
Basically, my idea is: if the screen is not scrollable yet (the list content fits in the screen size) I will set a height to the container inside scrollview and add a Expanded() widget before the add button (so the button will stay in the bottom of the container), but if the screen is scrollable (the list content not fits inside the screen size) so I remove the container height and the Expanded widget, then the button will follow the list now as normally.
I don't know if this is the better way to deal with that, I want to know if there is some way to do this without this 'dinamic' way that I am doing, only with fixed widgets and not changing the widget according to the state of the scrollview.
An example when the list becomes scrollable and the button will keep at list bottom
Here the list is not scrollable yet but the button must be at the screen bottom and not list bottom
(I dont wanna use bottomNavBar)
Anyone has any idea how I can solve this?
I have a solution for this. check the code bellow. I added some buttons to add or remove cards. The main trick is to use constraints like minHeight.
import 'package:flutter/material.dart';
class BottomButton extends StatefulWidget {
#override
_BottomButtonState createState() => _BottomButtonState();
}
class _BottomButtonState extends State<BottomButton> {
List<Widget> cards = [];
#override
Widget build(BuildContext context) {
var appBar2 = AppBar(
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () {
_addCard();
}),
IconButton(
icon: Icon(Icons.remove),
onPressed: () {
_removeCard();
}),
],
);
return Scaffold(
appBar: appBar2,
body: Container(
height: MediaQuery.of(context).size.height -
(MediaQuery.of(context).padding.top + appBar2.preferredSize.height),
alignment: Alignment.topCenter,
child: ListView(
primary: true,
children: [
Container(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
(MediaQuery.of(context).padding.top +
appBar2.preferredSize.height),
),
alignment: Alignment.topCenter,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
(MediaQuery.of(context).padding.top +
appBar2.preferredSize.height +
50),
),
alignment: Alignment.topCenter,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: cards,
),
),
ElevatedButton(
child: Text('this is a button'),
onPressed: () {},
),
],
),
)
],
),
),
);
}
void _addCard() {
Widget card = Card(
child: Container(
height: 100,
color: Colors.red,
padding: EdgeInsets.all(2),
),
);
setState(() {
cards.add(card);
});
}
void _removeCard() {
setState(() {
cards.removeLast();
});
}
}

Making a 2x2 grid in Flutter

I'm trying to create a 2x2 grid for displaying some info in cards. Disclaimer: I'm totally new to Dart and Flutter, so expect a lot of ignorance on the topic here.
These cards should have a fixed size, have an image, display some text... and be positioned from left to right, from top to bottom.
First, I tried to use the Flex widget, but it seems to only work horizontally or vertically. Therefore, my only solution was to use two Flexes, but only showing the second when the amount of elements is higher than 2 (which would only use one row).
Then, I tried using GridView, but it doesn't work in any possible way. It doesn't matter which example from the Internet I copy and paste to begin testing: they just won't show up in the screen unless they're the only thing that is shown in the app, with no other widget whatsoever. I still don't understand why that happens.
This is my current code:
First widgets in "home_page.dart":
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(padding: EdgeInsets.only(top: 30)),
Text(
'App test',
style: TextStyle(fontSize: 24),
),
EventsList(key: new Key('test')),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
The "EventList" part is a widget that should represent the grid functionality I explained before. This class gets some info from a service (which currently just sends some hardcoded info from a Future), and paints the given widgets ("Card" items, basically) into the EventList view:
class _EventsListState extends State<EventsList> {
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Event>>(
future: new EventsService().getEventsForCoords(),
builder: (context, AsyncSnapshot<List<Event>> snapshot) {
if (snapshot.hasData) {
return Padding(
padding: EdgeInsets.only(left: 20, right: 20),
child: Flex(
direction: Axis.horizontal,
verticalDirection: VerticalDirection.down,
mainAxisAlignment: MainAxisAlignment.center,
children: generateProximityEventCards(snapshot.data),
));
} else {
return CircularProgressIndicator();
}
});
}
List<Card> generateProximityEventCards(List<Event> eventList) {
// Load Events from API
print(eventList);
// Render each card
return eventList.map((Event ev) {
return Card(
child: Padding(
padding: EdgeInsets.only(bottom: 15),
child: Column(
children: <Widget>[
Image(
fit: BoxFit.cover,
image: ev.imageUrl,
height: 100,
width: 150,
),
Padding(
child: Text(ev.name),
padding: EdgeInsets.only(left: 10, right: 10),
),
Padding(
child: Text(ev.address),
padding: EdgeInsets.only(left: 10, right: 10),
),
],
),
));
}).toList();
}
}
This is how it currently looks:
As I said before, I understand that the Flex widget can't really get that 2x2 grid look that I'm looking for, which would be something like this (done with Paint):
So, some questions:
How can I get a grid like that working? Have in mind that I want to have more stuff below that, so it cannot be an "infinite" grid, nor a full window grid.
Is it possible to perform some scrolling to the right in the container of that grid? So in case there are more than 4 elements, I can get to the other ones just scrolling with the finger to the right.
As you can see in the first image, the second example is bigger than the first. How to limit the Card's size?
Thank you a lot for your help!
The reason the gridview was not working is because you need to set the shrinkWrap property of theGridView to true, to make it take up as little space as possible. (by default, scrollable widgets like gridview and listview take up as much vertical space as possible, which gives you an error if you put that inside a column widget)
Try using the scrollable GridView.count widget like this and setting shrinkWrap to true:
...
GridView.count(
primary: false,
padding: /* You can add padding: */ You can add padding const EdgeInsets.all(20),
crossAxisCount: /* This makes it 2x2: */ 2,
shrinkWrap: true,
children: generateProximityEventCards(snapshot.data),
...
Is this what you exactly want?
do let me know so that I can update the code for you
import 'package:flutter/material.dart';
class List extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Text('Inicio', style: TextStyle(color: Colors.black, fontSize: 18.0),),
),
body: GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
children: List.generate(
50,//this is the total number of cards
(index){
return Container(
child: Card(
color: Colors.blue,
),
);
}
),
),
);
}
}

How to make a container 'lighten' on hold / on tap in Flutter?

I am trying to create an app with a scroll view and the objects are clickable like the google news app. Can anyone answer how to animate the container to have a white glow on holding the tile?
Here is the list view builder I have for the app
Container(
padding: EdgeInsets.only(top: 16),
child: ListView.builder(
physics: ClampingScrollPhysics(),
itemCount: article.length,
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemBuilder: (context, index) {
return news_tile(
imageurl: article[index].urlToimage,
news_title: article[index].title,
news_desc: article[index].description,
web_url: article[index].url
);
}),
)
and this is the contents of the tile which the list view builder calls
class news_tile extends StatelessWidget {
String imageurl, news_title, news_desc,web_url;
news_tile({this.imageurl, this.news_title, this.news_desc,this.web_url});
Widget build(BuildContext context) {
return GestureDetector(
onTap: (){
Navigator.push(context, MaterialPageRoute(
builder: (context) => article_view(
web_url: web_url,
)
));
},
child: Container(
margin: EdgeInsets.only(bottom: 16),
child: Column(
children: <Widget>[
ClipRRect(borderRadius: BorderRadius.circular(6), child: Image.network(imageurl)),
SizedBox(
height: 8,
),
Text(news_title, style: TextStyle(fontSize: 17,fontWeight: FontWeight.w600)),
SizedBox(
height: 8,
),
Text(news_desc, style: TextStyle(color: Colors.black54))
],
),
),
);
}
}
You could go with the InkWell Widget. It provides a tapping/holding color effect similar to that. Have a look at the official docs here:
https://api.flutter.dev/flutter/material/InkWell-class.html
Note that you need a Material Widget as an ancestor of your InkWell, but the docs explain that more.
Hope it works for you!
Edit: Sorry, since you are working with a Container, Ink is also important for you:
https://api.flutter.dev/flutter/material/Ink-class.html
Check the docs section "The ink splashes aren't visible!" for why that is.