Flutter | Have a modalbottomsheet and wish to extract it as a widget - android

I am implementing a sort by function which displays sort options through a modal bottom sheet, I am able to do it in my "Home Page" widget. Would like to check if I can extract these codes and sub it as a widget for better organization. I am unable to do as I am concerned with the return values from the radio value.
Appreciate any help given, thanks!!
Here is my code:
child: TextButton.icon( // Button to press sort
onPressed: (() {
showModalBottomSheet( // show modal
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0)),
context: context,
builder: (BuildContext build) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[ // radio values
RadioListTile(
value: 1,
groupValue: selectedRadioTile,
title: Text(
"Case Earliest to Latest"),
onChanged: (val) {
print(
"Radio Tile pressed $val");
setSelectedRadioTile(val!);
print(selectedRadioTile);
Navigator.pop(context);
},
activeColor:
constants.secondaryBlueColour,
),
RadioListTile(
value: 2,
groupValue: selectedRadioTile,
title: Text(
"Case Latest to Earliest "),
onChanged: (val) {
print(
"Radio Tile pressed $val");
setSelectedRadioTile(val!);
print(selectedRadioTile);
Navigator.pop(context);
},
activeColor:
constants.secondaryBlueColour,
)
],
);
});
}),
icon: Icon(
Icons.sort,
size: 28,
color: constants.textGrayColour,
),
label: Text("Sort",
style: TextStyle(
color: constants.textGrayColour,
fontWeight: FontWeight.bold)))),***
Container(
margin: const EdgeInsets.only(top: 5),
width: MediaQuery.of(context).size.width * 0.5,
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 2.0,
color:
constants.categoryButtonBackgroundColour),
bottom: BorderSide(
width: 2.0,
color:
constants.categoryButtonBackgroundColour),
)),
child: TextButton.icon(
onPressed: () {},
icon: Icon(Icons.filter_alt,
size: 28, color: constants.textGrayColour),
label: Text("Filter",
style: TextStyle(
color: constants.textGrayColour,
fontWeight: FontWeight.bold))),
),
],
),
I implemented a SortWidget() but am wondering how I can return the current radio value to my homepage and set the state in the homepage based on the radio value

showModalBottomSheet is a future method, you can use async method for this. and Navigator.pop(context, value); will give you the result. you can also used callback method, seems not needed for your case.
onPressed:()async {
final value = await showModalBottomSheet(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
context: context,
builder: (BuildContext build) {
return MyBottomSheetWidget(selectedRadioTile: selectedRadioTile);
},
);
print("$value");
}
class MyBottomSheetWidget extends StatelessWidget {
// make it statefulWidget if you want to update dialog ui
const MyBottomSheetWidget({
Key? key,
required this.selectedRadioTile,
}) : super(key: key);
final selectedRadioTile;
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// radio values
RadioListTile(
value: 1,
groupValue: selectedRadioTile,
title: Text("Case Earliest to Latest"),
onChanged: (val) {
print("Radio Tile pressed $val");
Navigator.pop(context, val);
},
),
RadioListTile(
value: 2,
groupValue: selectedRadioTile,
title: Text("Case Latest to Earliest "),
onChanged: (val) {
print("Radio Tile pressed $val");
// setSelectedRadioTile(val!);
print(selectedRadioTile);
Navigator.pop(context, val);
},
)
],
);
}
}

