How to create an image gallery on flutter? - flutter

I have a Set<String> variable containing a list of image files. In a Card object, in addition to some Text elements, I want to create a gallery of images like this:
The number of images stored in Set<String> is variable. If possible, I want to show in a corner (top-right) the current number image and the total count of images (e.g. 1/5).
The images will be withdrawn from a webserver, and I don't know if it's more efficient to save them in cache or not. I don't want to use storage folder to save the images.
If possible, I would to withdraw all the images in a single http request, to save time.
Here the variable:
Set<String> _photosList = {
'http://myhost.com/image01.jpg',
'http://myhost.com/image02.bmp',
'http://myhost.com/image03.png',
'http://myhost.com/image04.gif',
};

There are certain things which I want you to explore before actually jumping into the code. These are:
PageView class, this will help you know about how this scrolling thing works like Gallery View. Also, will tell you how the nextPage and previousPage works with the icon clicks
PageController class how the pages works inside the PageView
Stack class, for aligning your arrow on top of your gallery
Let us now jump to the code how it works. Follow the comments to know about each work
// this will keep track of the current page index
int _pageIndex = 0;
// this is your page controller, which controls the page transition
final PageController _controller = new PageController();
Set<String> _photosList = {
'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcRqRwpDKN_zJr1C7pPeWcwOa36BtPm4HeLPgA&usqp=CAU',
'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSgjZ8pw5WLIGMBibVi_g4CMlSE-EOvrLv7Ag&usqp=CAU',
'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQUuMIENOhc1DmruZ6SwLc7JtrR6ZMBRAb3jQ&usqp=CAU',
'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcRzasDrBHWV-84vxbmlX7MTuz3QHqtT8jtTuA&usqp=CAU'
};
Now the UI of the Gallery View.
Please note: This code supports, swipe functionality in the view. If you want to disable it just add this line inside your Pageview.builder()
physics:new NeverScrollableScrollPhysics()
Container(
// use MediaQuery always, it will always adjust the dimensions
// according to different screens
height: MediaQuery.of(context).size.height * 0.3,
width: MediaQuery.of(context).size.width * 0.4,
// here is your stack
child: Stack(
children: [
// PageView.builder is just the part of PageView, read through
// Documentation, and you will get to know
PageView.builder(
controller: _controller,
// here you can remove swipe gesture. UNCOMMENT IT
// physics:new NeverScrollableScrollPhysics()
onPageChanged: (index){
// with each change updating the index of our variable too
setState(() => _pageIndex = index);
},
itemCount: _photosList.length,
// building the view of our gallery
itemBuilder: (BuildContext context, int position){
return Align(
alignment: Alignment.topLeft,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(_photosList.elementAt(position))
)
)
)
);
}
),
// this will come over the images, the icon buttons
Positioned(
left: 0.0,
right: 0.0,
top: MediaQuery.of(context).size.height * 0.12,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: (){
// checking if we are not on pos = 0
// then we can always go back else do nothing
if(_pageIndex != 0)
_controller.previousPage(duration: Duration(milliseconds: 200), curve: Curves.easeIn);
},
icon: Icon(Icons.arrow_back_ios, color: Colors.white, size: 28.0)
),
IconButton(
onPressed: (){
// checking if we are not on pos = photosList.length - 1
// we calculate 0 to length-1
// then we can always go forward else do nothing
if(_pageIndex < _photosList.length-1)
_controller.nextPage(duration: Duration(milliseconds: 200), curve: Curves.easeIn);
},
icon: Icon(Icons.arrow_forward_ios, color: Colors.white, size: 28.0)
),
]
)
)
]
)
)
Result
Pointer: In order to show them numbering on the corner. just make use of two variables which is already there for you in the code
_pageIndex, keeps an update of the page changes
_photosList.length, which gives you the total count of the images
Do something like this, and show it using Container in the same view.
//_pageIndex + 1, cos it starts from 0 not 1, and goes up to 4 not 5
Text('${_pageIndex+1}/$_photosList.length')

Related

Display multiple pages at the same time

