Flutter: Slide Button

I saw a post in a group who was looking for this. I make this for him and helped him. I do this kind of things a lot.

I am freelancer who help people building great product with the great Technologies. This was not related to my freelancing work it was community this. So, I have right to share the code to talk about the widget.

Let’s Start

Before starting let me tell you one thing if you are only looking for the code. Then click here and get the code or you can scroll to the bottom. But If you are looking for some skill which will enable you to build these features by yourself then you should follow the article.

I made the Widget in 4 Minutes but I took around x minutes in writing the article.

Widget Required

First let’s understand what are the widget

  • Stack
  • Positioned Widget
  • GestureDetector

These three widget are must and all other are used for styling.

Stack

Stack is a widget in flutter which is being used for layout. It’s same as it’s name, I mean english meaning of Stack and usage of stack are same.

With the help of stack we place one widget on the top of other widget.

Positioned

It is widget which help us in positioning a widget by telling the relative distance from left right top bottom.

GestureDetector

This widget will helps us in understanding the drag behavior on any widget. I mean when you drag and leave the contact from screen, we need the user drag position in order to update the UI.

There are more widget but those are pretty common widget which we will be using for making design beautiful.

Approach

I know some of might not think of building this design because it’s little difficult to think. How to approach these kind of problems. Approaching to any problem has two possible outcomes

Break things into small part and it will starts looking simpler to you. I follow this principle always, breaking complex problem into small parts and combining them at the end.

  • Pass: You achieve the result.
  • Fail: You learnt something new.

In early days I used to think all the things which I can’t build is very difficult. I was not absolutely correct it’s not very difficult, When you think anything without breaking them into small parts, it will definitely look different.

You should try difficult things because it always teach you something new even if you fails. If you ever fail in building then accept that you fail then only you will realise your mistake and learnings.

The full source code is available here and you can grab that and use it. Source code will never teach you how I approached the solution and what you should learn my learning path.

Warm up

Let’s build the UI without animation.

  • Column
  • Container
sample.dart
  Widget _threeCircle() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      mainAxisSize: MainAxisSize.max,
      children: <Widget>[
        Container(
          width: 100.0,
          height: 100.0,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(100),
          ),
        ),
        Container(
          width: 100.0,
          height: 100.0,
          decoration: BoxDecoration(
            color: Colors.grey,
            borderRadius: BorderRadius.circular(100),
          ),
        ),
        Container(
          width: 100.0,
          height: 100.0,
          decoration: BoxDecoration(
            color: Colors.green,
            borderRadius: BorderRadius.circular(100),
          ),
        ),
      ],
    );
  }

Step 1

There is various of doing same thing and every method has it’s pros and cons. You should choose the method based on our use case. Here the above method can’t be used to do the animation part but we will use something similar.

Step 1 was for you motivation that if you try then you will definitely succeed today or tomorrow.

You might think end result is same so we can choose any method but it’s not correct. We should always consider the problem before choosing the solution.

Here also we made the UI and it’s good, this UI can be build using different approach. Each approach has it’s pros and cons.

# Column Stack
1 Arrange one widget after other Arrange widget on the top of other
2 Arrange one widget after other Arrange widget on the top of other
3 You can’t position children widget You can position children widget

As stack can be used easily used in some place to replace stack. Sometimes stack can be easily alternative of Column. When you want to position the widget location then you should choose stack because it helps a lot.

Let’s build the UI again using Stack

  • Stack
  • Container
sample.dart
  Widget _threeCircle() {
    return  Container(
      width: 100.0,
      height: 450.0,
      child: Stack(
        children: <Widget>[
          Positioned(
            top: 180.0, // This is important
            child: Container(
              width: 100.0,
              height: 100.0,
              decoration: BoxDecoration(
                color: Colors.grey,
                borderRadius: BorderRadius.circular(100),
              ),
            )
          ),
          Positioned(
            top:0,
            child: Container(
              width: 100.0,
              height: 100.0,
              decoration: BoxDecoration(
                color: Colors.green,
                borderRadius: BorderRadius.circular(100),
              ),
            )
          ),
          Positioned(
            bottom:0,
            child: Container(
              width: 100.0,
              height: 100.0,
              decoration: BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.circular(100),
              ),
            )
          ),
        ],
      )
    );
  }

