Make a great Android UX: How to make a swipe button

Design not about making something beautiful, it is about making something great. You can do a great app by many ways, but one of my favorites is to make it the simplest and intuitive as possible, yet being original. It is important for your UI to respond to the user accordingly to importance of a certain action. Sometimes this action is too much important to be triggered by a simple click, like unlock your smartphone for example. Click in a button and receive a confirm dialog is ok. It works. But everybody does that! What can't we make something different!?

Well, today we will.

In this tutorial I am going to show how to make a swipe button. It allows the user to make an important action without necessarily having to confirm his/her intentions, because it is hard to swipe the button by accident (But please remember: It is not impossible. So take care!). So you have a much more intuitive, nicer and simpler UX.

Cool right? So let's go!

This tutorial is based in this repository: https://github.com/ebanx/swipe-button/ . If you prefer to just use the button, you can simply add it in your app using Gradle.

Part 1 — Let's define our xml

from giphy

Our button has a circle moving part and a rounded background. So let's make those parts:

The circle, shape_button.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="50dp" />
<solid android:color="@android:color/white" />
</shape>
</item>
</selector>

And the background, shape_rounded.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:state_selected="false">
<shape android:shape="rectangle">
<corners android:radius="150dp" />
<solid android:color="#4D6070" />
</shape>
</item>
</selector>

The radius can be any number that is very, so the borders are a semi-circle. You can use different colors if your prefer.

So now we have the xml that we next. Let's start making our custom view.

Part 2 — Starting to make a custom view

We are going to extend RelativeLayout, like this:

public class SwipeButton extends RelativeLayout {
private ImageView slidingButton;
private float initialX;
private boolean active;
private int initialButtonWidth;
private TextView centerText;

private Drawable disabledDrawable;
private Drawable enabledDrawable;
public SwipeButton(Context context) {
super(context);

init(context, null, -1, -1);
}

public SwipeButton(Context context, AttributeSet attrs) {
super(context, attrs);

init(context, attrs, -1, -1);
}

public SwipeButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

init(context, attrs, defStyleAttr, -1);
}

@TargetApi(21)
public SwipeButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
//Don't worry about this method right now... I'm going to talk about this latter.
}

So you must me thinking… "This is a button, why are we extending a RelativeLayout??"

Calm down! We are going to figure this out!

As this is a complex button, we need to to add many views to be able to accomplish the behavior we need, so we need a ViewGroup. RelativeLayout is a good option because many views will overlap in this component.

We have many inner views. So let's split this implementation in steps.

It also important for you the understand the variables of this class:

slidingButton -> This is the moving part of the component. It contains the icon.

initialX -> It is the position of the moving part when the user is going to start to move it.

active -> It is the variable that says that the button is active or not.

initialButtonWidth -> This the initial width of the moving part. We need to save it so we can morph back to the initial position.

centerText ->The text in the center of the button.

disabledDrawable / enabledDrawable -> The icons that the moving part is going to hold when the button is enabled or disabled. Use the icon that you prefer.

The background

First, we add a background

private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
RelativeLayout background = new RelativeLayout(context);

LayoutParams layoutParamsView = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

layoutParamsView.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);

background.setBackground(ContextCompat.getDrawable(context, R.drawable.shape_rounded));

addView(background, layoutParamsView);

This is the view responsible to the rounded background of the button.

The text

Now we have to add a informative text in the button

inal TextView centerText = new TextView(context);
this.centerText = centerText;

LayoutParams layoutParams = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
centerText.setText("SWIPE"); //add any text you need
centerText.setTextColor(Color.WHITE);
centerText.setPadding(35, 35, 35, 35);
background.addView(centerText, layoutParams);

This is the text in the center of the component. I am hard coding the text for simplicity, but you can use string resources for internationalization. You can add the padding that you prefer. That is the way you control the size of your component background.

Add the moving icon

final ImageView swipeButton = new ImageView(context);
this.slidingButton = swipeButton;

disabledDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_lock_open_black_24dp);
enabledDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_lock_outline_black_24dp);

slidingButton.setImageDrawable(disabledDrawable);
slidingButton.setPadding(40, 40, 40, 40);

LayoutParams layoutParamsButton = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

layoutParamsButton.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
layoutParamsButton.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
swipeButton.setBackground(ContextCompat.getDrawable(context, R.drawable.shape_button));
swipeButton.setImageDrawable(disabledDrawable);
addView(swipeButton, layoutParamsButton);

This is the moving part of the component. We have to set the icons for the disabled and enabled states. Setting the padding is the way to set the size of the moving part of the component.

Add a touch listner

setOnTouchListener(getButtonTouchListener());

Add this line add the end of init method. We are going to see more about this in the next section. Then, the init is going to be:

private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
RelativeLayout background = this;

LayoutParams layoutParamsView = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

layoutParamsView.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);

background.setBackground(ContextCompat.getDrawable(context, R.drawable.shape_rounded));
addView(background, layoutParamsView); final TextView centerText = new TextView(context);
this.centerText = centerText;
centerText.setGravity(Gravity.CENTER);

LayoutParams layoutParams = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
centerText.setText("SWIPE"); //add any text you need
centerText.setTextColor(Color.WHITE);
background.addView(centerText, layoutParams); final ImageView swipeButton = new ImageView(context);
this.slidingButton = swipeButton;

