How can I create this bevelled shape in Flutter? It should support a child which can be centered and scale when the child size changes.
You can achieve this by using ClipPath with BeveledRectangleBorder
Example
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Shape',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CustomShapeDemo(),
);
}
}
class CustomShapeDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Custom Shape Demo'),
),
body: Center(
child: ClipPath(
clipper: ShapeBorderClipper(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(100.0)),
),
child: Container(
height: 200.0,
width: 400.0,
color: Colors.red,
child: Center(
child: Text('80',
style: TextStyle(color: Colors.white, fontSize: 85.0)),
),
),
),
));
}
}
Result
Custom clipping path
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ClipPath(
clipper: Sky(),
child: Container(
width: 400,
height: 200,
color: Colors.red,
child: Center(
child: Text('80',
style: TextStyle(color: Colors.white, fontSize: 85.0)),
),
),
),
),
);
}
}
class Sky extends CustomClipper<Path> {
#override
getClip(Size size) {
double h = size.height;
double w = size.width;
double xOffset = 0.1;
Path path = Path()
..lineTo(w * xOffset, h)
..lineTo(w - w * xOffset, h)
..lineTo(w, h / 2)
..lineTo(w - w * xOffset, 0)
..lineTo(w * xOffset, 0.0)
..lineTo(0.0, h / 2)
..lineTo(w * xOffset, h);
return path;
}
#override
bool shouldReclip(CustomClipper oldClipper) {
return true;
}
}
Related
I have a main widget called DashboardWidget. Inside it, I have a Scaffold with BottomNavigationBar and a FloatingActionButton:
Now, I want to make a widget that would be dragged from the bottom by:
Swiping up with the finger.
Pressing on FloatingActionButton.
In other words, I want to expand the BottomNavigationBar.
Here's a design concept in case I was unclear.
The problem is, I'm not sure where to start to implement that. I've thought about removing the BottomNavigationBar and create a custom widget that can be expanded, but I'm not sure if it's possible either.
Output:
I used a different approach and did it without AnimationController, GlobalKey etc, the logic code is very short (_handleClick).
I only used 4 variables, simple and short!
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static double _minHeight = 80, _maxHeight = 600;
Offset _offset = Offset(0, _minHeight);
bool _isOpen = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF6F6F6),
appBar: AppBar(backgroundColor: Color(0xFFF6F6F6), elevation: 0),
body: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Align(
alignment: Alignment.topLeft,
child: FlatButton(
onPressed: _handleClick,
splashColor: Colors.transparent,
textColor: Colors.grey,
child: Text(_isOpen ? "Back" : ""),
),
),
Align(child: FlutterLogo(size: 300)),
GestureDetector(
onPanUpdate: (details) {
_offset = Offset(0, _offset.dy - details.delta.dy);
if (_offset.dy < _HomePageState._minHeight) {
_offset = Offset(0, _HomePageState._minHeight);
_isOpen = false;
} else if (_offset.dy > _HomePageState._maxHeight) {
_offset = Offset(0, _HomePageState._maxHeight);
_isOpen = true;
}
setState(() {});
},
child: AnimatedContainer(
duration: Duration.zero,
curve: Curves.easeOut,
height: _offset.dy,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 10)]),
child: Text("This is my Bottom sheet"),
),
),
Positioned(
bottom: 2 * _HomePageState._minHeight - _offset.dy - 28, // 56 is the height of FAB so we use here half of it.
child: FloatingActionButton(
child: Icon(_isOpen ? Icons.keyboard_arrow_down : Icons.add),
onPressed: _handleClick,
),
),
],
),
);
}
// first it opens the sheet and when called again it closes.
void _handleClick() {
_isOpen = !_isOpen;
Timer.periodic(Duration(milliseconds: 5), (timer) {
if (_isOpen) {
double value = _offset.dy + 10; // we increment the height of the Container by 10 every 5ms
_offset = Offset(0, value);
if (_offset.dy > _maxHeight) {
_offset = Offset(0, _maxHeight); // makes sure it does't go above maxHeight
timer.cancel();
}
} else {
double value = _offset.dy - 10; // we decrement the height by 10 here
_offset = Offset(0, value);
if (_offset.dy < _minHeight) {
_offset = Offset(0, _minHeight); // makes sure it doesn't go beyond minHeight
timer.cancel();
}
}
setState(() {});
});
}
}
You can use the BottomSheet class.
Here is a Medium-tutorial for using that, here is a youtube-tutorial using it and here is the documentation for the class.
The only difference from the tutorials is that you have to add an extra call method for showBottomSheet from your FloatingActionButton when it is touched.
Bonus: here is the Material Design page on how to use it.
You can check this code, it is a complete example of how to start implementing this kind of UI, take it with a grain of salt.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Orination Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
bool _isOpen;
double _dragStart;
double _hieght;
double _maxHight;
double _currentPosition;
GlobalKey _cardKey;
AnimationController _controller;
Animation<double> _cardAnimation;
#override
void initState() {
_isOpen = false;
_hieght = 50.0;
_cardKey = GlobalKey();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 700));
_cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.addListener(() {
setState(() {
_hieght = _cardAnimation.value;
});
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent,
titleSpacing: 0.0,
title: _isOpen
? MaterialButton(
child: Text(
"Back",
style: TextStyle(color: Colors.red),
),
onPressed: () {
_isOpen = false;
_cardAnimation = Tween(begin: _hieght, end: 50.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.forward(from: 0.0);
},
)
: Text(""),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.keyboard_arrow_up),
onPressed: () {
final RenderBox renderBoxCard = _cardKey.currentContext
.findRenderObject();
_maxHight = renderBoxCard.size.height;
_cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.forward(from: 0.0);
_isOpen = true;
}),
body: Stack(
key: _cardKey,
alignment: Alignment.bottomCenter,
children: <Widget>[
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black12,
),
GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child:Material(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0),
),
elevation: 60.0,
color: Colors.white,
// shadowColor: Colors.,
child: Container(
height: _hieght,
child: Center(
child: Text("Hello, You can drag up"),
),
),
),
),
],
),
);
}
void _onPanStart(DragStartDetails details) {
_dragStart = details.globalPosition.dy;
_currentPosition = _hieght;
}
void _onPanUpdate(DragUpdateDetails details) {
final RenderBox renderBoxCard = _cardKey.currentContext.findRenderObject();
_maxHight = renderBoxCard.size.height;
final hieght = _currentPosition - details.globalPosition.dy + _dragStart;
print(
"_currentPosition = $_currentPosition _hieght = $_hieght hieght = $hieght");
if (hieght <= _maxHight && hieght >= 50.0) {
setState(() {
_hieght = _currentPosition - details.globalPosition.dy + _dragStart;
});
}
}
void _onPanEnd(DragEndDetails details) {
_currentPosition = _hieght;
if (_hieght <= 60.0) {
setState(() {
_isOpen = false;
});
} else {
setState(() {
_isOpen = true;
});
}
}
}
Edit: I modified the code by using Material Widget instead of A container with shadow for better performance,If you have any issue, please let me know .
I'm new to Flutter, I have a requirement where I need to place a RaisedButton at the edge of an image as shown in the below screenshot(because of privacy, I covered the content on the mockup)
I tried by changing the padding but it's not working with all the devices in iOS and Android. Please help me out to achieve this for all kind of devices present in iOS and Android.
class SO extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.orange.shade200,
body: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.asset('assets/images/pngs/cake.png'),
SizedBox(
height: 25,
)
],
),
RaisedButton(
onPressed: () {},
child: Text("sample button"),
),
],
),
);
}
}
gives
EDIT: One of the non-hacky ways to do this.
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatelessWidget {
final sm = 100.0, lg = 200.0;
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
width: lg,
height: lg,
alignment: FractionalOffset.bottomCenter +
FractionalOffset.fromOffsetAndSize(
Offset(0, sm / 2),
Size(sm, sm),
),
child: Container(
color: Colors.blue,
width: sm,
height: sm,
),
);
}
}
Did you try Stack in Flutter ?
I had followed the hero animation tutorial in flutter. When i tried to add one more sceen i just noticed that the timedilation property is affecting the loading time of other screens also. And I had tried resetting the variable to zero but it didn't work as expected.
class PhotoHero extends StatelessWidget {
const PhotoHero({Key key, this.photo, this.onTap, this.width})
: super(key: key);
final String photo;
final VoidCallback onTap;
final double width;
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
); }}
class HeroAnimation extends StatelessWidget {
Widget build(BuildContext context) {
timeDilation = 10.0; // 1.0 means normal animation speed.
return Scaffold(
appBar: AppBar(
title: const Text('Basic Hero Animation'),
),
body: Center(
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 300.0,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (context) => SecondScreen()));
},
),
),
); }}
void main() {
runApp(MaterialApp(
routes: {
'/': (context) => HeroAnimation(),
'/second': (context) => SecondScreen(),
'/third': (context) => ThirdScreen(),
}, ));}
class ThirdScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Title(color: Colors.red, child: Text('Dummy Title')),
),
); }}
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
// Set background to blue to emphasize that it's a new route.
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: 'images/flippers-alpha.png',
width: 100.0,
onTap: () {
Navigator.of(context).pushNamed('/third');
},
),
),
); }}
This is expected as timeDilation is global property sort of , so you need to set it every time you need to change the speed of your animation onTap will be perfect place to do so,
check the modified code below
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class PhotoHero extends StatelessWidget {
const PhotoHero({Key key, this.photo, this.onTap, this.width})
: super(key: key);
final String photo;
final VoidCallback onTap;
final double width;
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Hero(
tag: photo,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Image.asset(
photo,
fit: BoxFit.contain,
),
),
),
),
); }}
class HeroAnimation extends StatelessWidget {
Widget build(BuildContext context) {
//timeDilation = 10.0; // 1.0 means normal animation speed.
return Scaffold(
appBar: AppBar(
title: const Text('Basic Hero Animation'),
),
body: Center(
child: PhotoHero(
photo: '/images/flippers-alpha.png',
width: 300.0,
onTap: () {
timeDilation = 10.0;
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (context) => SecondScreen()));
},
),
),
); }}
void main() {
runApp(MaterialApp(
routes: {
'/': (context) => HeroAnimation(),
'/second': (context) => SecondScreen(),
'/third': (context) => ThirdScreen(),
}, ));}
class ThirdScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
timeDilation = 1.0;
return Scaffold(
appBar: AppBar(
title: Title(color: Colors.red, child: Text('Dummy Title')),
),
); }}
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
//timeDilation = 1.0;
return Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
// Set background to blue to emphasize that it's a new route.
color: Colors.lightBlueAccent,
padding: const EdgeInsets.all(16.0),
alignment: Alignment.topLeft,
child: PhotoHero(
photo: '/images/flippers-alpha.png',
width: 100.0,
onTap: () {
Navigator.of(context).pushNamed('/third');
},
),
),
); }}
If you add timeDilation it will affect the other screens animation time. Because it's a global property. In order to go back to the normal animation speed you have to change that variable value to 1.0 which is the normal animation speed.
class FirstScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
timeDilation = 8.0; // Since we need the animation to slow down.
}
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
timeDilation = 1.0; // Changing the value to normal animation speed.
}
As a note, if you are coming back using back button the build method won't get called so the timeDilation value won't change to the value of the screen which you're in. In this case you've to make your screen as StatefulWidget then you can set the value on the life cycle methods.
I'm attempting to create a draggable slider-like widget (like a confirm slider). My question is if there is a way to constrain the draggable area?
import 'package:flutter/material.dart';
import 'confirmation_slider.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: new ListView(
children: <Widget>[
new Container(
margin: EdgeInsets.only(
top: 50.0
),
),
new Container(
margin: EdgeInsets.only(
left: 50.0,
right: 50.0
),
child: new Draggable(
axis: Axis.horizontal,
child: new FlutterLogo(size: 50.0),
feedback: new FlutterLogo(size: 50.0),
),
height: 50.0,
color: Colors.green
),
],
),
),
);
}
}
I imagined that the container class would constrain the draggable area, but it doesn't appear to do that.
No. That's not the goal of Draggable widget. Instead, use a GestureDetector to detect drag. Then combine it with something like Align to move your content around
Here's a fully working slider based on your current code.
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: Slider(),
),
),
);
}
}
class Slider extends StatefulWidget {
final ValueChanged<double> valueChanged;
Slider({this.valueChanged});
#override
SliderState createState() {
return new SliderState();
}
}
class SliderState extends State<Slider> {
ValueNotifier<double> valueListener = ValueNotifier(.0);
#override
void initState() {
valueListener.addListener(notifyParent);
super.initState();
}
void notifyParent() {
if (widget.valueChanged != null) {
widget.valueChanged(valueListener.value);
}
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Builder(
builder: (context) {
final handle = GestureDetector(
onHorizontalDragUpdate: (details) {
valueListener.value = (valueListener.value +
details.delta.dx / context.size.width)
.clamp(.0, 1.0);
},
child: FlutterLogo(size: 50.0),
);
return AnimatedBuilder(
animation: valueListener,
builder: (context, child) {
return Align(
alignment: Alignment(valueListener.value * 2 - 1, .5),
child: child,
);
},
child: handle,
);
},
),
);
}
}
As at 2022 here's a replica of #Remi's answer above, with minor tweaks to handle revisions to flutter/dart since 2018 (e.g. handling null-safety)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Slider(),
),
),
);
}
}
class Slider extends StatefulWidget {
final ValueChanged<double>? valueChanged;
const Slider({this.valueChanged});
#override
SliderState createState() {
return SliderState();
}
}
class SliderState extends State<Slider> {
ValueNotifier<double> valueListener = ValueNotifier(.0);
#override
void initState() {
valueListener.addListener(notifyParent);
super.initState();
}
void notifyParent() {
if (widget.valueChanged != null) {
widget.valueChanged!(valueListener.value);
}
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Builder(
builder: (context) {
final handle = GestureDetector(
onHorizontalDragUpdate: (details) {
valueListener.value = (valueListener.value + details.delta.dx / context.size!.width).clamp(.0, 1.0);
},
child: const FlutterLogo(size: 50.0),
);
return AnimatedBuilder(
animation: valueListener,
builder: (context, child) {
return Align(
alignment: Alignment(valueListener.value * 2 - 1, .5),
child: child,
);
},
child: handle,
);
},
),
);
}
}
I'm trying to make a flip card, what would be the best way to get the effect
I would use an AnimatedBuilder or AnimatedWidget to animate the values of a Transform widget. ScaleTransition almost does this for you, but it scales both directions, and you only want one.
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePageState createState() => new MyHomePageState();
}
class MyCustomCard extends StatelessWidget {
MyCustomCard({ this.colors });
final MaterialColor colors;
Widget build(BuildContext context) {
return new Container(
alignment: FractionalOffset.center,
height: 144.0,
width: 360.0,
decoration: new BoxDecoration(
color: colors.shade50,
border: new Border.all(color: new Color(0xFF9E9E9E)),
),
child: new FlutterLogo(size: 100.0, colors: colors),
);
}
}
class MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController _controller;
Animation<double> _frontScale;
Animation<double> _backScale;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_frontScale = new Tween(
begin: 1.0,
end: 0.0,
).animate(new CurvedAnimation(
parent: _controller,
curve: new Interval(0.0, 0.5, curve: Curves.easeIn),
));
_backScale = new CurvedAnimation(
parent: _controller,
curve: new Interval(0.5, 1.0, curve: Curves.easeOut),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return new Scaffold(
appBar: new AppBar(),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.flip_to_back),
onPressed: () {
setState(() {
if (_controller.isCompleted || _controller.velocity > 0)
_controller.reverse();
else
_controller.forward();
});
},
),
body: new Center(
child: new Stack(
children: <Widget>[
new AnimatedBuilder(
child: new MyCustomCard(colors: Colors.orange),
animation: _backScale,
builder: (BuildContext context, Widget child) {
final Matrix4 transform = new Matrix4.identity()
..scale(1.0, _backScale.value, 1.0);
return new Transform(
transform: transform,
alignment: FractionalOffset.center,
child: child,
);
},
),
new AnimatedBuilder(
child: new MyCustomCard(colors: Colors.blue),
animation: _frontScale,
builder: (BuildContext context, Widget child) {
final Matrix4 transform = new Matrix4.identity()
..scale(1.0, _frontScale.value, 1.0);
return new Transform(
transform: transform,
alignment: FractionalOffset.center,
child: child,
);
},
),
],
),
),
);
}
}
I used simple approach, rotated it on X axis. Here is the full code.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController _controller;
bool _flag = true;
Color _color = Colors.blue;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1), value: 1);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.crop_rotate),
onPressed: () async {
if (_flag) {
await _controller.reverse();
setState(() {
_color = Colors.orange;
});
await _controller.forward();
} else {
await _controller.reverse();
setState(() {
_color = Colors.blue;
});
await _controller.forward();
}
_flag = !_flag;
},
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform(
transform: Matrix4.rotationX((1 - _controller.value) * math.pi / 2),
alignment: Alignment.center,
child: Container(
height: 100,
margin: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
decoration: BoxDecoration(color: _color.withOpacity(0.2), border: Border.all(color: Colors.grey)),
child: FlutterLogo(colors: _color, size: double.maxFinite),
),
);
},
),
),
);
}
}
You can use the flip_card Flutter package. It lets you define a front and back widget and can be flipped horizontally or vertically.