showModalBottomSheet is actually a function which can't converted to widget without having some other widget in place. What you can do is, create a function which hold code of this showModalBottomSheet and call that function on button click.
But if you want to create a separate widget then you can create the widget from the internal code of the showModalBottomSheet which starts with return Column.
You need to create a widget which can take two properties which are int variable named selected and a Function named setSelected. Then you can call that widget from inside the showModalBottomSheet and pass two props from your page. This selected will be set as selectedRadioTile & setSelected will be set as setSelectedRadioTile.
Example Code
class BottomFilter extends StatelessWidget {
const BottomFilter(
{Key? key,
required this.selected,
required this.setSelected})
: super(key: key);
final int selected;
final Function setSelected;
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
// radio values
RadioListTile(
value: 1,
groupValue: selected,
title: Text("Case Earliest to Latest"),
onChanged: (val) {
print("Radio Tile pressed $val");
setSelected(val!);
print(selected);
Navigator.pop(context);
},
activeColor: Colors.amber,
),
RadioListTile(
value: 2,
groupValue: selected,
title: Text("Case Latest to Earliest "),
onChanged: (val) {
print("Radio Tile pressed $val");
setSelected(val!);
print(selected);
Navigator.pop(context);
},
activeColor: Colors.amber,
)
],
);
}
}
Call it like this
builder: (BuildContext build) {
return BottomFilter(selected: selectedRadioTile, setSelected: setSelectedRadioTile);
})
Dartpad link to test this code https://dartpad.dev/?id=9359bc416ae48b996085d6f98a977e27

Related

Is there a way to add multiple value for dropdownmenu item?

So basically I want to store another value on dropdown menu item which is the ID and parking name. basically inside the dropdown menu item i want a variable to hold the ID and another variable to hold the parking name. Heres the code
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('Parking')
.where('vacancy' , isEqualTo: 'available' )
.snapshots(),
// .orderBy('parking')
// .snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return Container();
return DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Colors.blue,
Colors.blue,
Colors.blue
//add more colors
]),
borderRadius: BorderRadius.circular(5),
),
child: Padding(
padding: const EdgeInsets.only(left:30, right:30),
child: DropdownButton(
isExpanded: false,
// value: selectedValue,
items: snapshot.data?.docs.map((value) {
return DropdownMenuItem(
value: value.get('parking'),
child: Text('${value.get('parking')}'),
);
}).toList(),
onChanged: (value) {
setState(
() {
dropdownvalue3 = value;
},
);
},
value: dropdownvalue3,
hint: Text('Parking Spot '),
),
),
);
},
),
Is there a way to make this works? or do i have to change to something else? Please I need help

Flutter AnimatedCrossFade messes up widget formatting

