Android Basic Training Course: Creating a StackView

Android Basic Training Course: Creating a StackView

    Stack is a common design that we can see at fronted-end programming including mobile app development. When using Android phone, you can realize that showing "recent apps" is a type of stack view design:
    This design is very diverse but by this post, just about "Android basic development", I would like to talk about using StackView widget. Like ListView or GridView, it's a subclass of AdapterView, so it has the same operation mechanism (adapter of a "list" of object). Now, following this tutorial step by step, you will understand the way to use it!

Preparing some drawables resource

    The fact that stack view is usually used for display "stack of images" or something that similarity. So, after start a new Android Studio project, please add some own images to drawable folder. Make sure that these images have the same size:

Put StackView to your Activity layout

    Just including a StackView in your activity layout file like this:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="info.devexchanges.stackview.MainActivity">

    <StackView
        android:id="@+id/stack_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>

Create layout for each StackView item

    As noted above, Stack view is usually used for display list of images, so I will make a layout which contains an ImageView to show a drawable resource and a TextView to present it's name for each StackView item like this:
item_stack.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imageView"
        android:contentDescription="@null"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/image_1"
        android:layout_gravity="center_horizontal" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/activity_horizontal_margin"
        android:textColor="@android:color/holo_blue_dark"
        android:textStyle="bold" />

</LinearLayout>

Create a POJO class

    Java object for each Stack view item:
StackItem.java
package info.devexchanges.stackview;

public class StackItem {

    private int drawableId;
    private String imageName;

    public StackItem(int id, String imageName) {
        this.imageName = imageName;
        this.drawableId = id;
    }

    public String getImageName() {
        return imageName;
    }

    public int getDrawableId() {
        return drawableId;
    }
}

Create a StackView adapter class

    This is the most important step of this tutorial. Like ListView, we can create a subclass of ArrayAdapter or BaseAdapter to build the adapter class which holding and displaying data. I also provide a ViewHolder to make convertView clearly:
StackAdapter.java
package info.devexchanges.stackview;

import android.annotation.SuppressLint;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.List;

public class StackAdapter extends ArrayAdapter<StackItem> {

    private List<StackItem> items;
    private Context context;

    public StackAdapter(Context context, int textViewResourceId, List<StackItem> objects) {
        super(context, textViewResourceId, objects);
        this.items = objects;
        this.context = context;
    }

    @NonNull
    @SuppressLint("InflateParams")
    public View getView(int position, View convertView, @NonNull ViewGroup parent) {
        ViewHolder holder; //initialize a null ViewHolder object
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.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_stack, 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();
        }
        StackItem stackItem = items.get(position);

        //set stack item data to views
        holder.image.setImageResource(stackItem.getDrawableId());
        holder.imageName.setText(stackItem.getImageName());

        return convertView;
    }

    private class ViewHolder {
        private TextView imageName;
        private ImageView image;

        public ViewHolder(View view) {
            this.image = (ImageView) view.findViewById(R.id.imageView);
            this.imageName = (TextView) view.findViewById(R.id.textView);
        }
    }
}

Activity programmatically code

    There is nothing special in the main activity code. Just initializing an adapter with available data and attaching it to the StackView:
MainActivity.java
package info.devexchanges.stackview;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.StackView;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private final int[] IMAGE_IDs = {R.drawable.image_1, R.drawable.image_2, R.drawable.image_3,
            R.drawable.image_4, R.drawable.image_5, R.drawable.image_6};

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

        StackView stackView = (StackView) findViewById(R.id.stack_view);

        ArrayList<StackItem> stackItems = new ArrayList<>();
        for (int imageId : IMAGE_IDs) {
            String name = getResources().getResourceEntryName(imageId);
            stackItems.add(new StackItem(imageId, name));
        }

        StackAdapter stackAdapter = new StackAdapter(this, R.layout.item_stack, stackItems);
        stackView.setAdapter(stackAdapter);
    }
}

Running the application

    Our output will be like this:

The weakness of the "default" StackView

    This "official" widget of Android SDK has the biggest weakness is swipe listener is absent, detect this user action is not a easily. Selected item event still not working well, either. I recommend to use a third-party library to make this layout better in programming. These are some of them:
I also had a post about creating Swipe Card Stack, read this tutorial HERE!




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!