Related
For some reason, when I press the TextField, it focuses for a split second and then unfocuses immediately as the soft keyboard comes up. I can still type and submit, but the labelText doesn't disappear like it's supposed to and most importantly, FocusManager.instance.primaryFocus?.unfocus() doesn't let the keyboard disappear.
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(5),
child: Row(
children: [
Expanded(
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
),
child: Container(
margin: const EdgeInsets.only(left: 10, bottom: 10, top: 10),
child: TextField(
onTap: () => myFocusNode.requestFocus(),
decoration: const InputDecoration(
labelText: ' Enter task',
border: InputBorder.none,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
controller: textController,
focusNode: myFocusNode,
onSubmitted: (_) {
submit();
myFocusNode.requestFocus();
textController.clear();
},
),
),
),
),
CircleAvatar(
child: TextButton(
onPressed: () {
submit();
myFocusNode.requestFocus();
textController.clear();
},
child: const FittedBox(
child: Text(
'Add',
style: TextStyle(
color: Colors.white,
),
),
),
),
),
],
),
);
}
I think this may be because the app is rebuilt when the soft keyboard shows up, but I'm not sure. What can I do to fix this?
I think you need to remove this:
onTap: () => myFocusNode.requestFocus(),
If you need, you can control the focus action when filed si submitted with the textInputAction property:
// Go to next field
textInputAction: TextInputAction.next
// Go to previous field
textInputAction: TextInputAction.previous
// Don't move focus
textInputAction: TextInputAction.none
// Many other possible values, check the doc ...
UPDATE
When i try your build code on a MediaPad tablet, it work like a charm, here is my implementation:
import 'package:flutter/material.dart';
class DoorMeasure extends StatefulWidget {
const DoorMeasure({Key? key}) : super(key: key);
#override
State<DoorMeasure> createState() => _DoorMeasureState();
}
class _DoorMeasureState extends State<DoorMeasure> {
var myFocusNode;
var textController = TextEditingController()..text = '';
#override
void initState() {
myFocusNode = new FocusNode();
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
),
child: Container(
margin: const EdgeInsets.only(left: 10, bottom: 10, top: 10),
child: TextField(
onTap: () => myFocusNode.requestFocus(),
decoration: const InputDecoration(
labelText: ' Enter task',
border: InputBorder.none,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
controller: textController,
focusNode: myFocusNode,
onSubmitted: (_) {
myFocusNode.requestFocus();
textController.clear();
},
),
),
),
),
CircleAvatar(
child: TextButton(
onPressed: () {
myFocusNode.requestFocus();
textController.clear();
},
child: const FittedBox(
child: Text(
'Add',
style: TextStyle(
color: Colors.white,
),
),
),
),
),
],
),
);
}
}
I want to make tutorial screen that show to user at beginning. it's like below :
my specific question, how to make some certain elements will show normally and other are opaque ?
also the arrow and text, how to make them point perfectly based on mobile device screen size (mobile responsiveness) ?
As RoyalGriffin mentioned, you can use highlighter_coachmark library, and I am also aware of the error you are getting, the error is there because you are using RangeSlider class which is imported from 2 different packages. Can you try this example in your app and check if it is working?
Add highlighter_coachmark to your pubspec.yaml file
dependencies:
flutter:
sdk: flutter
highlighter_coachmark: ^0.0.3
Run flutter packages get
Example:
import 'package:highlighter_coachmark/highlighter_coachmark.dart';
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GlobalKey _fabKey = GlobalObjectKey("fab"); // used by FAB
GlobalKey _buttonKey = GlobalObjectKey("button"); // used by RaisedButton
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
key: _fabKey, // setting key
onPressed: null,
child: Icon(Icons.add),
),
body: Center(
child: RaisedButton(
key: _buttonKey, // setting key
onPressed: showFAB,
child: Text("RaisedButton"),
),
),
);
}
// we trigger this method on RaisedButton click
void showFAB() {
CoachMark coachMarkFAB = CoachMark();
RenderBox target = _fabKey.currentContext.findRenderObject();
// you can change the shape of the mark
Rect markRect = target.localToGlobal(Offset.zero) & target.size;
markRect = Rect.fromCircle(center: markRect.center, radius: markRect.longestSide * 0.6);
coachMarkFAB.show(
targetContext: _fabKey.currentContext,
markRect: markRect,
children: [
Center(
child: Text(
"This is called\nFloatingActionButton",
style: const TextStyle(
fontSize: 24.0,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
)
],
duration: null, // we don't want to dismiss this mark automatically so we are passing null
// when this mark is closed, after 1s we show mark on RaisedButton
onClose: () => Timer(Duration(seconds: 1), () => showButton()),
);
}
// this is triggered once first mark is dismissed
void showButton() {
CoachMark coachMarkTile = CoachMark();
RenderBox target = _buttonKey.currentContext.findRenderObject();
Rect markRect = target.localToGlobal(Offset.zero) & target.size;
markRect = markRect.inflate(5.0);
coachMarkTile.show(
targetContext: _fabKey.currentContext,
markRect: markRect,
markShape: BoxShape.rectangle,
children: [
Positioned(
top: markRect.bottom + 15.0,
right: 5.0,
child: Text(
"And this is a RaisedButton",
style: const TextStyle(
fontSize: 24.0,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
)
],
duration: Duration(seconds: 5), // this effect will only last for 5s
);
}
}
Output:
You can use this library to help you achieve what you need. It allows you to mark views which you want to highlight and how you want to highlight them.
Wrap your current top widget with a Stack widget, having the first child of the Stack your current widget.
Below this widget add a Container with black color, wrapped with Opacity like so:
return Stack(
children: <Widget>[
Scaffold( //first child of the stack - the current widget you have
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text("Foo"),
Text("Bar"),
],
),
)),
Opacity( //seconds child - Opaque layer
opacity: 0.7,
child: Container(
decoration: BoxDecoration(color: Colors.black),
),
)
],
);
you then need to create image assets of the descriptions and arrows, in 1x, 2x, 3x resolutions, and place them in your assets folder in the appropriate structure as described here: https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets
you can then use Image.asset(...) widget to load your images (they will be loaded in the correct resolution), and place these widgets on a different container that will also be a child of the stack, and will be placed below the black container in the children list (the Opacity widget on the example above).
It should be mentioned that instead of an opaque approach the Material-oriented feature_discovery package uses animation and integrates into the app object hierarchy itself and therefore requires less custom highlight programming. The turnkey solution also supports multi-step highlights.
Screenshot (Using null-safety):
Since highlighter_coachmark doesn't support null-safety as of this writing, use tutorial_coach_mark which supports null-safety.
Full Code:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final List<TargetFocus> targets;
final GlobalKey _key1 = GlobalKey();
final GlobalKey _key2 = GlobalKey();
final GlobalKey _key3 = GlobalKey();
#override
void initState() {
super.initState();
targets = [
TargetFocus(
identify: 'Target 1',
keyTarget: _key1,
contents: [
TargetContent(
align: ContentAlign.bottom,
child: _buildColumn(title: 'First Button', subtitle: 'Hey!!! I am the first button.'),
),
],
),
TargetFocus(
identify: 'Target 2',
keyTarget: _key2,
contents: [
TargetContent(
align: ContentAlign.top,
child: _buildColumn(title: 'Second Button', subtitle: 'I am the second.'),
),
],
),
TargetFocus(
identify: 'Target 3',
keyTarget: _key3,
contents: [
TargetContent(
align: ContentAlign.left,
child: _buildColumn(title: 'Third Button', subtitle: '... and I am third.'),
)
],
),
];
}
Column _buildColumn({required String title, required String subtitle}) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(subtitle),
)
],
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: ElevatedButton(
key: _key1,
onPressed: () {},
child: Text('Button 1'),
),
),
Align(
alignment: Alignment.center,
child: ElevatedButton(
key: _key2,
onPressed: () {
TutorialCoachMark(
context,
targets: targets,
colorShadow: Colors.cyanAccent,
).show();
},
child: Text('Button 2'),
),
),
Align(
alignment: Alignment.bottomRight,
child: ElevatedButton(
key: _key3,
onPressed: () {},
child: Text('Button 3'),
),
),
],
),
),
);
}
}
Thanks to #josxha for the suggestion.
If you don't want to rely on external libraries, you can just do it yourself. It's actually not that hard.
Using a stack widget you can put the semi-transparent overlay on top of everything. Now, how do you "cut holes" into that overlay that emphasize underlying UI elements?
Here is an article that covers the exact topic: https://www.flutterclutter.dev/flutter/tutorials/how-to-cut-a-hole-in-an-overlay/2020/510/
I will summarize the possibilities you have:
Use a ClipPath
By using a CustomClipper, given a widget, you can define what's being drawn and what's not. You can then just draw a rectangle or an oval around the relevant underlying UI element:
class InvertedClipper extends CustomClipper<Path> {
#override
Path getClip(Size size) {
return Path.combine(
PathOperation.difference,
Path()..addRect(
Rect.fromLTWH(0, 0, size.width, size.height)
),
Path()
..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
..close(),
);
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}
Insert it like this in your app:
ClipPath(
clipper: InvertedClipper(),
child: Container(
color: Colors.black54,
),
);
Use a CustomPainter
Instead of cutting a hole in an overlay, you can directly draw a shape that is as big as the screen and has the hole already cut out:
class HolePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black54;
canvas.drawPath(
Path.combine(
PathOperation.difference,
Path()..addRect(
Rect.fromLTWH(0, 0, size.width, size.height)
),
Path()
..addOval(Rect.fromCircle(center: Offset(size.width -44, size.height - 44), radius: 40))
..close(),
),
paint
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
Insert it like this:
CustomPaint(
size: MediaQuery.of(context).size,
painter: HolePainter()
);
Use ColorFiltered
This solution works without paint. It cuts holes where children in the widget trees are inserted by using a specific blendMode:
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black54,
BlendMode.srcOut
),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: Colors.transparent,
),
child: Align(
alignment: Alignment.bottomRight,
child: Container(
margin: const EdgeInsets.only(right: 4, bottom: 4),
height: 80,
width: 80,
decoration: BoxDecoration(
// Color does not matter but must not be transparent
color: Colors.black,
borderRadius: BorderRadius.circular(40),
),
),
),
),
],
),
);
I would like to achieve Android like TextField validation in Flutter.
I tried the TextField docs of Flutter and the InputDecoration has a property of errorText but is displays error at the bottom of textfield. I want to achieve something like below in Flutter
You can use a TextFormField along with a custom Tooltip inside a Stack to achieve this effect. In the decoration property for the TextFormField you can use the suffixIcon property of the InputDecoration class to pass the error icon.
And you can use a bool variable to show/hide the tooltip message when the validation error occurs.
Example code for the TextFormField:
TextFormField(
decoration: InputDecoration(
//Set the different border properties for a custom design
suffixIcon: IconButton(
icon: Icon(Icons.error, color: Colors.red),
onPressed: () {
setState(() {
showTooltip = !showTooltip; //Toggles the tooltip
});
},
),
),
validator: (String value) {
if(value.isEmpty) {
setState(() {
showTooltip = true; //Toggles the tooltip
});
return "";
}
}
);
You can wrap the above code along with your custom tooltip widget code with a Stack to achieve the error tooltip effect.
Below is a quick working example. You can design your own custom tooltip widget and use it in your code.
Example:
class LoginAlternate extends StatefulWidget {
#override
_LoginAlternateState createState() => _LoginAlternateState();
}
class _LoginAlternateState extends State<LoginAlternate> {
GlobalKey<FormState> _formKey = GlobalKey();
bool showTooltip = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blueGrey,
body: Container(
padding: EdgeInsets.symmetric(
horizontal: 100,
vertical: 100
),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
Stack(
alignment: Alignment.topRight,
overflow: Overflow.visible,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderSide: BorderSide.none
),
suffixIcon: IconButton(
icon: Icon(Icons.error, color: Colors.red,),
onPressed: () {
setState(() {
showTooltip = !showTooltip;
});
},
),
hintText: "Password"
),
validator: (value) {
if(value.isEmpty) {
setState(() {
showTooltip = true;
});
return "";
}
},
),
Positioned(
top: 50,
right: 10,
//You can use your own custom tooltip widget over here in place of below Container
child: showTooltip
? Container(
width: 250,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all( color: Colors.red, width: 2.0 ),
borderRadius: BorderRadius.circular(10)
),
padding: EdgeInsets.symmetric(horizontal: 10),
child: Center(
child: Text(
"Your passwords must match and be 6 characters long.",
),
),
) : Container(),
)
],
),
RaisedButton(
child: Text("Validate"),
onPressed: () {
_formKey.currentState.validate();
},
),
],
),
),
),
);
}
}
I think you are talking about ToolTip
You can use this library or Go through Flutter doc
new Tooltip(message: "Hello ToolTip", child: new Text("Press"));
you can use the library super_tooltip #
You can customize this type of error by using overlay widget. Error can be shown with overlay widget and that error icon can be changed using inputDecoration of TextField.
Here is the link for understanding overlay widget--
https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9
I can't figure out why there is space at the bottom of TextField between the text and the blue line.
Here is my code :
Future<Null> _selectNoteType (BuildContext context) async {
switch (await showDialog<Null>(
context: context,
builder: (BuildContext context) {
return new SimpleDialog(
title: const Text('Select Note Type'),
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: new TextField(
keyboardType: TextInputType.text,
maxLines: 1,
style: new TextStyle(
color: Colors.black,
fontSize: 20.0
),
),
),
new SimpleDialogOption(
onPressed: () {},
child: const Text('Text')
),
new SimpleDialogOption(
onPressed: () {},
child: const Text('Checklist')
),
],
);
}
)) {}
}
In my case the TextField would still not collapse even after using InputDecoration.collapsed().
My version doesn't have any padding at all and takes the minimum size:
TextField(
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
isDense: true,
border: InputBorder.none,
),
minLines: 1,
maxLines: 1,
);
Live preview:
https://dartpad.dev/3f9149a1c8f5eec352c796e7585e233c
You can use a collapsed InputDecoration for the decoration: property of the TextField.
Future<Null> _selectNoteType(BuildContext context) async {
InputDecoration decoration = const InputDecoration.collapsed()
..applyDefaults(Theme.of(context).inputDecorationTheme);
switch (await showDialog<Null>(
context: context,
builder: (BuildContext context) {
return new SimpleDialog(
title: const Text('Select Note Type'),
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: new TextField(
decoration: decoration,
keyboardType: TextInputType.text,
maxLines: 1,
style: new TextStyle(color: Colors.black, fontSize: 20.0),
),
),
new SimpleDialogOption(
onPressed: () {}, child: const Text('Text')),
new SimpleDialogOption(
onPressed: () {}, child: const Text('Checklist')),
],
);
})) {
}
}
But you must know the consequences of using a collapsed InputDecoration. From the documentation:
/// Whether the decoration is the same size as the input field.
///
/// A collapsed decoration cannot have [labelText], [errorText], an [icon].
///
/// To create a collapsed input decoration, use [InputDecoration..collapsed].
final bool isCollapsed;
And for the InputDecoration.collapse() constructor:
/// Defines an [InputDecorator] that is the same size as the input field.
///
/// This type of input decoration does not include a border by default.
///
/// Sets the [isCollapsed] property to true.
const InputDecoration.collapsed({
isDense will do the trick. uses less vertical space
TextField(
decoration: InputDecoration(
isDense: true,
),
);
Somedays ago I decided to choose an Ui for an app from Pinterest to practice building apps with Flutter but I'm stuck with the Slider which shows an "more" and "delete" button on horizontal drag. Picture on the right.
I don't have enough knowledge to use Gestures combined with Animations to create something like this in flutter. Thats why I hope that someone of you can make an example for everyone like me that we can understand how to implement something like this in a ListView.builder.
(Source)
An gif example from the macOS mail App:
I created a package for doing this kind of layout: flutter_slidable (Thanks Rémi Rousselet for the based idea)
With this package it's easier to create contextual actions for a list item. For example if you want to create the kind of animation you described:
You will use this code:
new Slidable(
delegate: new SlidableDrawerDelegate(),
actionExtentRatio: 0.25,
child: new Container(
color: Colors.white,
child: new ListTile(
leading: new CircleAvatar(
backgroundColor: Colors.indigoAccent,
child: new Text('$3'),
foregroundColor: Colors.white,
),
title: new Text('Tile n°$3'),
subtitle: new Text('SlidableDrawerDelegate'),
),
),
actions: <Widget>[
new IconSlideAction(
caption: 'Archive',
color: Colors.blue,
icon: Icons.archive,
onTap: () => _showSnackBar('Archive'),
),
new IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () => _showSnackBar('Share'),
),
],
secondaryActions: <Widget>[
new IconSlideAction(
caption: 'More',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
new IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
);
There's already a widget for this kind of gesture. It's called Dismissible.
You can find it here. https://docs.flutter.io/flutter/widgets/Dismissible-class.html
EDIT
If you need the exact same transtion, you'd probably have to implement if yourself.
I made a basic example. You'd probably want to tweak the animation a bit, but it's working at least.
class Test extends StatefulWidget {
#override
_TestState createState() => new _TestState();
}
class _TestState extends State<Test> {
double rating = 3.5;
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView(
children: ListTile
.divideTiles(
context: context,
tiles: new List.generate(42, (index) {
return new SlideMenu(
child: new ListTile(
title: new Container(child: new Text("Drag me")),
),
menuItems: <Widget>[
new Container(
child: new IconButton(
icon: new Icon(Icons.delete),
),
),
new Container(
child: new IconButton(
icon: new Icon(Icons.info),
),
),
],
);
}),
)
.toList(),
),
);
}
}
class SlideMenu extends StatefulWidget {
final Widget child;
final List<Widget> menuItems;
SlideMenu({this.child, this.menuItems});
#override
_SlideMenuState createState() => new _SlideMenuState();
}
class _SlideMenuState extends State<SlideMenu> with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
initState() {
super.initState();
_controller = new AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
}
#override
dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
final animation = new Tween(
begin: const Offset(0.0, 0.0),
end: const Offset(-0.2, 0.0)
).animate(new CurveTween(curve: Curves.decelerate).animate(_controller));
return new GestureDetector(
onHorizontalDragUpdate: (data) {
// we can access context.size here
setState(() {
_controller.value -= data.primaryDelta / context.size.width;
});
},
onHorizontalDragEnd: (data) {
if (data.primaryVelocity > 2500)
_controller.animateTo(.0); //close menu on fast swipe in the right direction
else if (_controller.value >= .5 || data.primaryVelocity < -2500) // fully open if dragged a lot to left or on fast swipe to left
_controller.animateTo(1.0);
else // close if none of above
_controller.animateTo(.0);
},
child: new Stack(
children: <Widget>[
new SlideTransition(position: animation, child: widget.child),
new Positioned.fill(
child: new LayoutBuilder(
builder: (context, constraint) {
return new AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return new Stack(
children: <Widget>[
new Positioned(
right: .0,
top: .0,
bottom: .0,
width: constraint.maxWidth * animation.value.dx * -1,
child: new Container(
color: Colors.black26,
child: new Row(
children: widget.menuItems.map((child) {
return new Expanded(
child: child,
);
}).toList(),
),
),
),
],
);
},
);
},
),
)
],
),
);
}
}
EDIT
Flutter no longer allows type Animation<FractionalOffset> in SlideTransition animation property. According to this post https://groups.google.com/forum/#!topic/flutter-dev/fmr-C9xK5t4 it should be replaced with AlignmentTween but this also doesn't work. Instead, according to this issue: https://github.com/flutter/flutter/issues/13812 replacing it instead with a raw Tween and directly creating Offset object works instead. Unfortunately, the code is much less clear.
Updated Code with Null Safety: Flutter: 2.x
Firstly you need to add the flutter_slidable package in your project and add below code then Let's enjoy...
Slidable(
actionPane: SlidableDrawerActionPane(),
actionExtentRatio: 0.25,
child: Container(
color: Colors.white,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.indigoAccent,
child: Text('$3'),
foregroundColor: Colors.white,
),
title: Text('Tile n°$3'),
subtitle: Text('SlidableDrawerDelegate'),
),
),
actions: <Widget>[
IconSlideAction(
caption: 'Archive',
color: Colors.blue,
icon: Icons.archive,
onTap: () => _showSnackBar('Archive'),
),
IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () => _showSnackBar('Share'),
),
],
secondaryActions: <Widget>[
IconSlideAction(
caption: 'More',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
);
I have a task that needs the same swipeable menu actions I tried answeres of Romain Rastel and Rémi Rousselet. but I have complex widget tree. the issue with that slideable solutions is they go on other widgets(to left widgets of listview). I found a batter solution here someone wrote a nice article medium and GitHub sample is here.
I look at a lot of articles and answers, and find #Rémi Rousselet answer the best fitted to use without third party libraries.
Just put some improvements to #Rémi's code to make it usable in modern SDK without errors and null safety.
Also I smooth a little bit movement, to make the speed of buttons appeared the same as finger movement.
And I put some comments into the code:
import 'package:flutter/material.dart';
class SlidebleList extends StatefulWidget {
const SlidebleList({Key? key}) : super(key: key);
#override
State<SlidebleList> createState() => _SlidebleListState();
}
class _SlidebleListState extends State<SlidebleList> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: ListTile.divideTiles(
context: context,
tiles: List.generate(42, (index) {
return SlideMenu(
menuItems: <Widget>[
Container(
color: Colors.black12,
child: IconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {},
),
),
Container(
color: Colors.red,
child: IconButton(
color: Colors.white,
icon: const Icon(Icons.delete),
onPressed: () {},
),
),
],
child: const ListTile(
title: Text("Just drag me"),
),
);
}),
).toList(),
),
);
}
}
class SlideMenu extends StatefulWidget {
final Widget child;
final List<Widget> menuItems;
const SlideMenu({Key? key,
required this.child, required this.menuItems
}) : super(key: key);
#override
State<SlideMenu> createState() => _SlideMenuState();
}
class _SlideMenuState extends State<SlideMenu> with SingleTickerProviderStateMixin {
late AnimationController _controller;
#override
initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 200));
}
#override
dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
//Here the end field will determine the size of buttons which will appear after sliding
//If you need to appear them at the beginning, you need to change to "+" Offset coordinates (0.2, 0.0)
final animation =
Tween(begin: const Offset(0.0, 0.0),
end: const Offset(-0.2, 0.0))
.animate(CurveTween(curve: Curves.decelerate).animate(_controller));
return GestureDetector(
onHorizontalDragUpdate: (data) {
// we can access context.size here
setState(() {
//Here we set value of Animation controller depending on our finger move in horizontal axis
//If you want to slide to the right, change "-" to "+"
_controller.value -= (data.primaryDelta! / (context.size!.width*0.2));
});
},
onHorizontalDragEnd: (data) {
//To change slide direction, change to data.primaryVelocity! < -1500
if (data.primaryVelocity! > 1500)
_controller.animateTo(.0); //close menu on fast swipe in the right direction
//To change slide direction, change to data.primaryVelocity! > 1500
else if (_controller.value >= .5 || data.primaryVelocity! < -1500)
_controller.animateTo(1.0); // fully open if dragged a lot to left or on fast swipe to left
else // close if none of above
_controller.animateTo(.0);
},
child: LayoutBuilder(builder: (context, constraint) {
return Stack(
children: [
SlideTransition(
position: animation,
child: widget.child,
),
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
//To change slide direction to right, replace the right parameter with left:
return Positioned(
right: .0,
top: .0,
bottom: .0,
width: constraint.maxWidth * animation.value.dx * -1,
child: Row(
children: widget.menuItems.map((child) {
return Expanded(
child: child,
);
}).toList(),
),
);
})
],
);
})
);
}
}
i had the same problem and and as the accepted answer suggests, i used flutter_slidable
but i needed a custom look for the actions and also i wanted them to be vertically aligned not horizontal.
i noticed that actionPane() can take a list of widgets as children not only
SlidableAction.
so i was able to make my custom actions,and wanted to share the code and results with you here.
this is the layout
this is the code i used :
ListView.builder(
itemBuilder: (context, index) {
return Slidable(
startActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.25,
// A pane can dismiss the Slidable.
// All actions are defined in the children parameter.
children: [
Expanded(
flex: 1,
child: Card(
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Column(
children: [
Expanded(
child: InkWell(
child: Container(
width: double.infinity,
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.edit,
color:
Colors.deepPurple),
Text(
LocalizationKeys.edit.tr,
style: TextStyle(
color:
Colors.deepPurple,
fontSize: 16),
),
],
),
),
onTap: () {},
),
),
Container(
height: 1,
color: Colors.deepPurple,
),
Expanded(
child: InkWell(
child: Container(
width: double.infinity,
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.delete,
color: Colors.red),
Text(
LocalizationKeys
.app_delete.tr,
style: TextStyle(
color: Colors.red,
fontSize: 16),
),
],
),
),
onTap: () {},
),
),
],
),
),
),
]),
child: Card(
margin: EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 16),
Text(_lecturesViewModel
.lectures.value[index].centerName),
SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_lecturesViewModel
.lectures.value[index].classLevel),
Text(_lecturesViewModel
.lectures.value[index].material),
],
),
SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.location_pin),
Text(_lecturesViewModel
.lectures.value[index].city),
Text(_lecturesViewModel
.lectures.value[index].area),
],
),
SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Icon(Icons.calendar_today),
Text(_lecturesViewModel
.lectures.value[index].day),
],
),
Container(
height: 1,
width: 60,
color: Colors.black,
),
Column(
children: [
Icon(Icons.punch_clock),
Text(_lecturesViewModel
.lectures.value[index].time),
],
),
Container(
height: 1,
width: 60,
color: Colors.black,
),
Column(
children: [
Icon(Icons.money),
Text(
"${_lecturesViewModel.lectures.value[index].price.toString()}ج "),
],
)
]),
SizedBox(height: 16),
]),
),
);
},
itemCount: _lecturesViewModel.lectures.length,
physics: BouncingScrollPhysics(),
)