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)
Related
Please look at this image Home Page
Now when I scroll the ListView it becomes like this -
Home Page
Now I know the reason why this is happening, it is because I used ListView as a parent to this entire view and added ListView.builder() and other widgets as its child.
What I want is to scroll the ListView.builder() without scrolling the entire page.
For this I first tried to use Column as parent but that ended up giving the overflow pixels error.
And then I set the physics: const NeverScrollableScrollPhysics() inside the parent ListView but after that it made my ListView.builder() to show all of its list items.
Here is my Code for Home Screen
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 45, bottom: 24),
children: [
header(),
const SizedBox(height: 36),
const BalanceCard(),
const SizedBox(height: 36),
Recent()
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: bottomNavigationBar(),
),
],
),
); }
Recent List Code
class RecentItems extends StatefulWidget {
final List<Transaction> transactions;
RecentItems({required this.transactions});
#override
State<RecentItems> createState() => _RecentItemsState();
}
class _RecentItemsState extends State<RecentItems> {
#override
Widget build(BuildContext context) {
return SizedBox(
height: 450,
child: Expanded(child: ListView.builder(
itemBuilder: (context, index) {
final item = widget.transactions[index].toString();
return Dismissible(
direction: DismissDirection.endToStart,
key: UniqueKey(),
onDismissed: (direction) {
setState(() {
widget.transactions.removeAt(index);
});
// Then show a snackbar.
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Transaction Deleted')));
},
background: Container(
color: Colors.red,
alignment: AlignmentDirectional.centerEnd,
child: const Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 15.0, 0.0),
child: Icon(
EvaIcons.trash2,
color: Colors.white,
),
),
),
child: Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: ListTile(
leading: CircleAvatar(
radius: 30,
foregroundImage: widget.transactions[index].Image,
backgroundColor: primaryColor,
),
title: Text(
widget.transactions[index].title,
style: const TextStyle(color: secondaryColor),
),
subtitle: Text(
DateFormat.yMMMd().format(widget.transactions[index].date),
),
trailing: Text(
'\$${widget.transactions[index].amount}',
style: const TextStyle(color: secondaryColor),
),
),
),
);
},
itemCount: widget.transactions.length,
),)
);
}
}
Recent Widget -
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Recent Transactions',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: secondaryColor),
),
const SizedBox(height: 5),
RecentItems(transactions: _userTransactions),
],
),
)
The entire screen scrolls because Recent() is included in the same ListView as header() and balanceCard().
Try something like this:
Scaffold(
body: Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.only(top: 45, bottom: 24),
children: [
header(),
const SizedBox(height: 36),
const BalanceCard(),
const SizedBox(height: 36),
],
),
),
// Recent items removed from ListView
Recent(),
Align(
alignment: Alignment.bottomCenter,
child: bottomNavigationBar(),
),
],
),
)
I hope this helps.
By simplifying your code, this is an example of a layout where you have a single Column in the Scaffold. The Column contains some sized, unsized and aligned children.
One child, Recent is a ListView, without explicit height, but wrapped into an Expanded widget. This way it will occupy all the remaining area left by the other children, and it will be scrollable.
(You will run into trouble with this if the children without Recent occupy all the available area.)
Please have a look at this code, you can copy-paste it into a DartPad fiddle:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) => Scaffold(
body: Column(
children: [
const Text('header()'),
const SizedBox(height: 36),
const Text('BalanceCard()'),
const SizedBox(height: 36),
Expanded(child: Recent()),
const Align(
alignment: Alignment.bottomCenter,
child: Text('bottomNavigationBar()'),
),
],
),
);
}
class Recent extends StatelessWidget {
#override
Widget build(BuildContext context) => ListView.builder(
itemCount: 100, itemBuilder: (context, index) => Text('Item $index'));
}
I want to write an ui as below:
the main feature is when click the button ,a new random position Text will be created inside the red box, the problem I faced is when I click the button ,a randon position Text widget will be create,but the old Text was gone, anyone can help me ? following is my code:
class AddNewWidget extends StatefulWidget {
const AddNewWidget({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _AddNewWidgetState();
}
class _AddNewWidgetState extends State<AddNewWidget> {
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("Add Text"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Add Text Below:'),
Stack(
children: [
Container(
width: double.infinity,
height: 600,
decoration: BoxDecoration(
border: Border.all(width: 2.0, color: Colors.red),
),
),
_addText(),
],
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _press,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
_press() {
//print("_press");
setState(() {});
}
_addText() {
return Positioned(
child: Text(
"hello ${Random().nextInt(10)}",
key: ValueKey(Random().nextInt(100)),
),
left: (Random().nextInt(300)).toDouble(),
top: (Random().nextInt(600)).toDouble(),
);`enter code here`
}
}
Create list to hold the generated item and then show the list on stack like
class AddNewWidget extends StatefulWidget {
const AddNewWidget({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _AddNewWidgetState();
}
class _AddNewWidgetState extends State<AddNewWidget> {
List<Widget> items = [];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Add Text"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Add Text Below:'),
Expanded(
child: Stack(
children: [
Container(
height: 600,
decoration: BoxDecoration(
border: Border.all(
width: 2.0,
color: Colors.red,
),
),
),
...items.map((e) => e).toList()
],
),
),
FloatingActionButton(
onPressed: _press,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _press,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
_press() {
items.add(_addText());
setState(() {});
}
Widget _addText() {
return Positioned(
child: Text(
"hello ${Random().nextInt(10)}",
key: ValueKey(Random().nextInt(100)),
),
left: (Random().nextInt(300)).toDouble(),
top: (Random().nextInt(600)).toDouble(),
);
}
}
include two fab, if you need to position any fab, place it within stack with Positioned(bottom:-20,righ:20), play with this value
How can I create an overlapping card effect in a SliverAppBar as shown in the figure:
I tried following this article of Medium but the problem is it uses Stack with
Stack{
...
overflow: Overflow.visible,
...
}
to implement this and in Stack, overflowing part of widgets don't take inputs thus making my TabBar half Dead.
What can I do to avoid this?
Full Code
import 'package:flutter/material.dart';
Future<void> main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MenuList(),
);
}
}
class MenuList extends StatefulWidget {
#override
_MenuListState createState() => _MenuListState();
}
class _MenuListState extends State<MenuList> {
static const double _appBarBottomBtnPosition =
24.0; //change this value to position your button vertically
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: Text(
'Testing',
style: TextStyle(color: Colors.red),
),
),
SliverAppBar(
pinned: true,
expandedHeight: 200.0,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
titlePadding: EdgeInsets.only(bottom: 25),
title: Text('Title'),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(0.0),
child: Transform.translate(
offset: const Offset(0, _appBarBottomBtnPosition),
child: RaisedButton(
shape: StadiumBorder(),
child: Text("Click Here"),
onPressed: () {},
),
),
),
),
SliverPadding(
padding: EdgeInsets.only(top: _appBarBottomBtnPosition),
),
SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(context, index){
Color color = Colors.red.withOpacity(1- (index%10)/10);
return Container(
color: color,
alignment: Alignment.center,
child: Text("Color: $color"),
);
}
),
),
],
),
);
}
}
Currently I am trying to develop a BottomSheet that expands to a specific size. When that size is reached, the user should be able to drag the BottomSheet a little bit up. I have implmented the GestureDetector inside the BottomSheet, so that I am able to detect a vertical drag. The drag function is called, but unfortunately it is not changing the size of the BottomSheet.
This is my code:
//These values are outside of the classes
double initial;
double _kShoppingMenuHeight;
//My custom BottomSheet with rounded corner
Future<T> showRoundedBottomSheet<T> ({
#required BuildContext context,
#required Widget child,
double height
}) {
assert(context != null);
assert(child != null);
return showModalBottomSheet(
context: context,
builder: (BuildContext context){
return new Container(
height: (height != null
? height
: 400.0
),
color: Color(0xFF737373),
child: new Container(
decoration: new BoxDecoration(
color: Colors.white,
borderRadius: new BorderRadius.only(
topLeft: const Radius.circular(5.0),
topRight: const Radius.circular(5.0)
)
),
child: Builder(
builder: (BuildContext context){
return child;
},
)
),
);
}
);
}
//The function that opens the BottomSheet
// this is in another class
return showRoundedBottomSheet(
context: context,
height: _kShoppingMenuHeight,
//Make bottomsheet draggable and fixed at specific point
child: ShoppingMenu(
title: _title("Ihre Listen"),
items: items
)
);
//The stateful widget with the content
return GestureDetector(
onVerticalDragStart: (DragStartDetails details){
initial = details.globalPosition.dy;
},
onVerticalDragUpdate: (DragUpdateDetails details){
setState(() {
_kShoppingMenuHeight = MediaQuery.of(context).size.height / 2 - details.globalPosition.dy;
if(_kShoppingMenuHeight.isNegative) _kShoppingMenuHeight = _kShoppingMenuHeight * (-1);
});
},
onVerticalDragEnd: (DragEndDetails details){
},
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (overscroll){
overscroll.disallowGlow();
},
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: _kShoppingMenuHeight
),
child: ListView(
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 30.0, left: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 10.0),
child: widget.title,
),
Column(
children: widget.items
)
],
),
),
Divider(),
GestureDetector(
child: ListTile(
leading: Icon(Icons.add, color: Colors.black54),
title: Text(
"Neue Liste erstellen"
),
),
onTap: (){
Navigator.pop(context, "neue Liste");
},
),
Divider(),
GestureDetector(
child: ListTile(
leading: Icon(OMIcons.smsFailed, color: Colors.black54),
title: Text(
"Feedback geben"
),
),
onTap: (){
Navigator.pop(context, "feedback");
},
)
],
),
),
),
);
This is a complete example of how you can drag around with your modal bottom sheet.
The idea is to wrap the content of the sheet by a stream builder ,and update the stream when drag occurs. let me know if you need further explanation.
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: MyWidget(),
),
);
}
}
StreamController<double> controller = StreamController.broadcast();
class MyWidget extends StatefulWidget{
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
double position;
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
child: Text('Show Buttom Sheet'),
onPressed: () {
showModalBottomSheet(context: context, builder: (context){
return StreamBuilder(
stream: controller.stream,
builder:(context,snapshot) => GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details){
position = MediaQuery.of(context).size.height- details.globalPosition.dy;
print('position dy = ${position}');
position.isNegative?Navigator.pop(context)
:controller.add(position);
},
behavior: HitTestBehavior.translucent,
child:
Container(
color: Colors.red,
height: snapshot.hasData ? snapshot.data:200.0,
width: double.infinity,
child: Text('Child'),
)),
);
});
}),
),
);
}
}
I think setState() call on the wrong widget.
setState() need to be called on the widget holding the Scaffold because bottom sheet belongs to the scaffold itself .
inherited widget may be the solution
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.