LayoutParams layoutParamsButton = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);

layoutParamsButton.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
layoutParamsButton.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
swipeButton.setBackground(ContextCompat.getDrawable(context, R.drawable.shape_button)); swipeButton.setImageDrawable(disabledDrawable); addView(swipeButton, layoutParamsButton); setOnTouchListener(getButtonTouchListener());
}

So right now you can add this button in your app and see the result. Like this:

<your.package.here.SwipeButton
android:id="@+id/swipe_btn"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
/>

The button will look like this:

Nice!

Ok this is looking good so far. But we not behavior yet. In the next section, you will some logic to your component!

Part 3 — Implementing the button login

Let's implment the getButtonTouchListener.

private OnTouchListener getButtonTouchListener() {
return new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
//Movement logic here
return true;
case MotionEvent.ACTION_UP:
//Release logic here
return true;
}

return false;
}
};
}

Now we have our listener. Let's add the logic inside the ACTION_MOVE case.

Movement logic

if (initialX == 0) {
initialX = slidingButton.getX();
}
if (event.getX() > initialX + slidingButton.getWidth() / 2 &&
event.getX() + slidingButton.getWidth() / 2 < getWidth()) {
slidingButton.setX(event.getX() - slidingButton.getWidth() / 2);
centerText.setAlpha(1 - 1.3f * (slidingButton.getX() + slidingButton.getWidth()) / getWidth());
}

if (event.getX() + slidingButton.getWidth() / 2 > getWidth() &&
slidingButton.getX() + slidingButton.getWidth() / 2 < getWidth()) {
slidingButton.setX(getWidth() - slidingButton.getWidth());
}

if (event.getX() < slidingButton.getWidth() / 2 &&
slidingButton.getX() > 0) {
slidingButton.setX(0);
}

Let's explain those IFs.

The first one is just to make sure that we know the initial position of the button, in case it is not zero.

The second IF is the one responsible to make the button follow the finger of the user. event.getX() returns the positions of the current touch. In this part of the code, we set the center of the button in the position of the touch and we decrease the alpha of the text as the swipe increases.

The third and the fourth IFs are responsible to set the moving part position to the limits of the component when the user swipes outside of the limits.

Release logic

In the release, we need three methods:

case MotionEvent.ACTION_UP:
if (active) {
collapseButton();
} else {
initialButtonWidth = slidingButton.getWidth();

if (slidingButton.getX() + slidingButton.getWidth() > getWidth() * 0.85) {
expandButton();
} else {
moveButtonBack();
}
}

return true;
}

If the button is released and it is active, we need it to collapse. If the button is released very close to the final of the swipe, then need it to expand. If the button is release far away from the right edge, it must animate the way back.

Those are the animations:

expand animation

private void expandButton() {
final ValueAnimator positionAnimator =
ValueAnimator.ofFloat(slidingButton.getX(), 0);
positionAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float x = (Float) positionAnimator.getAnimatedValue();
slidingButton.setX(x);
}
});


final ValueAnimator widthAnimator = ValueAnimator.ofInt(
slidingButton.getWidth(),
getWidth());

widthAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ViewGroup.LayoutParams params = slidingButton.getLayoutParams();
params.width = (Integer) widthAnimator.getAnimatedValue();
slidingButton.setLayoutParams(params);
}
});


AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);

active = true;
slidingButton.setImageDrawable(enabledDrawable);
}
});

animatorSet.playTogether(positionAnimator, widthAnimator);
animatorSet.start();
}

This animation is a bit complicated right? Don't worry, you got this! Let me explain:

First, the slidingButton is going to animate do the left edge of our component. That's the positionAnimator.

Second, the button must expand and occupy the entire space of the component. That the widthAnimator.

Third, we create an AnimatorSet so we can play the animations together. At the end of the animation, we set the state of the button as active and change the image of the button.

Collapse animation

private void collapseButton() {
final ValueAnimator widthAnimator = ValueAnimator.ofInt(
slidingButton.getWidth(),
initialButtonWidth);

widthAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ViewGroup.LayoutParams params = slidingButton.getLayoutParams();
params.width = (Integer) widthAnimator.getAnimatedValue();
slidingButton.setLayoutParams(params);
}
});

widthAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
active = false;
slidingButton.setImageDrawable(disabledDrawable);
}
});

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(
centerText, "alpha", 1);

AnimatorSet animatorSet = new AnimatorSet();

animatorSet.playTogether(objectAnimator, widthAnimator);
animatorSet.start();
}

This animation is similar to the expand button, but this time we increase the alpha of the text so it can become visible again.

Move the button back

private void moveButtonBack() {
final ValueAnimator positionAnimator =
ValueAnimator.ofFloat(slidingButton.getX(), 0);
positionAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
positionAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float x = (Float) positionAnimator.getAnimatedValue();
slidingButton.setX(x);
}
});

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(
centerText, "alpha", 1);

positionAnimator.setDuration(200);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(objectAnimator, positionAnimator);
animatorSet.start();
}

This animation move the button back to the left edge and recovers the alpha of the component text.

Part 4 — The result

If you did everything right you will get this (use a dark backgound in your activity):

And that's it, you made it!

Congrats!

If you enjoyed reading this, please click the heart icon below. This will help to share the story with others. If you would like to see more content like this one, follow me.

Software developer at GetStream.io — I love Android =]