Step 2

Now you see that we made the UI using Stack and you also see that that one of Positioned widget has a value which doesn’t make much sense (top:180.0).

This value is there because this will helps us in doing animation. It’s a static value but we will use variable to set the position. That positional variable will update on touch and drag.

How to detect the drag behavior of the user?

You can use GestureDetector for this, it’s the best solution out there which can help us. You can read more about GestureDetector on Flutter docs website.

  Positioned(
    top: 180.0, // This is important
    GestureDetector(
      onTap: () {
        print('someone clicked on Container');
      },
      onPanUpdate: (DragUpdateDetails details) {
        print('someone dragging on the widget');
      },
      onPanEnd: (DragEndDetails details){
        print('someone lift the finger from the widget');
      }
      child: Container(
        width: 100.0,
        height: 100.0,
        decoration: BoxDecoration(
          color: Colors.grey,
          borderRadius: BorderRadius.circular(100),
        ),
      )
    ),
  )

Step 3

Let’s move towards combining all the above logic and build the final solution. It was very much important for me to teach you about the stack and gesture detector before moving to the actual solution.

Let’s see the signature of our Slide Button Widget. It will be having 3 required arguments and these are those

  • label1 : The first Label (Success)
  • label2 : The second Label (Fail)
  • onSelected : Callback for onSelect

Remember we will utilize these value while building the Widget but you can make it more dynamic and robust. Change the color change the shape and size.

slide_button.dart
class SlideButton extends StatefulWidget {
  final String label1;
  final String label2;
  final SlideEvent onSelected;
  SlideButton({
    @required this.label1,
    @required this.label2,
    @required this.onSelected,
  });
  @override
  _SlideButtonState createState() => _SlideButtonState();
}


class _SlideButtonState extends State<SlideButton> {