I'm trying to create a smooth animation using the AnimatedCrossFade widget but I noticed 2 problems:
Button dimension changes and expands during animation.
The desired outcome is that both buttons match the parent's width and that the color and text changes transition smoothly, but here's what happens.
Without AnimatedCrossFade, Button 1 looks like this:
If I wrap it inside an AnimatedCrossFade widget, Button 1 looks like this:
While the transition is happening, It looks like this:
TextField with InputDecoration stroke becomes thinner
I have multiple TextField widgets that I want to use in the page but some need to be animated in. The problem is that when I put a TextField inside an AnimatedCrossFade widget, the bottom line becomes thinner making the layout look horrible. Here's a comparison of how a TextField looks inside an AnimatedCrossFade (top) and outside of it (bottom).
This is how the layout looks after animation.
But it should look like this.
This code sample should be enough to recreate what I'm trying to explain.
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isExpanded = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView(
padding: EdgeInsets.symmetric(horizontal: 60, vertical: 60),
children: [
ElevatedButton(
child: Text(_isExpanded ? "Collapse" : "Expand"),
onPressed: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
),
AnimatedCrossFade(
crossFadeState: _isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(seconds: 1),
firstChild: TextField(
decoration: InputDecoration(
hintText: "Text",
),
),
secondChild: SizedBox.shrink(),
),
TextField(
decoration: InputDecoration(
hintText: "Text",
),
),
AnimatedCrossFade(
crossFadeState: !_isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(seconds: 1),
firstChild: ElevatedButton(
child: Text("Button 1"),
onPressed: () {},
),
secondChild: ElevatedButton(
child: Text("Button 2"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {},
),
),
],
),
);
}
}
Hope this is what you want?
I think to handle this case, you need to use layoutBuilder of AnimatedCrossFade
if you click on layoutBuilder you can find details.
Updated
wrap with padding to solve TextFiledFormat, for more you can use decoration.
To use max width i used like this
AnimatedCrossFade(
crossFadeState: _isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(seconds: 1),
firstChild: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: TextField(
key: ValueKey("text1"),
decoration: InputDecoration(
hintText: "Text1",
),
),
),
secondChild: SizedBox.shrink(),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: TextField(
key: ValueKey("text2"),
decoration: InputDecoration(
hintText: "Text",
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: AnimatedCrossFade(
crossFadeState: !_isExpanded
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: Duration(seconds: 1),
alignment: Alignment.center,
layoutBuilder:
(topChild, topChildKey, bottomChild, bottomChildKey) {
return topChild;
},
secondChild: ElevatedButton(
child: Text("Button 2"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {},
),
firstChild: ElevatedButton(
child: Text("Button 1"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.red),
),
onPressed: () {},
),
),
),
To get default size of button wrapped with Center inside layoutBuilder
layoutBuilder:
(topChild, topChildKey, bottomChild, bottomChildKey) {
return topChild;
},

How do i make a scrollable list like google tasks ui in Flutter?

I'm stuck with making a scrollable list like Google Task app when you reach end of the list if any task is completed it shown in another list with custom header as you can see here, I'm using sliver
Widget showTaskList() {
final todos = Hive.box('todos');
return ValueListenableBuilder(
valueListenable: Hive.box('todos').listenable(),
builder: (context, todoData, _) {
int dataLen = todos.length;
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
floating: true,
expandedHeight: 100,
flexibleSpace: Container(
padding: EdgeInsets.only(
left: MediaQuery.of(context).size.width / 10,
top: MediaQuery.of(context).size.height / 17),
height: 100,
color: Colors.white,
child: Text(
'My Task',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w600),
),
),
),
SliverList(
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
final todoData = todos.getAt(index);
Map todoJson = jsonDecode(todoData);
final data = Todo.fromJson(todoJson);
return MaterialButton(
padding: EdgeInsets.zero,
onPressed: () {},
child: Container(
color: Colors.white,
child: ListTile(
leading: IconButton(
icon: data.done
? Icon(
Icons.done,
color: Colors.red,
)
: Icon(
Icons.done,
),
onPressed: () {
final todoData = Todo(
details: data.details,
title: data.title,
done: data.done ? false : true);
updataTodo(todoData, index);
}),
title: Text(
data.title,
style: TextStyle(
decoration: data.done
? TextDecoration.lineThrough
: TextDecoration.none),
),
subtitle: Text(data.details),
trailing: IconButton(
icon: Icon(Icons.delete_forever),
onPressed: () {
todos.deleteAt(index);
}),
),
),
);
}, childCount: dataLen),
),
],
);
});
}
ShowTaskList is called on
Scaffold(
body: SafeArea(
child: Column(children: <Widget>[
Expanded(
child: showTaskList()
),
]),
),
I tried OffStageSliver to make an widget disappear if no complete todo is present but that did not work and also can not use any other widget on CustomScrollView because that conflict with viewport because it only accept slivers widget.
Here what i have achieved so far
You can try use ScrollController put it on CustomScrollView and listen to it's controller in initState like this :
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
// If it reach end do something here...
}
});
}
I suggest you make bool variable to show your widget, initialize it with false and then after it reach end of controller call setState and make your variable true, which you can't call setState in initState so you have to make another function to make it work like this:
reachEnd() {
setState(() {
end = true;
});
}
Put that function in initState. And make condition based on your bool variabel in your widget
if(end) _yourWidget()
Just like that. I hope you can understand and hopefully this is working the way you want.

How can I prevent Navigator push from refreshing the screen

