98

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.

enter image description here (Source)

An gif example from the macOS mail App:

enter image description here

Lukas Kirner
  • 3,989
  • 6
  • 23
  • 30

6 Answers6

161

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:

Drawer (iOS) animation

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'),
    ),
  ],
);
Romain Rastel
  • 5,144
  • 2
  • 25
  • 23
  • 4
    looks great. I implement to my project. I wonder if its possible to implement an action that runs a function when slide full screen direction – Bilal Şimşek Jan 31 '19 at 10:58
  • 2
    awesome widget. I don't know why are not these great widgets already included in Flutter. One question, is possible to close the option when we click again on the element? (to avoid another swipe to close them again) – Dani Sep 05 '19 at 14:46
  • 1
    cool widget to have. One question I have is how do I tell the user that the row is slidable? – Panduranga Rao Sadhu Feb 03 '20 at 07:54
  • 1
    @PandurangaRaoSadhu meybe you can use tooltip or something like that – Ali Rn Oct 09 '20 at 17:13
  • Hi Roman the flutter_slidable package is pretty good i wanted to know how to show a demo swipe animation only to the first item when the customer first time opens the page – Snivio Dec 09 '21 at 19:27
  • as of today in 2022, if you use the latest flutter slideable package, it completely changed, so better if you put flutter_slidable: ^0.5.7 in the pubspec than the latest – Smith July Mar 17 '22 at 09:59
  • Great work, Awesome widget. I don't know why are not these great widgets already included in Flutter. But your work is appreciated! – Ali ijaz Apr 26 '22 at 09:04
  • I have a problem with it, since it gets "actionExtentRatio" it takes reacts to size of the child widget. what is I want to add an exact width to action pane? – Ardeshir ojan Nov 19 '22 at 21:18
  • Is there a way to close the slider when we slide open another list in listview builder? – Sindu Mar 01 '23 at 08:14
58

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.

enter image description here

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.

Daniel Brotherston
  • 1,954
  • 4
  • 18
  • 28
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • 1
    The original post is asking about how to place a widget to appear at the right corner when you swipe, in the gif provided, that would be the red rectangle with the trash icon in it, the question is not about the swipe effect itself rather making the red rectangle appear when you swipe. – Shady Aziza Oct 10 '17 at 10:19
  • 1
    Dismissible contains a `background` parameter which is made for that purpose. – Rémi Rousselet Oct 10 '17 at 11:42
  • But you need to stop the swipe effect at a certain position in order for the user to be able to interact with the items in the background, otherwise the widget is going to resize to full size or zero before you will be able to interact with the background, is there a way to freeze the swipe at a certain position so it shows enough of the background and be able to interact with it ? – Shady Aziza Oct 10 '17 at 12:51
  • But that's kinda against Material rules. Which flutter will somehow prevents you from doing. This is clearly a "Dismiss swipe", which is used to send the item off screen. Something like a "More info" should be done using an "Edge swipe" (like android notifications bar) or an "Expand" gesture (android notification card here) – Rémi Rousselet Oct 10 '17 at 15:03
  • So and how can I implement what I want even if its against the Material Rules? – Lukas Kirner Oct 10 '17 at 16:33
  • 1
    @Darky this is already fine in iOS design language, why would flutter prevent me from doing this just because it does not follow material design, maybe I am making two views, one for android and one for iOS, check this: https://i.imgur.com/jvsfElV.gif?1 – Shady Aziza Oct 10 '17 at 17:42
  • @Darky then how can i move an widget off screen by an swipe without the widget Dismissible? – Lukas Kirner Oct 10 '17 at 21:37
  • 1
    @azizia It's not that flutter prevents you from doing anything else but material. Just that Cupertino theme is not fully finished yet I guess ? Anyway if you want to have this exact transition, you'd have to implement it yourself. I edited my post with an example. – Rémi Rousselet Oct 11 '17 at 11:17
  • Thank! this answer works! i this case how could i slide only one item at time @RémiRousselet – Andy Torres Jan 19 '21 at 01:52
  • i wish prevent this: please check img https://ibb.co/MRGRBf2 – Andy Torres Jan 19 '21 at 01:58
8

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'),
    ),
  ],
);
Nimantha
  • 6,405
  • 6
  • 28
  • 69
5

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(),
                    ),
                  );
                })
          ],
        );
      })
    );
  }
}

enter image description here

Dmitry Rodionov
  • 333
  • 2
  • 6
4

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.

Amit Goswami
  • 314
  • 2
  • 11
  • 2
    This is just the flutter example code for Dismissible. no delete button unlike in the answers provided above. – Pavan Dec 05 '19 at 06:24
0

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

enter image description here

enter image description here

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(),
                  )
Ahmed Nabil
  • 136
  • 1
  • 7