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 andThrough 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
dependencies
scope of your app-level build.gradle:
compile 'link.fls:swipestack:0.3.0'
Declaring in Activity/Fragment layout
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
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 methodsonStackEmpty()
,onViewSwipedToLeft()
,onViewSwipedToRight()
ofSwipeStack.SwipeStackListener
interface. swipeTopViewToRight()
/swipeTopViewToLeft()
: programmatically dismiss the top view to the right/left.resetStack()
: Resets the current adapter position and repopulates the stack.
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
CardItem.java
The menu file, containing a label to reset data of the stack:
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;
}
}
res/menu/menu_main.xml
Running this application, you'll have this output:
<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>
Conclusions
Finally, for more details, please go to the library page on @Github to read it's document and post your issues!