Related
I have an application like this:
My aim is that when I press the eye icon next to the text "Hello", I want a box to open just below the text and write the German version of "Hello". So it will say "Hallo".
My purpose is to show the meaning of the word.
When I press the eye, I want to show the German of the word. How can I make a white box under the word Hello, that is, the box in which the German language will be written?
Codes:
import 'package:flutter/material.dart';
import 'package:carousel_slider/carousel_slider.dart';
class selamlasmaLearn extends StatelessWidget {
List <wordAndMeaning> wordsList = [wordAndMeaning("Hello", "Hallo"), wordAndMeaning("Go", "Gehen")];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (context) {
final double height = MediaQuery.of(context).size.height;
return CarouselSlider(
options: CarouselOptions(
height: height,
viewportFraction: 1.0,
enlargeCenterPage: false,
),
items: wordsList.map((wordAndMeaning word) {
return Builder(
builder: (BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(color: Colors.amber),
child: Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
word.word,
style: TextStyle(fontSize: 45, color: Colors.white),
),
SizedBox(width: 10,),
Icon(Icons.remove_red_eye_sharp, color: Colors.white, size: 25,), // <<<<<<<<<
],
),
),
);
},
);
}).toList(),
);
}
),
);
}
}
class wordAndMeaning {
String word;
String meaning;
wordAndMeaning(this.word, this.meaning);
}
I keep the word and its German in a list called wordsList.
Thanks for the help in advance.
You can convert the widget to StatefulWidget or use a ValueNotifier to control the preserve/notify the state visibility.
You can use Visibility widget or just if to show and hide German text.
class selamlasmaLearn extends StatefulWidget {
#override
State<selamlasmaLearn> createState() => _selamlasmaLearnState();
}
class _selamlasmaLearnState extends State<selamlasmaLearn> {
bool _showGerman = false;
List<wordAndMeaning> wordsList = [
wordAndMeaning("Hello", "Hallo"),
wordAndMeaning("Go", "Gehen")
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(builder: (context) {
final double height = MediaQuery.of(context).size.height;
return CarouselSlider(
options: CarouselOptions(
height: height,
viewportFraction: 1.0,
enlargeCenterPage: false,
),
items: wordsList.map((wordAndMeaning word) {
return Builder(
builder: (BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(color: Colors.amber),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(word.word,
style:
TextStyle(fontSize: 45, color: Colors.white)),
if (_showGerman) Text(word.meaning), //modify the way you want
],
),
const SizedBox(
width: 10,
),
IconButton(
icon: Icon(Icons.remove_red_eye_sharp),
color: Colors.white,
iconSize: 25,
onPressed: () {
setState(() {
_showGerman = !_showGerman;
});
},
),
],
),
);
},
);
}).toList(),
);
}),
);
}
}
Use the Tooltip widget
I'm emphasizing on the popup part in your question title. When using a Tooltip you ensure that your widgets do not shift position or jump when the Tooltip widget appear, as the example below illustrates.
Example code:
import 'package:flutter/material.dart';
class TooltipExample extends StatelessWidget {
const TooltipExample({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
// Set the tooltip to trigger on a single tap, tapping outside the
// widget will make the tooltip disappear.
triggerMode: TooltipTriggerMode.tap,
// The message shown when the tooltip appears.
message: "Tooltip showing!",
// Consider adjusting this to your needs.
showDuration: const Duration(days: 1),
// The widget that must be clicked to show the tooltip.
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Text("Hello"),
SizedBox(
width: 8,
),
Icon(Icons.visibility),
],
),
),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text("Cover me!"),
)
],
),
);
}
}
// Some code to run the above example, note the theme part that turns the
// tooltip white.
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
// Style the overall design of tooltips in the app in one place,
// or provide in each tooltip individually.
theme: ThemeData(
tooltipTheme: const TooltipThemeData(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
textStyle: TextStyle(
backgroundColor: Colors.white,
color: Colors.black,
),
),
),
home: const Scaffold(
backgroundColor: Colors.amber,
body: TooltipExample(),
),
);
}
}
void main() => runApp(const App());
Here is how it looks:
Note that the Tooltip widget overlays whatever is below it. (instead of pushing it further down - like toggling the visibility of a normal widget in a row or column would have done)
I'm doing a project in Flutter in which I'm getting live bit rate using a API and I'm getting my rate but can't display on my screen its say it null..! code below:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'coin_data.dart';
import 'dart:io' show Platform;
import 'networking.dart';
class PriceScreen extends StatefulWidget {
#override
_PriceScreenState createState() => _PriceScreenState();
}
class _PriceScreenState extends State<PriceScreen> {
BitNetwork bitNetwork = BitNetwork('$BitCoinURL/BTC/USD?apikey=$BitCoinKey');
int bitRate;
void getCurrentBitRate() async {
dynamic bitData = await bitNetwork.getData();
double temp = bitData['rate'];
bitRate = temp.toInt();
print(bitRate);
}
String selectedCurrency = 'USD';`enter code here`
#override
Widget build(BuildContext context) {
getCurrentBitRate();
return Scaffold(
appBar: AppBar(
title: Text('Coin Ticker'),
),`enter code here`
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(18.0, 18.0, 18.0, 0),
child: Card(
color: Colors.lightBlueAccent,
elevation: 5.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 28.0),
child: Text(
'1 BTC = $bitRate USD',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
color: Colors.white,
),
),
),
),
),
Container(
height: 150.0,
alignment: Alignment.center,
padding: EdgeInsets.only(bottom: 30.0),
color: Colors.lightBlue,
child: Platform.isIOS ? iOSPicker() : androidDropdown()),
],
),
);
}
}
answer in console:
I/flutter (14181): 47131
I/flutter (14181): 47131
I/flutter (14181): 47129
output on screen is = 1 BTC = null USD. => ????
You need to wait for currency loading, wrap your widget to FutureBuilder:
Future<int> getCurrentBitRate() async {
dynamic bitData = await bitNetwork.getData();
double temp = bitData['rate'];
return temp.toInt();
}
// build method
child: FutureBuilder<int>(
future: getCurrentBitRate(),
builder (context, snapshot) {
if (snapshot.hasData) {
final bitRate = snapshot.data;
return Column(
// Your column here.
);
}
return CircularProgressIndicator();
}
),
Also, you can find more information about how to work with async features here and read more about FutureBuilder here.
The problem is you're not awaiting getCurrentBitRate() and you are also calling it in your build method. Only UI code should be in the build method. What I recommend you do is override initState() and call it in there (Still can't await it, but it will be called before build);
#override
initState(){
getCurrentBitRate();
super.initState();
}
This will help with your issue, but it's not the best solution. I recommend looking up tutorials on some external state management system, such as BLoC, Provider and/or RxDart. This will make situations like this much easier to debug.
The bitRate value is null because you are calling it in build function & your method getCurrentBitRate() is an async method, which means that the method will wait to get the value but till then your build method would already finish rendering the widgets with bitRate value still null.
There are multiple ways to fix this but the one I would recommend is as follows:
Call your method getCurrentBitRate() in initState method & remove it from the build function as it is the first method that runs in your widget & use setState so that updated value of bitRate is shown in your widget.
class _PriceScreenState extends State<PriceScreen> {
BitNetwork bitNetwork = BitNetwork('$BitCoinURL/BTC/USD?apikey=$BitCoinKey');
int bitRate;
#override
void initState() {
super.initState();
getCurrentBitRate(); // Call it in initState & use setState
}
void getCurrentBitRate() async {
dynamic bitData = await bitNetwork.getData();
double temp = bitData['rate'];
bitRate = temp.toInt();
print(bitRate);
if (mounted) { // <--- mounted property checks whether your widget is still present in the widget tree
setState((){}); // Will update the UI once the value is retrieved
}
}
String selectedCurrency = 'USD';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Coin Ticker'),
),`enter code here`
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(18.0, 18.0, 18.0, 0),
child: Card(
color: Colors.lightBlueAccent,
elevation: 5.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 28.0),
child: Text(
'1 BTC = $bitRate USD',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
color: Colors.white,
),
),
),
),
),
Container(
height: 150.0,
alignment: Alignment.center,
padding: EdgeInsets.only(bottom: 30.0),
color: Colors.lightBlue,
child: Platform.isIOS ? iOSPicker() : androidDropdown()),
],
),
);
}
}
It's null because when build() is called, getCurrentBitRate() didn't complete it's job yet.
For those operations FutureBuilder is one of the best widget. It just needs a future, and a builder to declare what to do after the data received.
// CHANGE TO FUTURE STYLE
Future<Int> getCurrentBitRate() async {
dynamic bitData = await bitNetwork.getData();
double temp = bitData['rate'];
bitRate = temp.toInt();
print(bitRate);
return bitRate;
}
Then change build structure to this
// DECLARE A FUTURE FOR getCurrentBitRate()
Future _future;
initState(){
_future = await getCurrentBitRate();
super.initState();
}
#override
Widget build(BuildContext context) {
// getCurrentBitRate(); REMOVE THIS LINE
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
if(snapshot.hasData){
// YOUR DATA IS READY
double temp = snapshot.data['rate'];
// JUST CONTINUE REST OF ORIGINAL CODE BELOW
return Scaffold(
appBar: AppBar(
title: Text('Coin Ticker'),
),
...
}
}
);
I'm building a download manager. How can listview widget listen to any changes such when download complete, the widget has to refresh automatically and when file is deleted the widget has to refresh automatically.
This is my code the secondtab which is responsible for download action once an action is fired from the first tab. The flutter_file_manager package is used fetch data in the directory. Share your thoughts on this?
import 'dart:io';
import 'package:VideoTube/tabs/share.dart';
import 'package:flutter/material.dart';
import 'package:flutter_file_manager/flutter_file_manager.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart';
class SecondTab extends StatefulWidget {
SecondTab({Key key}) : super(key: key);
#override
_SecondTabState createState() => _SecondTabState();
}
class _SecondTabState extends State<SecondTab> {
var files;
ShareModel share;
_SecondTabState({this.share});
void getFiles() async {
//asyn function to get list of files
//List<StorageInfo> storageInfo = await PathProviderEx.getStorageInfo();
//var root = storageInfo[0].rootDir; //storageInfo[1] for SD card, geting the root directory
var fm = FileManager(root: Directory('/storage/emulated/0/VideoTube/')); //
files = await fm.filesTree(
//set fm.dirsTree() for directory/folder tree list
// excludedPaths: ["/storage/emulated/0/VideoTube/"],
// extensions: ["mp4"] //optional, to filter files, remove to list all,
//remove this if your are grabbing folder list
);
setState(() {}); //update the UI
}
#override
void initState() {
getFiles(); //call getFiles() function on initial state.
super.initState();
}
#override
Widget build(BuildContext context) {
// super.build(context);
return Scaffold(
appBar: AppBar(
title: Text("File/Folder list from SD Card"),
backgroundColor: Colors.redAccent),
body: Column(children: <Widget>[
Flexible(
flex: 3,
child: Consumer<ShareProgress>(builder: (context, value, _) {
return Padding(
padding: EdgeInsets.all(15.0),
child: FittedBox(
child: LinearPercentIndicator(
width: 100.0,
fillColor: Colors.white,
linearGradient: LinearGradient(
colors: [Colors.red, Colors.blue],
),
lineHeight: 6.0,
percent: value.progress / 100,
center: Text(
"${value.progress}%",
style: TextStyle(fontSize: 5.0),
),
trailing: Icon(
Icons.mood,
size: 5,
),
linearStrokeCap: LinearStrokeCap.roundAll,
backgroundColor: Colors.grey,
),
),
);
}
// Container(
// child: Text('downloading.. ${value.progress}'),
// ),
),
),
FutureBuilder(
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
return Flexible(
flex: 9,
child: Container(
child: files == null
? Text("Searching Files")
: ListView.builder(
//if file/folder list is grabbed, then show here
itemCount: files?.length ?? 0,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(files[index].path.split('/').last),
leading: Icon(Icons.video_label),
trailing: Icon(
Icons.delete,
color: Colors.redAccent,
),
));
},
),
),
);
},
),
]));
}
}
This is how the UI looks like:
I see you are using the FutureBuilder, if I have understood the problem correctly, the StreamBuilder may be the right solution, it listens for changes in a stream and rebuilds the child widgets with every change.
new to Dart / Flutter here. I created a gridview and upon tapping an item in the grid, I'd like to pass my JSON data to the next page. However, I'm getting an error that I believe has something to do with not typecasting properly. I'd like the next page to display a title, description and image (the same data that gets displayed in a gridview) but I get an error that says type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Session' in type cast.
Here's my code:
import 'package:flutter/material.dart';
import 'dart:convert';
/*
Build a custom media widget that takes all the attributes from its JSON index.
1. FutureBuilder to build the UI based on data that we wait to receive (Future)
2. For each item in JSON, return a styled grid item containing the metadata
3. Text should be part of the component as well
4. Pass metadata in as arguments to the session information page/widget
5. Use an onTap() + Navigator.push() to select the audio file
6. Also use this https://flutter.dev/docs/cookbook/navigation/hero-animations
*/
class ExplorerGrid extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.only(left: 8.0, right: 8.0),
child: FutureBuilder(
// an asset :bundle is just the resources used by a given application
future:
DefaultAssetBundle.of(context).loadString('audio/media.json'),
builder: (context, snapshot) {
// Loading indicator
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text(snapshot.error); // or whatever
}
var mediaData = json.decode(snapshot.data.toString());
List<Session> mediaDataSession = (mediaData as List<dynamic>).cast<Session>();
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
childAspectRatio: 0.83,
),
itemCount: mediaData.length,
itemBuilder: (BuildContext context, int index) {
// Column should consist of Image, title and description / metadata
return Container(
margin: EdgeInsets.only(top: 12.0),
child: InkWell(
highlightColor: Colors.green[100],
borderRadius: BorderRadius.circular(4.0),
onTap: () => {
// Route and build the metadata page based on the widget clicked
Navigator.push(
context,
MaterialPageRoute(
// session: needs to receive a List with the type of Session e.g. List<Session>
builder: (context) => SessionMetadata(session: mediaDataSession[index])
)
)
},
child: Column(
children: [
FittedBox(
child: ClipRRect(
borderRadius: BorderRadius.circular(30.0),
child: Image(
image: AssetImage(mediaData[index]['image']),
),
),
fit: BoxFit.fill,
),
// Session Title Row
Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
mediaData[index]['name'],
style: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Bryant',
fontSize: 17.0),
),
),
),
],
),
// Description Row
Flexible(
child: Row(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.only(left: 0.0),
child: Text(
mediaData[index]['description'],
style: TextStyle(
fontWeight: FontWeight.normal,
fontFamily: 'Bryant',
fontSize: 14.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 3,
),
),
),
],
),
),
],
),
),
);
});
}),
),
);
}
}
// A way to represent a session and a session metadata page below
class Session {
// Variables for storing metadata
final String title;
final String description;
final String image;
Session(this.title, this.description, this.image);
}
// Create a page that we pass session metadata to
class SessionMetadata extends StatelessWidget {
final Session session;
// Constructor to require this information
SessionMetadata({ Key key, #required this.session}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(session.title),
),
body: Column(
children: [
Image(
image: AssetImage(session.image)
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(session.description)
),
]
)
);
}
}
I'm kinda stuck and any help would be appreciated!
Because you are using fetched json directly. First of all, convert your body into Map:
Map.from(jsonDecode(body));
body is that _InternalLinkedHashMap<String, dynamic> data in your case.
Then add your fromJson factory into your class:
class Session{
...
factory Session.fromJson(Map<String, Object> json) {
return Session(json['title'], json['description'], json['image']);
}
...
And by using decoded Map from your request, put it that factory, you'll get objects.
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.