I am writing a flutter program where the user should select a value from a DropdownButtonFormField. once the selection is made, the choice should be displayed on the dropdown. I use a push route to get the data from a second screen in which the choice is utilized. My problem is after selecting the option, the page refreshes and therefore doesnt show the selected value on the dropdown.
Below is my code:
I create the Dropdownbuttonformfield in a file called shared.dart so I can call it in multiple files:
class UserDropdownList extends StatefulWidget {
#override
_UserDropdownListState createState() => _UserDropdownListState();
}
class _UserDropdownListState extends State<UserDropdownList> {
String currentUser;
#override
Widget build(BuildContext context) {
final user = Provider.of<List<User>>(context) ?? [];
return DropdownButtonFormField(
isExpanded: true,
decoration: textInputDecoration,
value: currentUser,
hint: Text(
'Incoming Officer',
),
onChanged: (val) {
setState(() => currentUser = val);
var route = MaterialPageRoute(
builder: (BuildContext context) =>
FinalForm(chosenUser: currentUser,)
);
Navigator.of(context).push(route);
},
// onChanged: (val) => setState(() => currentUser = val),
items: user.map((user){
return DropdownMenuItem(
value: user.userId,
child: Text(user.name)
);
}).toList(),
);
}
}
I then call the Custom button in my main page like so
class FinalForm extends StatefulWidget {
//code for importing selected user
final String chosenUser;
FinalForm({Key key, this.chosenUser}) : super (key: key);
#override
_FinalForm createState() => _FinalFormState();
}
class _FinalFormState extends State<FinalForm> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Final Form')
),
body: Form(
child: Center(
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.fromLTRB(5, 5, 5, 5),
children: <Widget>[
SizedBox(height: 20.0),
Align(
child: Text(
'Select Incoming Officer',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: Colors.blueAccent,
),
)
),
SizedBox(height: 20.0),
StreamProvider<List<User>>.value(
value: DatabaseService().users,
child: UserDropdownList(),
),
SizedBox(height: 20.0),
Text("${widget.chosenUser}"),
],),
),
),
);
}
}
Is there a way to keep the selected value on the dropdown or prevent the screen from reloading?
If you are navigating away from the current page / view, it would make sense for the current dropdown selection to be lost. You can pass the current selection as an argument to the push function to redisplay on the new page. Hth

How to expand a card on tap in flutter?

