Cards stack like Tinder application in Android

    Card stack is an exciting UI in mobile development, it provides an intuitive view of the "paper stack". There are a lot of applications in Google Play have this design, Tinder is a typical example.
    In Android, each card stack element will be created by using CardView - a class from Design Support Library - but in order to make whole view look like a stack, we must use a third-party library which provide a custom view to hold data and
    Through this post, I would like to present a library called SwipeStack which developed by Frederik Schweiger. It's can help us to build a swipe cards stack easily through some simple steps.
    DEMO VIDEO:

Import library to Android Studio Project

    The simplest way to use this library is add this dependency to dependencies scope of your app-level build.gradle:
compile 'link.fls:swipestack:0.3.0'

Declaring in Activity/Fragment layout

    The class used to make cards stack layout is SwipeStack, you must put an instance to your activity/fragment layout (xml) file like this:
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:weightSum="1">

    <link.fls.swipestack.SwipeStack
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="500dp"
        app:stack_rotation="0" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true">

        <TextView
            android:id="@+id/empty"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true" />

        <ImageView
            android:id="@+id/love"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_marginLeft="10dp"
            android:layout_toRightOf="@id/empty"
            android:contentDescription="@null"
            android:src="@drawable/love" />

        <ImageView
            android:id="@+id/cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:layout_marginRight="10dp"
            android:layout_toLeftOf="@id/empty"
            android:contentDescription="@null"
            android:src="@drawable/cancel" />
    </RelativeLayout>

</RelativeLayout>
    For more customizing XML attributes (like stack_rotation, swipe_rotation,...) of SwipeStack, please view this entry of it's library page.

Creating an adapter class

    SwipeStack work like a ListView, so you must make an adapter class (based on BaseAdapter) which holds the data and creates the views for the stack. In this sample project, I will load a Bitmap to each stack element so I use decodeSampledBitmapFromResource() method to scaled down version to memory, avoid OutOfMemory error.
    Source code of this adapter class:
CardsAdapter.java
package info.devexchanges.cardsstack;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class CardsAdapter extends BaseAdapter {

    private Activity activity;
    private final static int AVATAR_WIDTH = 150;
    private final static int AVATAR_HEIGHT = 300;
    private List<CardItem> data;

    public CardsAdapter(Activity activity, List<CardItem> data) {
        this.data = data;
        this.activity = activity;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public CardItem getItem(int position) {
        return data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        LayoutInflater inflater = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        // If holder not exist then locate all view from UI file.
        if (convertView == null) {
            // inflate UI from XML file
            convertView = inflater.inflate(R.layout.item_card, parent, false);
            // get all UI view
            holder = new ViewHolder(convertView);
            // set tag for holder
            convertView.setTag(holder);
        } else {
            // if holder created, get tag from view
            holder = (ViewHolder) convertView.getTag();
        }

        //setting data to views
        holder.name.setText(getItem(position).getName());
        holder.location.setText(getItem(position).getLocation());
        holder.avatar.setImageBitmap(decodeSampledBitmapFromResource(activity.getResources(),
                getItem(position).getDrawableId(), AVATAR_WIDTH, AVATAR_HEIGHT));

        return convertView;
    }

    private class ViewHolder{
        private ImageView avatar;
        private TextView name;
        private TextView location;

        public ViewHolder(View view) {
            avatar = (ImageView)view.findViewById(R.id.avatar);
            name = (TextView)view.findViewById(R.id.name);
            location = (TextView)view.findViewById(R.id.location);
        }
    }

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }
}

Configuration in Activity/Fragment java code

    We've done the most important work: building an adapter class for SwipeStack. Now, paying attention to some of it's necessary methods:
  • Handling swipe event of Cards stack by use setListener(SwipeStackListener()) method and overriding 3 methods onStackEmpty(), onViewSwipedToLeft(), onViewSwipedToRight() of SwipeStack.SwipeStackListener interface.
  • swipeTopViewToRight()/swipeTopViewToLeft(): programmatically dismiss the top view to the right/left.
  • resetStack(): Resets the current adapter position and repopulates the stack.
    Source code for the activity:
MainActivity.java
package info.devexchanges.cardsstack;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

import java.util.ArrayList;

import link.fls.swipestack.SwipeStack;

public class MainActivity extends AppCompatActivity {

    private SwipeStack cardStack;
    private CardsAdapter cardsAdapter;
    private ArrayList<CardItem> cardItems;
    private View btnCancel;
    private View btnLove;
    private int currentPosition;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cardStack = (SwipeStack) findViewById(R.id.container);
        btnCancel = findViewById(R.id.cancel);
        btnLove = findViewById(R.id.love);

        setCardStackAdapter();
        currentPosition = 0;

        //Handling swipe event of Cards stack
        cardStack.setListener(new SwipeStack.SwipeStackListener() {
            @Override
            public void onViewSwipedToLeft(int position) {
                currentPosition = position + 1;
            }

            @Override
            public void onViewSwipedToRight(int position) {
                currentPosition = position + 1;
            }

            @Override
            public void onStackEmpty() {

            }
        });

        btnCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                cardStack.swipeTopViewToRight();
            }
        });

        btnLove.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "You liked " + cardItems.get(currentPosition).getName(),
                        Toast.LENGTH_SHORT).show();
                cardStack.swipeTopViewToLeft();
            }
        });
    }

    private void setCardStackAdapter() {
        cardItems = new ArrayList<>();

        cardItems.add(new CardItem(R.drawable.a, "Huyen My", "Hanoi"));
        cardItems.add(new CardItem(R.drawable.f, "Do Ha", "Nghe An"));
        cardItems.add(new CardItem(R.drawable.g, "Dong Nhi", "Hue"));
        cardItems.add(new CardItem(R.drawable.e, "Le Quyen", "Sai Gon"));
        cardItems.add(new CardItem(R.drawable.c, "Phuong Linh", "Thanh Hoa"));
        cardItems.add(new CardItem(R.drawable.d, "Phuong Vy", "Hanoi"));
        cardItems.add(new CardItem(R.drawable.b, "Ha Ho", "Da Nang"));

        cardsAdapter = new CardsAdapter(this, cardItems);
        cardStack.setAdapter(cardsAdapter);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.reset) {
            cardStack.resetStack();
        }
        return super.onOptionsItemSelected(item);
    }
}

Some necessary files

    The POJO class of project, the model of a stack view element:
CardItem.java
package info.devexchanges.cardsstack;

public class CardItem {

    private int drawableId;
    private String name;
    private String location;

    public CardItem(int drawableId, String name, String location) {
        this.drawableId = drawableId;
        this.name = name;
        this.location = location;
    }

    public int getDrawableId() {
        return drawableId;
    }

    public void setDrawableId(int drawableId) {
        this.drawableId = drawableId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLocation() {
        return location;
    }
}
    The menu file, containing a label to reset data of the stack:
res/menu/menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MyActivity">
    <item
        android:id="@+id/reset"
        android:title="Reset"
        app:showAsAction="always" />
</menu>
    Running this application, you'll have this output:

Conclusions

    Up to now, you may realized that we are able to make a cards stack with swipe animation like Tinder application easily by using a third-party library. By searching on the Internet, you may find out a lot similar libraries:
    Finally, for more details, please go to the library page on @Github to read it's document and post your issues!

Share


Previous post
« Prev Post
Next post
Next Post »