  double buttonPosition = 110.0;
  String event;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100.0,
      height: 340.0,
      child: Stack(
        children: <Widget>[
          Positioned(
            left: 0,
            right: 0,
            top: buttonPosition,
            child: GestureDetector(
              onPanUpdate: onPanUpdate, // Event Listener
              onPanEnd: onPanEnd,       // Event Listener
              child: Container(
                width: 100.0,
                height: 100.0,
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(100),
                  border: Border.all(width: 1,color: Colors.grey),
                ),
                child: Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Icon(Icons.keyboard_arrow_up),
                      Icon(Icons.keyboard_arrow_down),
                    ],
                  ),
                ),
              ),
            ),
          ),
          Positioned(
            top: 0,
            left: 0,
            right: 0,
            child: Container(
              width: 100.0,
              height: 80.0,
              decoration: BoxDecoration(color: Colors.green,borderRadius: BorderRadius.circular(100)),
              child: Center(
                  child: Text(
                '${widget.label1.toUpperCase()}', // I am utilizing the label text
                style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: Container(
              width: 100.0,
              height: 80.0,
              decoration: BoxDecoration(color: Colors.red,borderRadius: BorderRadius.circular(100)),
              child: Center(
                  child: Text(
                  '${widget.label2.toUpperCase()}', // I am utilizing the label text
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  // You should focus here because all the logical part will come here.

  void onPanUpdate(DragUpdateDetails details) {

  }

  void onPanEnd(DragEndDetails details) {

  }
}

The above code is too much but if you have gone through step-1 and step-2 then this looks much simpler to you. Everything I used in the above code is from previous steps.

Step 3

Let’s write the logic and start the animation based on the user interaction. I am working on some constant value which means this logic will work properly with this size of screen only. To make it multipurpose you need to use the value or range of value in percentage.

  • onPanUpdate When user moves button by swiping up or down we want button to move.
  • onPanEnd When user raise his finger from the button, we expect the button to be set at default position.
**partial-slide_button.dart

  void onPanUpdate(DragUpdateDetails details) {
    setState(() {
      buttonPosition += details.delta.dy;
      if (buttonPosition < 30) {
        buttonPosition = 30;
      } else if (buttonPosition > 180) {
        buttonPosition = 190;
      }

      if (buttonPosition < 80) {
        event = widget.label1;  // setting label 1 as selected item
      } else if (buttonPosition > 140) {
        event = widget.label2;  // setting label 2 as selected item
      } else {
        event = null;
      }

    });
  }

  void onPanEnd(DragEndDetails details) {
    setState(() {
      buttonPosition = 110;
      widget.onSelected(event); // sending selected label value to onSelected
    });
  }

onPanEnd is easy and doesn’t require any explanation because it is setting the value 110 and updating the state. It is also sending a callback to the user with the selected option label1/ label2/ null.

Full Code

main.dart

import 'package:flutter/material.dart';
import 'slide_button.dart';

void main(){
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}


class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor:Colors.black,
      body: Center(
        child: SlideButton(
        label1:'Success',
        label2:'Fail',
        onSelected :(label){}
        ),
      ),
    );
  }
}

slide_button.dart
import 'package:flutter/material.dart';

typedef SlideEvent = void Function(String selectedLabel);

class SlideButton extends StatefulWidget {
  final String label1;
  final String label2;
  final SlideEvent onSelected;
  SlideButton({
    @required this.label1,
    @required this.label2,
    @required this.onSelected,
  });
  @override
  _SlideButtonState createState() => _SlideButtonState();
}

class _SlideButtonState extends State<SlideButton> {
  List<Color> gradient = [
    Colors.red,
    Colors.grey,
  ];
  double buttonPosition = 110.0;
  String event;
  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: Duration(milliseconds: 300),
      curve: Curves.easeInOut,
      width: 100.0,
      height: 340.0,
      padding: EdgeInsets.all(10),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topRight,
          end: Alignment.bottomLeft,
          colors: gradient,
        ),
        borderRadius: BorderRadius.circular(100),
      ),
      child: Stack(
        children: <Widget>[
          Positioned(
            left: 0,
            right: 0,
            top: buttonPosition,
            child: GestureDetector(
              onPanUpdate: onPanUpdate,
              onPanEnd: onPanEnd,
              child: Container(
                width: 100.0,
                height: 100.0,
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(100),
                  border: Border.all(
                    width: 1,
                    color: Colors.grey,
                  ),
                ),
                child: Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Icon(
                        Icons.keyboard_arrow_up,
                      ),
                      Icon(
                        Icons.keyboard_arrow_down,
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
          Positioned(
            top: 0,
            left: 0,
            right: 0,
            child: Container(
              width: 100.0,
              height: 80.0,
              decoration: BoxDecoration(
                color: Colors.green,
                borderRadius: BorderRadius.circular(100),
              ),
              child: Center(
                  child: Text(
                '${widget.label1.toUpperCase()}',
                style: TextStyle(
                  color: Colors.white,
                ),
              )),
            ),
          ),
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: Container(
              width: 100.0,
              height: 80.0,
              decoration: BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.circular(100),
              ),
              child: Center(
                  child: Text(
                '${widget.label2.toUpperCase()}',
                style: TextStyle(
                  color: Colors.white,
                ),
              )),
            ),
          ),
        ],
      ),
    );
  }

  void onPanUpdate(DragUpdateDetails details) {
    setState(() {
      buttonPosition += details.delta.dy;
      if (buttonPosition < 30) {
        buttonPosition = 30;
      } else if (buttonPosition > 180) {
        buttonPosition = 190;
      }

      if (buttonPosition < 80) {
        gradient = [
          Colors.green,
          Colors.white,
        ];
        event = widget.label1;
      } else if (buttonPosition > 140) {
        gradient = [
          Colors.white,
          Colors.red,
        ];
        event = widget.label2;
      } else {
        gradient = [
          Colors.white,
          Colors.white,
        ];
        event = null;
      }
    });
  }

  void onPanEnd(DragEndDetails details) {
    setState(() {
      gradient = [
        Colors.white,
        Colors.white,
      ];
      buttonPosition = 110;
      widget.onSelected(event);
    });
  }
}