I would like to achieve the material design card behavior on tap. When I tap it, it should expand fullscreen and reveal additional content/new page. How do I achieve it?
https://material.io/design/components/cards.html#behavior
I tried with Navigator.of(context).push() to reveal new page and play with Hero animations to move the card background to new Scaffold, however it seems it is not the way to go since new page is not revealing from the card itself, or I cannot make it to. I am trying to achieve the same behavior as in the material.io that I presented above. Would you please guide me somehow?
Thank you
A while ago I tried replicating that exact page/transition and while I didn't get it to look perfectly like it, I did get fairly close. Keep in mind that this was put together quickly and doesn't really follow best practices or anything.
The important part is the Hero widgets, and especially the tags that go along with them - if they don't match, it won't do it.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
),
body: ListView.builder(
itemBuilder: (context, index) {
return TileItem(num: index);
},
),
),
);
}
}
class TileItem extends StatelessWidget {
final int num;
const TileItem({Key key, this.num}) : super(key: key);
#override
Widget build(BuildContext context) {
return Hero(
tag: "card$num",
child: Card(
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(
Radius.circular(8.0),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
AspectRatio(
aspectRatio: 485.0 / 384.0,
child: Image.network("https://picsum.photos/485/384?image=$num"),
),
Material(
child: ListTile(
title: Text("Item $num"),
subtitle: Text("This is item #$num"),
),
)
],
),
Positioned(
left: 0.0,
top: 0.0,
bottom: 0.0,
right: 0.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
await Future.delayed(Duration(milliseconds: 200));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return new PageItem(num: num);
},
fullscreenDialog: true,
),
);
},
),
),
),
],
),
),
);
}
}
class PageItem extends StatelessWidget {
final int num;
const PageItem({Key key, this.num}) : super(key: key);
#override
Widget build(BuildContext context) {
AppBar appBar = new AppBar(
primary: false,
leading: IconTheme(data: IconThemeData(color: Colors.white), child: CloseButton()),
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.4),
Colors.black.withOpacity(0.1),
],
),
),
),
backgroundColor: Colors.transparent,
);
final MediaQueryData mediaQuery = MediaQuery.of(context);
return Stack(children: <Widget>[
Hero(
tag: "card$num",
child: Material(
child: Column(
children: <Widget>[
AspectRatio(
aspectRatio: 485.0 / 384.0,
child: Image.network("https://picsum.photos/485/384?image=$num"),
),
Material(
child: ListTile(
title: Text("Item $num"),
subtitle: Text("This is item #$num"),
),
),
Expanded(
child: Center(child: Text("Some more content goes here!")),
)
],
),
),
),
Column(
children: <Widget>[
Container(
height: mediaQuery.padding.top,
),
ConstrainedBox(
constraints: BoxConstraints(maxHeight: appBar.preferredSize.height),
child: appBar,
)
],
),
]);
}
}
EDIT: in response to a comment, I'm going to write an explanation of how Hero works (or at least how I think it works =D).
Basically, when a transition between pages is started, the underlying mechanism that performs the transition (part of the Navigator more or less) looks for any 'hero' widgets in the current page and the new page. If a hero is found, its size and position is calculated for each of the pages.
As the transition between the pages is performed, the hero from the new page is moved to an overlay in the same place as the old hero, and then its size and position is animated towards its final size and position in the new page. (Note that you can change if you want with a bit of work - see this blog for more information about that).
This is what the OP was trying to achieve:
When you tap on a Card, its background color expands and becomes a background color of a Scaffold with an Appbar.
The easiest way to do this is to simply put the scaffold itself in the hero. Anything else will obscure the AppBar during the transition, as while it's doing the hero transition it is in an overlay. See the code below. Note that I've added in a class to make the transition happen slower so you can see what's going on, so to see it at normal speed change the part where it pushes a SlowMaterialPageRoute back to a MaterialPageRoute.
That looks something like this:
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple,
),
body: ListView.builder(
itemBuilder: (context, index) {
return TileItem(num: index);
},
),
),
);
}
}
Color colorFromNum(int num) {
var random = Random(num);
var r = random.nextInt(256);
var g = random.nextInt(256);
var b = random.nextInt(256);
return Color.fromARGB(255, r, g, b);
}
class TileItem extends StatelessWidget {
final int num;
const TileItem({Key key, this.num}) : super(key: key);
#override
Widget build(BuildContext context) {
return Hero(
tag: "card$num",
child: Card(
color: colorFromNum(num),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8.0),
),
),
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
AspectRatio(
aspectRatio: 485.0 / 384.0,
child: Image.network("https://picsum.photos/485/384?image=$num"),
),
Material(
type: MaterialType.transparency,
child: ListTile(
title: Text("Item $num"),
subtitle: Text("This is item #$num"),
),
)
],
),
Positioned(
left: 0.0,
top: 0.0,
bottom: 0.0,
right: 0.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
await Future.delayed(Duration(milliseconds: 200));
Navigator.push(
context,
SlowMaterialPageRoute(
builder: (context) {
return new PageItem(num: num);
},
fullscreenDialog: true,
),
);
},
),
),
),
],
),
),
);
}
}
class PageItem extends StatelessWidget {
final int num;
const PageItem({Key key, this.num}) : super(key: key);
#override
Widget build(BuildContext context) {
return Hero(
tag: "card$num",
child: Scaffold(
backgroundColor: colorFromNum(num),
appBar: AppBar(
backgroundColor: Colors.white.withOpacity(0.2),
),
),
);
}
}
class SlowMaterialPageRoute<T> extends MaterialPageRoute<T> {
SlowMaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
}) : super(builder: builder, settings: settings, fullscreenDialog: fullscreenDialog);
#override
Duration get transitionDuration => const Duration(seconds: 3);
}
However, there are situations in which it might not be optimal to have the entire scaffold doing the transition - maybe it has a lot of data, or is designed to fit in a specific amount of space. In that case, an option to make a version of whatever you want to do the hero transition that is essentially a 'fake' - i.e. have a stack with two layers, one which is the hero and has a background colour, scaffold, and whatever else you want to show up during the transition, and another layer on top which completely obscures the bottom layer (i.e. has a background with 100% opacity) that also has an app bar and whatever else you want.
There are probably better ways of doing it than that - for example, you could specify the hero separately using the method mentioned in the blog I linked to.
I achieved this by using the Flutter Hero Animation Widget. In order to do that you will need:
A source page where you start from and that contains the card you want to expand to full screen. Let's call it 'Home'
A destination page that will represent how your card will look like once expanded. Let's call it 'Details'.
(Optional) A data model to store data
Now let's take a look at this example below (You can find the full project code here):
First, let's make an Item class (i will put it in models/item.dart) to store our data. Each item will have its own id, title, subtitle, details and image url :
import 'package:flutter/material.dart';
class Item {
String title, subTitle, details, img;
int id;
Item({this.id, this.title, this.subTitle, this.details, this.img});
}
Now, let's initialize our material app in the main.dart file :
import 'package:flutter/material.dart';
import 'package:expanding_card_animation/home.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
);
}
}
Next, we will make our home page. It'll be a simple stateless widget, and will contain a list of Items that will be displayed in a ListView of Cards. A gesture detector is used to expand the card when tapping it. The expansion is just a navigation to the details page, but with the Hero animation, it looks like it just expanded the Card.
import 'package:flutter/material.dart';
import 'package:expanding_card_animation/details.dart';
import 'package:expanding_card_animation/models/item.dart';
class Home extends StatelessWidget {
List<Item> listItems = [
Item(
id: 1,
title: 'Title 1',
subTitle: 'SubTitle 1',
details: 'Details 1',
img:
'https://d1fmx1rbmqrxrr.cloudfront.net/cnet/i/edit/2019/04/eso1644bsmall.jpg'),
Item(
id: 2,
title: 'Title 2',
subTitle: 'SubTitle 2',
details: 'Details 2',
img:
'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__340.jpg'),
Item(
id: 3,
title: 'Title 3',
subTitle: 'SubTitle 3',
details: 'Details 3',
img: 'https://miro.medium.com/max/1200/1*mk1-6aYaf_Bes1E3Imhc0A.jpeg'),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home screen'),
),
body: Container(
margin: EdgeInsets.fromLTRB(40, 10, 40, 0),
child: ListView.builder(
itemCount: listItems.length,
itemBuilder: (BuildContext c, int index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Details(listItems[index])),
);
},
child: Card(
elevation: 7,
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.grey[400], width: 1.0),
borderRadius: BorderRadius.circular(10.0),
),
margin: EdgeInsets.fromLTRB(0, 0, 0, 20),
child: Column(
children: [
//Wrap the image widget inside a Hero widget
Hero(
//The tag must be unique for each element, so we used an id attribute
//in the item object for that
tag: '${listItems[index].id}',
child: Image.network(
"${listItems[index].img}",
scale: 1.0,
repeat: ImageRepeat.noRepeat,
fit: BoxFit.fill,
height: 250,
),
),
Divider(
height: 10,
),
Text(
listItems[index].title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 20,
),
],
),
),
);
}),
),
);
}
}
Finally, let's make the details page. It's also a simple stateless widget that will take the item's info as an input, and display them on full screen. Note that we wrapped the image widget inside another Hero widget, and make sure that you use the same tags used in the source page(here, we used the id in the passed item for that) :
import 'package:flutter/material.dart';
import 'package:expanding_card_animation/models/item.dart';
class Details extends StatelessWidget {
final Item item;
Details(this.item);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
extendBodyBehindAppBar: true,
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Hero(
//Make sure you have the same id associated to each element in the
//source page's list
tag: '${item.id}',
child: Image.network(
"${item.img}",
scale: 1.0,
repeat: ImageRepeat.noRepeat,
fit: BoxFit.fitWidth,
height: MediaQuery.of(context).size.height / 3,
),
),
SizedBox(
height: 30,
),
ListTile(
title: Text(
item.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
subtitle: Text(item.subTitle),
),
Divider(
height: 20,
thickness: 1,
),
Padding(
padding: EdgeInsets.only(left: 20),
child: Text(
item.details,
style: TextStyle(
fontSize: 25,
),
),
),
],
),
),
),
);
}
}
And that's it, now you can customize it as you wish. Hope i helped.

Categories

Resources