I have an app with two features, that have routes such as:
/feature1
/feature1/a
/feature2
/feature2/a
/feature2/a/b
/feature2/c
I can use GoRouter and its ShellRoute to switch between these one at a time using context.goNamed('feature2'), which would replace the entire screen with feature 2 (when tapping a tab in a tab bar for example). Here's a diagram of just the top level routes using tabs:
However, I would like to have an overview style menu which displays multiple destinations at once, so the user can see where they will be going before they go there (for example the preview page tabs in a mobile web browser). Here's a diagram:
and then tapping on either of the two pages would make them full screen:
Pressing the menu button at the bottom would return you to the overview menu page.
One way I have thought about solving this would be to make static preview images out of the routes when the menu button is tapped, and just display the previews. But these won't be live, and I would like a more elegant approach that actually displays the live contents of the route if possible.
Another way I have thought about solving this would be to use a top level GoRouter and then two descendant GoRouters each containing just one branch of the routes. I'm not sure if multiple GoRouters would lead to problems with things like if I wanted to context.go() to another branch.
If the ShellRoute.builder gave me access to all of the child page's widgets, I could display them however I wanted, but it just provides a single child.
I have not worked with 'go_router' or 'ShellRoute.builder', but I like to make custom animated widgets like this for apps. It's also hard to explain how it would work in your app, but here is my take on this.
Try copy pasting this in an empty page. I have written some notes in code comments that might help explain things a little bit. And, this is not perfect but with more polishing according to the needs it could work.
class CustomPageView extends StatefulWidget {
const CustomPageView({Key? key}) : super(key: key);
#override
State<CustomPageView> createState() => _CustomPageViewState();
}
class _CustomPageViewState extends State<CustomPageView> {
// Scroll Controller required to control scroll via code.
// When user taps on the navigation buttons, we will use this controller
// to scroll to the next/previous page.
final ScrollController _scrollController = ScrollController();
// Saving screen width and height to use it for the page size and page offset.
double _screenWidth = 0;
double _screenHeight = 0;
// A bool to toggle between full screen mode and normal mode.
bool _viewFull = false;
#override
void initState() {
super.initState();
// Get the screen width and height.
// This will be used to set the page size and page offset.
// As of now, this only works when page loads, not when orientation changes
// or page is resized. That requires a bit more work.
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_screenWidth = MediaQuery.of(context).size.width;
_screenHeight = MediaQuery.of(context).size.height;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
// 'Column' to wrap the 'Body' and 'BottomNavigationBar'
body: Column(
children: [
// 'Expanded' to take up the remaining space after the 'BottomNavigationBar'
Expanded(
// A 'Container' to wrap the overall 'Body' and aligned to center.
// So when it resizes, it will be centered.
child: Container(
alignment: Alignment.center,
// 'AnimatedContainer' to animate the overall height of the 'Body'
// when user taps on the 'Full Screen' button.
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: _viewFull ? 200 : _screenHeight,
// A 'ListView' to display the pages.
// 'ListView' is used here because we want to scroll horizontally.
// It also enables us to use 'PageView' like functionality, but
// requires a bit more work, to make the pages snap after scrolling.
child: ListView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
children: [
// A 'Container' to display the first page.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
color: Colors.blue,
),
// A 'Container' to display the second page.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
color: Colors.yellow,
),
],
),
),
),
),
// 'BottomNavigationBar' to show the navigation buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 'Feature 1' button
GestureDetector(
onTap: () {
// Scroll to the first page
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.red,
padding: const EdgeInsets.all(12),
child: const Text('Feature 1'),
),
),
// 'Feature 2' button
GestureDetector(
onTap: () {
// Scroll to the second page
_scrollController.animateTo(
_screenWidth,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.green,
padding: const EdgeInsets.all(12),
child: const Text('Feature 2'),
),
),
// 'Full Screen' button
GestureDetector(
onTap: () {
// Toggle between full screen mode and normal mode
setState(() {
_viewFull = !_viewFull;
});
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.purple,
padding: const EdgeInsets.all(12),
child: const Text('View Full'),
),
),
],
),
],
),
);
}
}

How to delete image path from list of currently selected photo?

