I am trying to build a multiple choice Row from a list of Container widgets (wrapped in GestureDetector for onTap). When the user clicks on one of the Container widgets in the Row, that Container widget changes color (to blue) to reflect the user's selection. If the user then taps a different Container widget in the Row, THAT widget changes color to reflect the user's selection, and all others are reset to the initial color (white). Additionally, the user's choice (index?) must be retrievable.
All I have been able to do so far is create a Row of Container widgets that change color on tap, but function independently of one another. That is to say that the user may click on multiple selections, which is bad. I need some advice on how to break through to next level of functionality, where only one container may be selected and the selected value is passed-out.
Cheers,
T
//choice button
class ChoiceButton extends StatefulWidget {
final String label;
final bool isPressed;
const ChoiceButton({
Key key,
this.isPressed = false,
this.label,
}) : super(key: key);
#override
_ChoiceButtonState createState() => _ChoiceButtonState();
}
class _ChoiceButtonState extends State<ChoiceButton> {
bool _isPressed;
#override
void initState() {
super.initState();
_isPressed = widget.isPressed;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_isPressed = !_isPressed;
});
},
child: Container(
height: 80,
width: 80,
decoration: BoxDecoration(
color: _isPressed ? Colors.blue : Colors.transparent,
border: Border.all(
color: Colors.blue,
width: 80 * 0.05,
),
),
child: Center(
child: Text(
widget.label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _isPressed ? Colors.white : Colors.blue,
),
textAlign: TextAlign.center,
),
),
),
);
}
}
//choice row
class ChoiceRow extends StatefulWidget {
#override
_ChoiceRowState createState() => _ChoiceRowState();
}
class _ChoiceRowState extends State<ChoiceRow> {
bool isPressed = false;
String classChoice = '';
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: \[
SizedBox(width: 30),
ChoiceButton(
isPressed: isPressed,
label: 'A',
),
SizedBox(width: 10),
ChoiceButton(
isPressed: isPressed,
label: 'B',
),
SizedBox(width: 10),
ChoiceButton(
isPressed: isPressed,
label: 'C',
),
\],
);
}
}
I am providing two answers for this .... Use whichever one you prefer.
The easy one :
So Flutter provides a widget named ToggleButton which will satisfy all the needs mentioned above. Follow this documentation for more information about this widget.
You can add your custom ChoiceButton design in the widget list of the toggle button and with some tweak you will also be able to achieve your ChioceRow design too.
Now if you are someone who like to make everything from scratch (like me :P) then I have made some changes in the code you provided above which will satisfy all your needs. Below is the edited code.
class ChoiceRow extends StatefulWidget {
#override
_ChoiceRowState createState() => _ChoiceRowState();
}
class _ChoiceRowState extends State<ChoiceRow> {
List<bool> isPressedList = [false,false,false];
String classChoice = '';
#override
Widget build(BuildContext context) {
print("Status L $isPressedList");
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(width: 30),
GestureDetector(
onTap: (){
print("Hello");
setState(() {
isPressedList[0] = true;
isPressedList[1] = false;
isPressedList[2] = false;
});
},
child: ChoiceButton(
isPressed: isPressedList[0],
label: 'A',
),
),
SizedBox(width: 10),
GestureDetector(
onTap: (){
setState(() {
isPressedList[0] = false;
isPressedList[1] = true;
isPressedList[2] = false;
});
},
child: ChoiceButton(
isPressed: isPressedList[1],
label: 'B',
),
),
SizedBox(width: 10),
GestureDetector(
onTap: (){
setState(() {
isPressedList[0] = false;
isPressedList[1] = false;
isPressedList[2] = true;
});
},
child: ChoiceButton(
isPressed: isPressedList[2],
label: 'C',
),
),
],
);
}
}
class ChoiceButton extends StatelessWidget {
final String label;
final bool isPressed;
ChoiceButton({this.label,this.isPressed});
#override
Widget build(BuildContext context) {
return Container(
height: 80,
width: 80,
decoration: BoxDecoration(
color: isPressed ? Colors.blue : Colors.transparent,
border: Border.all(
color: Colors.blue,
width: 80 * 0.05,
),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isPressed ? Colors.white : Colors.blue,
),
textAlign: TextAlign.center,
),
),
);
}
}
CHANGES :
I made a list (onPressedList) which keeps track of the current status of the toggle buttons (Like which is on and which others are off).
I moved the GestureDetector wrap on the button in the ChoiceRow class. This is because it will be difficult to pass the onTap result from ChoiceButton to ChoiceRow. (Now if you want the gesture detector in the Button class itself then you can make the list global or a static value in a completely different so that it can be properly accessed)
I made the ChoiceButton class Stateless as there is no need to keep it Stateful now.
The two things I did were adding a list that tracks the current status of the toggle buttons and when one toggle button is active all the others will be deactivated.
Now it is working as you mentioned above and you will also be able to keep track of the current status of all the buttons through "isPressedList".
GLHF :)
Related
I can't figure out how to attach a certain text to a random state of ElevatedButton. When I press the button, a random picture in a button should appear with its certain text under it. A button with a picture 1 with a text 1. A picture 2 with a text 2. There are 4 pictures with a certain text that should be shown under a picture.
A picture of ElevatedButton 'N'.
A text 'NICHTS. You get nothing'.
'H' — 'HALB. You get half of it'.
and so on.
Thank you!
class TrendlState extends StatefulWidget {
const TrendlState({Key? key}) : super(key: key);
#override
State<TrendlState> createState() => _TrendlStateState();
}
class _TrendlStateState extends State<TrendlState> {
int TrendlLetter = Random().nextInt(4) + 1;
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Press it to roll',
style: TextStyle(
fontSize: 30.0,
color: Colors.white,
fontFamily: 'Montserrat'),
),
SizedBox(
height: 20.0,
),
Center(
child: ElevatedButton(
child: Image.asset('images/frame$TrendlLetter.png'),
style: ElevatedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(60.0),
),
primary: Colors.black45,
fixedSize: const Size(
250,
250,
),
),
onPressed: () {
setState((){
TrendlLetter = Random().nextInt(4) + 1;
});
print('THE BUTTON WORKS');
}),
),
I've been banging my head against this for several days now, so I really hope somebody can shed some light.
I need a custom button that looks a certain way and runs simple animations on itself (really simple, like cross-fading to a different color and then back upon being pressed). I have written such a button, and it suits me perfectly in terms of looks and behavior.
But! In order for the animations to work, I had to derive my button from the StatefulWidget class. The problem is this: no matter what I do, I can't get the page to rebuild the button anew, with updated parameters.
I have implemented a simple on/off switch with two buttons to show what I mean. On this page I have two buttons: "Drop anchor" and "Retract anchor". I want only one button to be enabled at any given time. Pressing one of the buttons should disable it and enable the other one. But that doesn't happen!
I have put some text on the screen to illustrate that the page does indeed update on setState(). The text is governed by the same variable the buttons are. But for some reason Flutter updates the text, but not the buttons.
I've tried just about every solution I could find here.
Here's what I've tried:
I've added keys to my buttons and button's child. It helped with getting the AnimatedSwitch to do what I needed, but not with UI rebuilds.
I've tried dispatching notifications and calling setState() upon receiving them. No effect.
I've put a floating button to manually call setState() on the page to make sure it's called from the page widget, and not inside the button state. No effect.
I've tried wrapping the whole app in an AppBuilder, as was suggested on one of the threads here, and rebuilding the whole app on the press of the buttons, which works even for changing the Theme of the app. But it doesn't update my stateful buttons... Duh!
I've implemented the same example using ordinary stateless buttons, and it works exactly as I expect it to. So I'm about 75% sure the problem is that my custom button has its own state.
What else can I try?
Thank you for reading!
My example page code:
class _MyHomePageState extends State<MyHomePage> {
bool anchorIsDown = false;
void anchorUp() {
anchorIsDown = false;
setState(() {});
}
void anchorDown() {
anchorIsDown = true;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
flex: 3,
child: Container(
child: Center(
child: anchorIsDown ? Text('Anchor is down') : Text('Anchor is up'),
)
),
),
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StatefulButton(
keyString: 'anchor_drop',
opacity: 0.2,
visible: true,
onPressedColor: Colors.greenAccent,
onPressed: !anchorIsDown ? anchorDown : null,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Drop anchor')
],
),
),
Expanded(child: SizedBox(width: 0, height: 0)),
StatefulButton(
keyString: 'anchor_retract',
opacity: 0.2,
visible: true,
onPressedColor: Colors.greenAccent,
onPressed: anchorIsDown ? anchorUp : null,
child: Text('Retract anchor'),
),
],
),
),
),
Flexible(
flex: 3,
child: Container(),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () { setState(() {}); },
),
);
}
}
My stateful button class:
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math';
class StatefulButton extends StatefulWidget {
late final String keyString;
late final bool visible;
late final double? opacity;
late final VoidCallback? onPressed;
late final Color? onPressedColor;
late final Widget child;
StatefulButton(
{required this.keyString,
this.visible = true,
this.opacity = 0.05,
this.onPressedColor,
this.onPressed,
required this.child})
: super(key: Key(keyString));
#override
State createState() => StatefulButtonState();
}
class StatefulButtonState extends State<StatefulButton> {
late final bool visible;
late double? opacity;
VoidCallback? onPressed;
late final Color? onPressedColor;
late final Widget child;
bool isAnimating = false;
#override
void initState() {
visible = widget.visible;
opacity = widget.opacity;
onPressed = widget.onPressed;
onPressedColor = widget.onPressedColor;
child = widget.child;
isAnimating = false;
super.initState();
}
void onPressedWrapper() async {
isAnimating = true;
setState(() {});
if (onPressed != null) onPressed!();
await Future.delayed(Duration(milliseconds: 200));
isAnimating = false;
setState(() {});
}
Widget buildChildContainer() {
if (!isAnimating)
return Container(
key: Key(widget.keyString + '_normalstate'),
color: Theme.of(context).buttonColor,
child: Center(
child: child,
),
);
return Container(
key: Key(widget.keyString + '_pressedstate'),
decoration: BoxDecoration(
color: onPressedColor,
borderRadius: BorderRadius.all(Radius.circular(3.0))
),
child: Center(
child: child,
),
);
}
//If the button is enabled, it should provide tap feedback.
//This function will build such a button using AnimatedCrossFade
Widget buildEnabledButton() {
return OutlinedButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.all(1)),
side: MaterialStateProperty.all(BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
style: BorderStyle.solid)),
enableFeedback: false,
minimumSize: MaterialStateProperty.all(Size(150, 50)),
elevation: MaterialStateProperty.all(4.0),
shadowColor: MaterialStateProperty.all(Theme.of(context).shadowColor),
overlayColor: MaterialStateProperty.all(Colors.transparent),
backgroundColor:
MaterialStateProperty.all(Theme.of(context).buttonColor)),
onPressed: onPressed == null ? onPressed : onPressedWrapper,
child: AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: buildChildContainer(),
switchInCurve: Curves.bounceOut,
switchOutCurve: Curves.easeOut,
),
);
}
Widget buildDisabledButton() {
if (!visible) opacity = 0;
return Stack(
children: [
OutlinedButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.all(1)),
side: MaterialStateProperty.all(BorderSide(
color: Theme.of(context).dividerColor.withOpacity(sqrt(opacity!)),
width: 1,
style: visible ? BorderStyle.solid : BorderStyle.none)),
enableFeedback: false,
minimumSize: MaterialStateProperty.all(Size(1000, 1000)),
elevation: MaterialStateProperty.all(4.0 * opacity!),
shadowColor: visible
? MaterialStateProperty.all(
Theme.of(context).shadowColor.withOpacity(opacity!))
: MaterialStateProperty.all(Colors.transparent),
overlayColor: MaterialStateProperty.all(Colors.transparent),
backgroundColor: visible
? MaterialStateProperty.all(Theme.of(context)
.buttonColor
.withOpacity(sqrt(opacity!) / 2))
: MaterialStateProperty.all(Colors.transparent)),
onPressed: visible ? onPressed : null,
child: buildChildContainer()),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(3)),
color: Theme.of(context).canvasColor.withOpacity(1 - opacity!),
),
child: SizedBox(
child: Center(),
))
],
);
}
#override
Widget build(BuildContext context) {
return Expanded(
child: ((onPressed != null) && visible)
? buildEnabledButton()
: buildDisabledButton());
}
}
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
this is my second thread to this Topic, this time I will ad a picture and the full code.
The nav bar is working but I want an indicator on which is pressed (the text under the icon is already scaling up a bit but thats not enough)
I want hat the segment which is pressed has a more light background then the others.
How is that possible? I'm really new to Flutter and currently this is the only programming language which is complete confusing for me. screenshot
In the code, I just have included an icon font (font awesome) and 3 pages which the nav bar directs to. (neu, beliebt, profil)
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'Neu.dart';
import 'Beliebt.dart';
import 'Profil.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
int _selectedTab = 0;
final _pageOptions = [
NeuPage(),
BeliebtPage(),
ProfilPage(),
];
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.deepOrangeAccent,
primaryTextTheme: TextTheme(
title: TextStyle(color: Colors.white),
)),
home: Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/logo_straight.png',
fit: BoxFit.contain,
height: 32,
),
],
),
),
body: _pageOptions[_selectedTab],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedTab,
backgroundColor: Colors.deepOrangeAccent,
onTap: (int index) {
setState(() {
_selectedTab = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(FontAwesomeIcons.quoteRight, color: Colors.white),
title: Text('Neu', style: TextStyle(color: Colors.white),
)
),
BottomNavigationBarItem(
icon: Icon(Icons.whatshot, color: Colors.white),
title: Text('Beliebt', style: TextStyle(color: Colors.white),
)
),
BottomNavigationBarItem(
icon: Icon(Icons.account_circle, color: Colors.white),
title: Text('Profil', style: TextStyle(color: Colors.white),
)
),
],
),
),
);
}
}
Possible Solutions
Build your own Widget using row and columns
Flutter is an open soruse project , edit the original Widget and submit it or use it your self
here original Widget code
Paint over the Widget
Use a Container , Stack and the bottomNavigationBar size to move the Container
this is what i used here
Screen Record
Step 1
inside MyAppState add GlobalKey variable
GlobalKey _bottomNavigationBarKey = GlobalKey();
Step 2
assign the GlobalKey to the BottomNavigationBar
BottomNavigationBar(
key: _bottomNavigationBarKey,
...)
Step 3
inside MyAppState add _bottomNavigationBarSize variable
Size _bottomNavigationBarSize = Size(0, 0);
Step 4
inside MyAppState add _getbottomNavigationBarSize method to ask the framework for the bottomNavigationBar Size
_getbottomNavigationBarSize() {
final RenderBox bottomNavigationBarRenderBox =
_bottomNavigationBarKey.currentContext.findRenderObject();
final bottomNavigationBarSize = bottomNavigationBarRenderBox.size;
setState(() {
_bottomNavigationBarSize = bottomNavigationBarSize;
});
}
Step 5
inside initState at addPostFrameCallback call _getbottomNavigationBarSize method to tell the framework to calculate the size after the frame drawing is done
#override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => _getbottomNavigationBarSize());
}
Step 6
warp the bottomNavigationBar Widget in a Stack Widget
bottomNavigationBar:
Stack
(
children: <Widget>
[
BottomNavigationBar(.....),
],
)
Step 7
add an Positioned Widget after BottomNavigationBar
bottomNavigationBar:
Stack
(
children: <Widget>
[
BottomNavigationBar(.....),
Positioned(.....),
],
)
Step 8
set the Positioned Widget left property
item width = *bottomNavigationBar width dividend by the pages count
1st item offset = 0 * item width = 0
2st item end = 1 * item width = item width
2st item end = 2 *item width = 2 item width
container offset = item width multiplied by the _selectedTab index
Positioned(
left: (_bottomNavigationBarSize.width / _pageOptions.length) * _selectedTab,
),
Step 9
add an Positioned Widget after BottomNavigationBar
Positioned
(
...,
child: Container(.... ),
)
Step 10
in the Container set the height property to bottomNavigationBar height
Container(
height: _bottomNavigationBarSize.height,
....),
Step 10
in the Container set the width property to bottomNavigationBar width divided by the pages count
child: Container(
width: _bottomNavigationBarSize.width / _pageOptions.length,
....),
Step 11
in the Container set the color property to Black with 26% opacity.
child: Container(
....,
color: Colors.black26)
Full Code
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
GlobalKey _bottomNavigationBarKey = GlobalKey();
Size _bottomNavigationBarSize = Size(0, 0);
_getbottomNavigationBarSize() {
final RenderBox bottomNavigationBarRenderBox =
_bottomNavigationBarKey.currentContext.findRenderObject();
final bottomNavigationBarSize = bottomNavigationBarRenderBox.size;
setState(() {
_bottomNavigationBarSize = bottomNavigationBarSize;
});
}
#override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => _getbottomNavigationBarSize());
}
int _selectedTab = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
final _pageOptions = [
Text(
'Index 0: Home',
style: optionStyle,
),
Text(
'Index 1: Business',
style: optionStyle,
),
Text(
'Index 2: School',
style: optionStyle,
),
];
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.deepOrangeAccent,
primaryTextTheme: TextTheme(
title: TextStyle(color: Colors.white),
)),
home: Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Image"),
],
),
),
body: _pageOptions[_selectedTab],
bottomNavigationBar: Stack(
children: <Widget>[
BottomNavigationBar(
key: _bottomNavigationBarKey,
currentIndex: _selectedTab,
backgroundColor: Colors.deepOrangeAccent,
onTap: (int index) {
setState(() {
_selectedTab = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit, color: Colors.white),
title: Text(
'Neu',
style: TextStyle(color: Colors.white),
),
),
BottomNavigationBarItem(
icon: Icon(Icons.whatshot, color: Colors.white),
title: Text(
'Beliebt',
style: TextStyle(color: Colors.white),
)),
BottomNavigationBarItem(
icon: Icon(Icons.account_circle, color: Colors.white),
title: Text(
'Profil',
style: TextStyle(color: Colors.white),
)),
],
),
Positioned(
left: (_bottomNavigationBarSize.width / _pageOptions.length) *
_selectedTab,
child: Container(
height: _bottomNavigationBarSize.height,
width: _bottomNavigationBarSize.width / _pageOptions.length,
color: Colors.black26),
),
],
),
),
);
}
}
Ref
Get size and position of a widget in Flutter - Coflutter
check this answer I think that might help you , basically it says that you should wrap you nav bar inside material widget (not MatrialApp widget) and override the theme and specify another for your nav bar.
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.