I need a button to delete the currently viewed image. The image paths are stored in a List<String>.
The images appear via CarouselSlider from ImagePicker.
I've tried to see if I can delete the image via a hardcoded index with .removeAt() and that works but again, can't figure how to get current image
What is right way to grab the current index of the on screen image? I've tried with single images and it works find but getting the current index of the currently viewed photo is where I'm running into trouble.
This is how I am getting my Images:
List<String> pickedImageList = [];
final imagePicker = ImagePicker();
Future pickImage() async {
final userPickedImages = await imagePicker.pickMultiImage();
for (var image in userPickedImages!) {
pickedImageList.add(image.path);
}
setState(() {});
}
This is how I am showing the images. Button to delete currently selected photo is at the bottom with generic function:
Column(
children: [
ElevatedButton(
onPressed: pickImage,
child: const Text('Add another image')),
CarouselSlider(
options: CarouselOptions(
// height: 200,
viewportFraction: .9,
enlargeCenterPage: true,
),
items: pickedImageList
.map(
(item) => SizedBox(
width: 200,
height: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
item,
height: 200,
width: 200,
)),
),
// color: Colors.green,
)
.toList(),
),
ElevatedButton(
onPressed: () {
setState(() {
removeImage();
});
},
child: const Text('Remove'))
],
),
All of the delete buttons that people have solved on SO all involve single images which is not the problem.
I thought maybe a GestureDetector counting up and down with left and right swipes, resetting the counter when it reaches the length of the list. Negatives would be multiplied by it self to get current index usable.

Is there a parameter for default height for widgets in Flutter?

I am trying to implement my own "expandable panel" in Flutter, basically there are questions and their answers, all having different length naturally. What i want to do is to display the question in a container first, and when user clicks the container i want to expand the second container's height, which has the answer text, from 0 to the height value it's answer requires
double _theHeight = 0;
Column(
children: [
Container(
child:Inkwell(
onTap: () {setState({_theHeight = ????})},
child: Text(theQuestion)
)
),
Container(
height = _theHeight,
child: Text(theAnswer),
),
]
)
In such example, i tried to give _theHeight variable a constant value like 200 but as different answers have different lengths, this causes an overflow or too much unnecessary place.
Is there a default value for height such that the parent widget will only cover the space it's child requires? So can i type something like:
_theHeight = Container.default.height;
Thank you for your time.
You could simply wrap the answer inside the container.
No need to give specific height. You can just check if the widget is expanded or not
Example:
bool isExpanded = false;
Column(
children: [
Container(
child:Inkwell(
onTap: () {setState({isExpanded = !isExpanded})},
child: Text(theQuestion)
)
),
if(isExpanded)
Container(
child: Text(theAnswer),
),
]
)
For animated container, you could use AnimatedSize something like
AnimatedSize(
duration: const Duration(seconds: 1),
child: isExpanded ? Container() : const Text(theAnswer),
),

Maintaining state in Listview.Builder() of Stateful widget (a progress bar) problem

I am creating a listview using listview.builder. Each view is a card with a button on it and a progress bar (See screenshot)
Each card is a stateful widget. When you press the button, the progressbar starts a 5 second timer. When the timer is over, the object is removed from the LinkedHashMap which makes is dissapear from the screen too. This works, if only one button is pressed in the displayed list. If one button is pressed and after 2 seconds second or/and third button(s) are pressed, the animation of progress bar can be seen on all three, but as soon as a first one dissapears from the screen and the second and subsequest are re-drawn, they loose there state and they appear as if their buttons were never pressed.
What I am trying to do is, keep their states independent and keep their animations independent. When one card dissapears, the others gets shifted up/down accordingly but the progress animation to continue as it is as currently they seem to redraw themselve in unpressed/original state.
My code for my card is as follows:
class _DeliveryCardsViewState extends State<DeliveryCardsView> {
double percentage = 1.0;
bool countdownStarted = false;
String buttonText = "CLAIM ORDER";
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
child: Container(
padding: EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10.0, right: 10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text('#'+widget.delivery.uID),
GestureDetector(
onTap: () async {
if (!countdownStarted) {
countdownStarted = true;
setState(() {
buttonText = 'UNDO';
});
int j = 100;
while (j > 0) {
await Future.delayed(Duration(milliseconds: 50)); // Duration in seconds (50/10) = 5 seconds
j -= 1;
setState(() {
if (countdownStarted){
percentage = (j / 100).toDouble();
} else {
percentage = 1.0;
}
});
if (!countdownStarted){
setState(() {
buttonText = 'CLAIM ORDER';
});
break;
}
}
//Try to claim it now
if (countdownStarted) tryClaimOnFirebase();
} else {
countdownStarted = false;
}
},
child: Container(
padding: EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
Text(buttonText,style: TextStyle(color: Colors.white),),
SizedBox(height: 5.0,),
LinearPercentIndicator(
width: 100.0,
lineHeight: 7.0,
percent: percentage,
backgroundColor: Colors.green,
progressColor: Colors.red,
),
],
),
decoration: BoxDecoration(
color: mainGrey,
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
)
),
],
),
],
),
),
);
}
And my Listview.builder code is as follows:
return Container(color: Colors.grey[350],
child: ListView.builder(
key: UniqueKey(),
itemCount: deliveriesAvailable.length,
controller: _controller,
scrollDirection: Axis.vertical,
reverse: false,
shrinkWrap: true,
itemBuilder: (context,position){
return GestureDetector(
child: DeliveryCardsView(delivery: deliveriesAvailable.values.elementAt(position),),
onTap: (){},
);
}),
);
Error recording:
Working now.
Alright so, I think I underdtand what was happening. The entire stateful widget was getting disposed and rebuilt and as a result the 'logic' 'inside' the stateful widget was also resetting. What I did was the following:
1) Added a couple of extra variables in the 'Object'
2) When the view gets built again, it checks those variables and then 'resumes' the state it was last disposed in, and that included the the value of the progress bar counter.
Obvously for this to happen, the entire logic was taken out from onTap() and added to a new async method.
Simplessss :)

OnPressed not working inside Stack widget after Transforming a widget

In the given code,onPressed on the raised button works and translate FlatButton to the top. But onPressed on FlatButton is not working
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Transform(
transform: Matrix4.translationValues(
0.0,
_translateButton.value,
0.0,
),
child: FlatButton(
onPressed: () {
print('tapped Flat button');
},
child: Text('upper'),
),
),
RaisedButton(
onPressed: () {
animate();
print('tapped Raised button');
},
child: Text('lower'))
],
);
}
Here _translatebutton value changes from 0 to -60 when animate() is called
_animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_translateButton = Tween<double>(
begin: 0,
end: -60,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
0.0,
0.75,
curve: _curve,
),
));
Wrap the Transform widget in a SizedBox (expanded or from size, depending on your requirement.
I came across this problem last week and in my case, the composition was like this:
Stack(
children: [
Widget0,
Widget1,
Opacity(
opacity: sth,
child: Transform.translate(
offset: Offset(sth),
child: Transform.rotate(
angle:sth,
child: FlatButton(
onPressed: (){sth},
child: Text("sth"),
),
),
),
),
],
)
Based on the suggestion of rahulthakur319 on the issue number
27587
I wrapped my Transform.translate composition inside a new Stack and I wrapped the stack inside a Container. Remember that the new Container should have enough width and height to show its child. I personally used MediaQuery.of(context).size.
it's working even during the complex series of animations.
The final code:
Stack(
children: [
Widget0,
Widget1,
Opacity(
opacity: sth,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
Transform.translate(
offset: Offset(sth),
child: Transform.rotate(
angle: sth,
child: FlatButton(
onPressed: (){sth},
child: Text("sth"),
),
),
),
],
),
),
),
],
)
If your translation moves the button outside the area of the stack, the button no longer reacts to clicks. A simple way to test this is to wrap your Stack widget in a container and color it blue (or anything obvious), and if your button is moved outside the blue area, you know it's losing its clickability because it's outside of the Stack.
If this indeed is the issue, the solution is to keep the Stack inside the container, and then either set the container dimensions such that the button still stays within the border after translation, or reposition widgets relative to the container such that the translation stays within the border.
If someone is still trying to solve this issue, I solved it by wrapping the widget with IgnorePointer widget on which I don't want the pointer to reach.
reference from here
The answer I got was that in a View if an element is translated then the animation works correct but the click property is altered in someway that we can't use it after translating the element
I had this same issue my Switch widget was not working in the Stack.
The solution i found was to include it in SizeBox or Container with fixed width and height.
if your switch widget is in Row try to add Constraints on Row with SizeBox rather than adding it in every widget.