time

About RecyclerView in Android

Contents

Introduction

The RecyclerView class, delivered as part of the v7 support library suite, is an improved version of the ListView and the GridView classes provided by the Android framework.

In other words, the RecyclerView is a new ViewGroup that is prepared to render any adapter-based view in a similar way.

The RecyclerView widget is a more advanced and flexible version of ListView. This widget is a container for displaying large data sets that can be scrolled very efficiently by maintaining a limited number of views. It requires you to use the ViewHolder pattern which improves performance as you avoid initializing views every time.

One advantage over ListView or GridView is the built in support for animations as items are added, removed, or repositioned. Moreover, respect to ListView, RecyclerView is much more customizable.

Unlike the ListView, the RecyclerView also provides a choice of three built-in layout managers to control the way in which the list items are presented to the user:

  • LinearLayoutManager - the list items are presented as either a horizontal or vertical scrolling list.
  • GridLayoutManager - the list items are presented in grid format. This manager is best used when the list items of are of uniform size.
  • StaggeredGridLayoutManager - the list items are presented in a staggered grid format. This manager is best used when the list items are not of uniform size.

For situations where none of the three built-in managers provide the necessary layout, custom layout managers may be implemented by subclassing the RecyclerView.LayoutManager class.

The implementation of RecyclerView requires a few classes to be implemented. The most important classes are listed in the following table.

Class Purpose Optional
Adapter Provides the data and responsible for creating the views for the individual entry. Required
ViewHolder Contains references for all views that are filled by the data of the entry Required
LayoutManager Contains references for all views that are filled by the data of the entry Required, but default implementations available
ItemDecoration Responsible for drawing decorations around or on top of the view container of an entry Default behavior, but can be overridden
ItemAnimator Responsible to define the animation if entries are added, removed or reordered Default behavior, but can be overridden
android_recyclerview_scheme.png

Comparison between RecyclerView and ListView

There are a lots of new features in RecyclerView that are not present in existing ListView. The RecyclerView is more flexible, powerful and a major enhancement over ListView. Here I will try to give you a detailed insight into it.

  1. Custom Item Layouts. ListView can only layout the items in Vertical Arrangement and that arrangement cannot be customized according to our requirements. Suppose we need to create a horizontal list then that thing is not feasible with default ListView. But with introduction of Recyclerview we can easily create a horizontal or vertical List. By using LayoutManager component of RecyclerView we can easily define the orientation of items.
  2. Use Of ViewHolder Pattern. ListView adapters do not require the use of ViewHolder but RecyclerView require the use of ViewHolder that is used to store the reference of view’s. In ListView it is recommended to use the ViewHolder but it is not compulsion but in RecyclerView it is mandatory to use ViewHolder which is the main difference between RecyclerView and ListView. ViewHolder is a static inner class in our Adapter which holds references to the relevant view’s. By using these references our code can avoid time consuming findViewById() method to update the widgets with new data.
  3. Adapters. In ListView we use many adapter‘s like ArrayAdapter for displaying simple array data, BaseAdapter and SimpleAdapters for custom lists. In RecyclerView we only use RecyclerView.Adapter to set the data in list.
  4. Item Animator. ListView are lacking in support of good animation. RecyclerView brings a new dimensions in it. By using RecyclerView.ItemAnimator class we can easily animate the view.
  5. Item Decoration. In ListView dynamically decorating items like adding divider or border was not easy but in RecyclerView by using RecyclerView.ItemDecorator class we have a huge control on it.

Example of RecyclerView

Our plan is

  1. Create a new project using your Android Studio.
  2. Add RecylerView and CardView dependencies to build.gradle (Module:app).
  3. Add RecylerView to our activity_main.xml and complete this layout.
  4. Create a new layout for single row of RecyclerView using CardView.
  5. Create a RecylerViewHolder java class for Recyclerview by extending RecyclerView.ViewHolder.
  6. Create an RecylerAdapter java class for Recyclerview by extending RecyclerView.Adapter.
  7. Set RecylerView in MainActivity.

1. Open your Android Studio and create a new project

2. Go to GradleScripts > build.gradle (Module:app) and add below libraries to this file under dependencies block. My dependencies block is

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.0.1'
    compile 'com.android.support:recyclerview-v7:25.0.1'
    compile 'com.android.support:cardview-v7:25.0.1'
}

3. Open your activity_main.xml file and add Recyclerview to it

<?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"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rcView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:paddingBottom="16dp"
        android:paddingTop="16dp"
        android:scrollbars="vertical"
         />

</RelativeLayout>

4. Create a new layout for single row of Recyclerview using CardView.

CardView widget can be used to create simple cards. CardView extends the FrameLayout class and lets you show information inside cards that have a consistent look across the platform. CardView widgets can have shadows and rounded corners.

For single row of RecyclerView I am going to make a simple layout with one ImageView and two text for title and description. The complete layout is wrapped inside CardView to give it some good look. Here is my item_list.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:layout_marginBottom="8dp"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:background="#C5CAE9"
    android:foreground="?attr/selectableItemBackground"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <RelativeLayout
        android:layout_width="match_parent"
        android:gravity="center"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/avatar"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_centerVertical="true"
            android:layout_alignParentLeft="true"
            android:layout_marginLeft="10dp"
            android:scaleType="centerCrop"
            android:src="@android:drawable/star_big_on" />

        <TextView
            android:id="@+id/title"
            android:layout_centerVertical="true"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_toRightOf="@+id/avatar"
            android:text="Title"
            android:textColor="#000000"
            android:textAppearance="?attr/textAppearanceListItem"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/desc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/title"
            android:layout_marginLeft="16dp"
            android:layout_toRightOf="@+id/avatar"
            android:textColor="#000000"
            android:ellipsize="end"
            android:singleLine="true"
            android:text="Movie from IMDB 250"
            android:textAppearance="?attr/textAppearanceListItem"
            android:textSize="14sp" />
    </RelativeLayout>
</android.support.v7.widget.CardView>

5. Create a RecylerViewHolder java class for RecyclerView by extending RecyclerView.ViewHolder.

RecylerView uses a ViewHolder to store references to the views for one entry in the RecylerView. A ViewHolder class is typically a static inner class in your adapter which holds references to the relevant views. With these references your code can avoid the findViewById() method in an adapter to find the views which should be filled with your new data. This pattern avoids looking up the UI components all the time the system shows a row in the list and this is approximately 15% faster then using the findViewById() method.

Right click on your package and create a new java class name it RecyclerViewHolder and extends RecyclerView.ViewHolder. In this define two TextView and an ImageView of item_list.xml file. Copy and paste below code to your RecyclerViewHolder class. Here is my complete code of RecyclerViewHolder

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

public class RecyclerViewHolder extends RecyclerView.ViewHolder {
    TextView tv1, tv2;
    ImageView imageView;

    public RecyclerViewHolder(View itemView) {
        super(itemView);
        tv1 = (TextView) itemView.findViewById(R.id.title);
        tv2 = (TextView) itemView.findViewById(R.id.desc);
        imageView = (ImageView) itemView.findViewById(R.id.avatar);
    }
}

6. Create an RecylerAdapter java class for RecyclerView by extending RecyclerView.Adapter.

The adapter is a component that stands between the data model we want to show in our app UI and the UI component that renders this information. In other words, an adapter guides the way the information are shown in the UI.

To connect data set and view we need an adapter as you have already used adapter in Listview. But in case of RecyclerView we are not going to extend any base adapter or array adapter like ListView. For RecyclerView we are going to extend RecyclerView.Adapter in our Adapter class like below code. Create an new java class by right click on your package and name it RecyclerAdapter and extend RecyclerView.Adapter it will ask you to implement three method onCreateViewHolder(), onBindViewHolder(), getItemCount().

  • getItemCount() This method return the number of items present in the data.
  • onCreateViewHolder() Inside this method we specify the layout that each item of the RecyclerView should use. This is done by inflating the layout using LayoutInflater, passing the output to the constructor of the custom ViewHolder.
  • onBindViewHolder() This method is very similar to the getView method of a ListView's adapter. In our example, here's where you have to set the String values to TextView.

Now we will complete some small things like we will make an array for title text. We will also make a context variable LayoutInflater and a constructor of Adapter class just copy and paste below code in your adapter class above all the methods.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerViewHolder>{

    String[] name = {"The Shawshank Redemption", "The Godfather", 
         "The Godfather: Part II", "The Dark Knight", "Schindler's List", "12 Angry Men", "Pulp Fiction"};
    Context context;
    LayoutInflater inflater;

    public RecyclerAdapter(Context context) {
        this.context = context;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = inflater.inflate(R.layout.item_list, parent, false);

        RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerViewHolder holder, int position) {
        holder.tv1.setText(name[position]);
        holder.imageView.setOnClickListener(clickListener);
        holder.imageView.setTag(holder);
    }

    View.OnClickListener clickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            RecyclerViewHolder vholder = (RecyclerViewHolder) v.getTag();
            int position = vholder.getPosition();
            Toast.makeText(context, "Position is " + position, Toast.LENGTH_LONG).show();
        }
    };

    @Override
    public int getItemCount() {
        return name.length;
    }
}

As you can see we inflate item_list.xml file inside onCreateViewHolder(). Also have made object of RecyclerViewHolder inside onCreateViewHolder and return this object like below code.

Inside onBindViewHolder() method we have set text to TextView from name array and also have made onClickListener for click on ImageView of row of RecyclerView.

And last step is to set number of items of RecyclerView so inside getItemCount() method we have returned number of items of name array.

7. Set RecylerView in MainActivity and complete everything.

Open your MainActivity.java and we will complete following steps

  • Define RecyclerView and register.
  • Make an object of RecyclerAdapter class and set Adapter to RecyclerView.
  • Set LayoutManager to RecyclerView in my case I am using LinearLayoutManager.

Here is complete code of MainActivity.java.

public class MainActivity extends AppCompatActivity {
    RecyclerView recyclerView;

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

        recyclerView = (RecyclerView) findViewById(R.id.rcView);

        RecyclerAdapter adapter = new RecyclerAdapter(this);
        recyclerView.setAdapter(adapter);
        recyclerView.setHasFixedSize(true);

        // layout manager for RecyclerView

        //int columns = 2;
        //GridLayoutManager lm = new GridLayoutManager(this, columns, 
        //       GridLayoutManager.VERTICAL, false);
        //StaggeredGridLayoutManager lm = new StaggeredGridLayoutManager(columns, 
        //       StaggeredGridLayoutManager.VERTICAL);
        //lm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);

        LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(lm);
    }
}

Result

android_recyclerview_demo.png

Code on github

Item click listener for RecyclerView

In this section we'll handle user clicks on RecyclerView items. In other words, we want to know if the an user clicks on a item and what type of click he is making: simple click or long click.

When we use RecyclerView the things are a little more complex than ListView. In this case, we have to create a class that implements RecyclerView.OnItemTouchListener.

Right click on your package and create a new java class with name RecyclerItemListener and extends RecyclerView.OnItemTouchListener.

public class RecyclerItemListener 
    implements RecyclerView.OnItemTouchListener  {

    private RecyclerTouchListener listener;
    private GestureDetector gd;

    public interface RecyclerTouchListener {
        public void onClickItem(View v, int pos);
        public void onLongClickItem(View v, int pos);
    }

    public RecyclerItemListener(Context ctx, final RecyclerView rv, 
                                final RecyclerTouchListener listener) {
        this.listener = listener;
        gd = new GestureDetector(ctx, 
             new GestureDetector.SimpleOnGestureListener() {
                 @Override
                 public void onLongPress(MotionEvent e) {
                   View v = rv.findChildViewUnder(e.getX(), e.getY());
                   listener.onLongClickItem(v, rv.getChildAdapterPosition(v));
                }

               @Override
               public boolean onSingleTapUp(MotionEvent e) {
                  View v = rv.findChildViewUnder(e.getX(), e.getY());
                  listener.onClickItem(v, rv.getChildAdapterPosition(v));
                  return true;
              }
         });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        View child = rv.findChildViewUnder(e.getX(), e.getY());
        return ( child != null && gd.onTouchEvent(e));
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {}

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
}

We define a callback interface to notify the listener when the user clicks on an item. The first thing is creating an instance of GestureDetector so that we can know when the user clicks and what click type he is doing.

Next, our class overrides onInterceptTouchEvent to know if the user selects and item and if the gesture detected is handled by our instance.

To receive notification, in the listener class we simply implements the interface declared above. Following snippet goes to onCreate of MainActivity

recyclerView.addOnItemTouchListener(new RecyclerItemListener(getApplicationContext(), 
    recyclerView, new RecyclerItemListener.RecyclerTouchListener() {
        public void onClickItem(View v, int pos) {
            Toast.makeText(getApplicationContext(), "Simple click", Toast.LENGTH_LONG).show();
        }

        public void onLongClickItem(View v, int position) {
            Toast.makeText(getApplicationContext(), "Long click", Toast.LENGTH_LONG).show();
        }
}));

Decorate item for RecyclerView

In this section we'll customize the RecyclerView and add a row divider.. To do it we have to implement a custom class that extends RecyclerView.ItemDecoration.

Right click on your package and create a new java class with name DividerItemDecoration and extends RecyclerView.ItemDecoration.

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private Drawable div;

    public DividerItemDecoration(Drawable div) {
       this.div = div;
    }

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + div.getIntrinsicHeight();
            div.setBounds(left, top, right, bottom);
            div.draw(canvas);
        }
    }
}

In this class, we override onDrawOver and implements our UI customizazion.

To set this decorator add following snippet to onCreate of MainActivity

recyclerView.addItemDecoration(
    new DividerItemDecoration(ContextCompat.getDrawable(getApplicationContext(), 
        R.drawable.item_decorator)));

Where item_decorator is defined under res/drawable directroy in this way

<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
      <size android:height="1dp" />
      <solid android:color="#FFB1BEC4" />
</shape>

Also we can add some margin to first item like so

public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
    private int offset;

    public ItemOffsetDecoration(int offset) {
        this.offset = offset;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view,
                               RecyclerView parent, RecyclerView.State state) {

        if (parent.getChildAdapterPosition(view) == 0) {
                outRect.right = offset;
                outRect.left = offset;
                outRect.top = offset;
                outRect.bottom = offset;
            }
        }
    }
}

And use with following statement

recyclerView.addItemDecoration(new ItemOffsetDecoration(20));

Infinite scroll

There are several ways to create infinite scroll in RecyclerView:

  1. Use UltimateRecyclerView as substitution for RecyclerView. Tutotial here.
  2. Use Paginate to extend RecyclerView functionality. Read about it below.
  3. Use PlaceHolderView as substitution for RecyclerView. Tutotial here.
  4. Implement setOnLoadMoreListener. Details here.

My list of dependencies are

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support:recyclerview-v7:23.3.0'
    compile 'com.android.support:cardview-v7:23.3.0'
    compile 'com.github.castorflex.smoothprogressbar:library-circular:1.2.0'
    compile 'com.github.markomilos:paginate:0.5.1'
}

Main layout activity_main.xml is the same.

MainActivity have updated to new version

public class MainActivity extends AppCompatActivity implements Paginate.Callbacks {
    RecyclerView recyclerView;

    private boolean loading = false;
    private int currentPage = 0;
    protected int threshold = 4;
    protected int totalPages = 10;
    private Handler handler = new Handler();
    private Paginate paginate;
    protected boolean addLoadingRow = true;
    protected boolean customLoadingListItem = false;
    Random rnd = new Random();

    RecyclerAdapter moviesAdapter;
    ArrayList movies;

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

        recyclerView = (RecyclerView) findViewById(R.id.rcView);
        LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(lm);


        recyclerView.addOnItemTouchListener(new RecyclerItemListener(getApplicationContext(), recyclerView,
            new RecyclerItemListener.RecyclerTouchListener() {
                public void onClickItem(View v, int position) {
                    Toast.makeText(getApplicationContext(), "On Click Item interface",
                            Toast.LENGTH_LONG).show();
                }

                public void onLongClickItem(View v, int position) {
                    Toast.makeText(getApplicationContext(), "On Long Click Item interface",
                            Toast.LENGTH_LONG).show();
                }
        }));

        setupPagination();
    }

    protected void setupPagination() {
        if (paginate != null) {
            paginate.unbind();
        }

        handler.removeCallbacks(fakeCallback);

        movies = new ArrayList<String>(Arrays.asList("The Shawshank Redemption", "The Godfather",
                "The Godfather: Part II", "The Dark Knight", "Schindler's List", "12 Angry Men",
                "Pulp Fiction"));

        moviesAdapter = new RecyclerAdapter(this, movies);
        recyclerView.setAdapter(moviesAdapter);
        recyclerView.setHasFixedSize(true);

        loading = false;
        currentPage = 0;

        paginate = Paginate.with(recyclerView, this)
            .setLoadingTriggerThreshold(threshold)
            .addLoadingListItem(addLoadingRow)
            .setLoadingListItemCreator(customLoadingListItem ? new CustomLoadingListItemCreator() : null)
            .build();
    }

    @Override
    public synchronized void onLoadMore() {
        loading = true;
        // fake asynchronous loading that will generate page of random data after some delay
        handler.postDelayed(fakeCallback, 1000);
    }

    @Override
    public synchronized boolean isLoading() {
        return loading; // return boolean weather data is already loading or not
    }

    @Override
    public boolean hasLoadedAllItems() {
        return currentPage == totalPages; // if all pages are loaded return true
    }

    private Runnable fakeCallback = new Runnable() {
        @Override
        public void run() {
            currentPage++;
            moviesAdapter.add("Movie " + String.valueOf(rnd.nextInt(100)));
            loading = false;
        }
    };

    private class CustomLoadingListItemCreator implements LoadingListItemCreator {
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            View view = inflater.inflate(R.layout.custom_loading_list_item, parent, false);
            return new LoadingHolder(view);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            LoadingHolder lh = (LoadingHolder) holder;
            lh.tvLoading.setText(String.format("Total items loaded: %d.\nLoading more...", moviesAdapter.getItemCount()));
        }
    }

    static class LoadingHolder extends RecyclerView.ViewHolder {
        TextView tvLoading;

        public LoadingHolder(View itemView) {
            super(itemView);
            tvLoading = (TextView) itemView.findViewById(R.id.tv_loading_text);
        }
    }
}

Note some key things:

  • We implemented Paginate.Callbacks interface for MainActivity.
  • Paginate.Callbacks interface requires three methods onLoadMore, isLoading, hasLoadedAllItems.
  • We realize fakeCallback method for generating new items.
  • Class CustomLoadingListItemCreator is responsible for loader view.

Layout for custom loader is

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10dp">

    <fr.castorflex.android.circularprogressbar.CircularProgressBar
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:indeterminate="true"
        app:cpb_color="#F00"
        app:cpb_max_sweep_angle="300"
        app:cpb_min_sweep_angle="10"
        app:cpb_rotation_speed="1.0"
        app:cpb_stroke_width="4dp"
        app:cpb_sweep_speed="1.0"/>

    <TextView
        android:id="@+id/tv_loading_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        tools:text="Loading..."/>
</LinearLayout>

Following is new RecyclerAdapter

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerViewHolder> {
    List<String> names;
    Context context;
    LayoutInflater inflater;

    @Override
    public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = inflater.inflate(R.layout.item_list, parent, false);

        RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
        return viewHolder;
    }

    public RecyclerAdapter(Context context, List<String> name) {
        this.context = context;
        this.names = name;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public void onBindViewHolder(RecyclerViewHolder holder, int position) {
        holder.tv1.setText(names.get(position));
        holder.imageView.setTag(holder);
    }

    @Override
    public int getItemCount() {
        return names.size();
    }

    public void add(String name) {
        int previousDataSize = names.size();
        names.add(name);
        //notifyDataSetChanged();
        notifyItemInserted(names.size());
    }
}

Layout for RecyclerView items and RecyclerViewHolder are the same.

Result

android_recyclerview_infinite_scroll.png

Grid via GridLayoutManager

Similar to displaying items as a list, displaying items as a grid is simple using RecyclerView. In this section we are going to display images and text as grid using RecyclerView.

Our main layout has only one RecyclerView widget in LinearLayout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rvItems"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="1dp"
        />

</LinearLayout>

The next layout is for RecylerView grid item. It has a Textview child widget to display some text.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_margin="2dp">

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"/>

</LinearLayout>

Following is MainActivity with RecyclerView.Adapter and RecyclerViewHolder.

public class MainActivity extends AppCompatActivity {
    public class GridAdapter extends RecyclerView.Adapter<GridAdapter.RecyclerViewHolder>{
        Context context;
        LayoutInflater inflater;
        String[] items;
        Random rnd = new Random();

        public class RecyclerViewHolder extends RecyclerView.ViewHolder {
            TextView tvTitle;

            public RecyclerViewHolder(View view) {
                super(view);
                tvTitle = (TextView) view.findViewById(R.id.tvTitle);
            }
        }

        public GridAdapter (Context context, String[] items) {
            this.context = context;
            this.items = items;
            inflater = LayoutInflater.from(context);
        }

        @Override
        public GridAdapter.RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = inflater.inflate(R.layout.grid_item, parent, false);
            GridAdapter.RecyclerViewHolder viewHolder = new GridAdapter.RecyclerViewHolder(v);
            return viewHolder;
        }

        @Override
        public void onBindViewHolder(GridAdapter.RecyclerViewHolder holder, final int position) {
            final String title = items[position];

            int color = getRandomHSVColor();
            holder.tvTitle.setBackgroundColor(getLighterColor(color));
            holder.tvTitle.setTextColor(getReverseColor(color));

            holder.tvTitle.setText(title);
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(context, title, Toast.LENGTH_SHORT).show();
                }
            });
        }

        @Override
        public int getItemCount() {
            return items.length;
        }

        protected int getRandomHSVColor(){
            // Generate a random hue value between 0 to 360
            int hue = rnd.nextInt(361);
            // We make the color depth full
            float saturation = 1.0f;
            // We make a full bright color
            float value = 1.0f;
            // We avoid color transparency
            int alpha = 255;
            // Finally, generate the color
            int color = Color.HSVToColor(alpha, new float[]{hue, saturation, value});
            // Return the color
            return color;
        }

        protected int getReverseColor(int color){
            float[] hsv = new float[3];
            Color.RGBToHSV(
                    Color.red(color), // Red value
                    Color.green(color), // Green value
                    Color.blue(color), // Blue value
                    hsv
            );
            hsv[0] = (hsv[0] + 180) % 360;
            return Color.HSVToColor(hsv);
        }

        protected int getLighterColor(int color){
            float[] hsv = new float[3];
            Color.colorToHSV(color,hsv);
            hsv[2] = 0.2f + 0.8f * hsv[2];
            return Color.HSVToColor(hsv);
        }
    }

    int NUM_COLUMNS = 3;
    RecyclerView rvItems;
    String[] items = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven"};

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

        GridAdapter adapter = new GridAdapter(this, items);

        rvItems = (RecyclerView) findViewById(R.id.rvItems);
        rvItems.setLayoutManager(new GridLayoutManager(this, NUM_COLUMNS));
        rvItems.setAdapter(adapter);
        rvItems.setHasFixedSize(true);
    }
}

Result

android_recyclerview_grid.png

How to order items in RecyclerView via SortedList

SortedList is a sorted list implementation that can keep items in order and also notify for changes in the list such that it can be bound to a RecyclerView.Adapter.

It keeps items ordered using the compare(Object, Object) method and uses binary search to retrieve items. If the sorting criteria of your items may change, make sure you call appropriate methods while editing them to avoid data inconsistencies.

You can control the order of items and change notifications via the SortedList.Callback parameter. Callbacks methods can be divided into

  • boolean areContentsTheSame (T2 oldItem, T2 newItem)
  • boolean areItemsTheSame (T2 item1, T2 item2)
  • int compare (T2 o1, T2 o2)

First two methods: areContentsTheSame, areItemsTheSame are basically hashCode and equals implementation. Those are used to determine if object was added or updated and are called after compare method returns "0" in comparison step.

Let's create a list of people and sort by distance field.

public class MainActivity extends AppCompatActivity {
    private RecyclerView rvItems;
    private LinearLayoutManager llManager;
    private SortedListAdapter adapter;

    private class Person {
        private int id;
        private float distance;

        public Person(int id, float distance) {
            this.id = id;
            this.distance = distance;
        }

        public int getId() {
            return id;
        }

        public float getDistance() {
            return distance;
        }

        @Override
        public String toString() {
            return "Person: " + getId();
        }
    }

    private class TodoViewHolder extends RecyclerView.ViewHolder {
        TextView tvTitle;
        Person person;

        public TodoViewHolder(View v) {
            super(v);
            tvTitle = (TextView) v;
        }

        public void bindTo(Person item) {
            person = item;
            tvTitle.setText("id: " + item.getId() + ", distance: " + item.getDistance());
        }
    }

    private class SortedListAdapter extends RecyclerView.Adapter<TodoViewHolder> {
        SortedList<Person> items;
        final LayoutInflater inflater;

        public SortedListAdapter(LayoutInflater layoutInflater, Person... persons) {
            inflater = layoutInflater;

            items = new SortedList<>(Person.class, new SortedListAdapterCallback<Person>(this) {
                @Override
                public int compare(Person p0, Person p1) {
                    return Float.compare(p0.getDistance(), p1.getDistance());
                }

                @Override
                public boolean areContentsTheSame(Person oldItem, Person newItem) {
                    return oldItem.getDistance() == newItem.getDistance();
                }

                @Override
                public boolean areItemsTheSame(Person item1, Person item2) {
                    return item1.getId() == item2.getId();
                }
            });

            for (Person item : persons) {
                items.add(item);
            }
        }

        public void addItem(Person item) {
            items.add(item);
        }

        public void delItem(Person item) {
            items.remove(item);
        }

        public void updateItem(Person item) {
            int total = items.size();
            int pos = -1;

            for (int i = 0; i < total; ++i) {
                Person tmp = items.get(i);
                if (tmp.getId() == item.getId()) {
                    pos = i;
                    break;
                }
            }

            if (pos >= 0) {
                items.updateItemAt(pos, item);
            }
        }

        @Override
        public TodoViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
            return new TodoViewHolder(inflater.inflate(R.layout.sorted_item, parent, false));
        }

        @Override
        public void onBindViewHolder(TodoViewHolder holder, int position) {
            holder.bindTo(items.get(position));
        }

        @Override
        public int getItemCount() {
            return items.size();
        }

    }

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

        rvItems = (RecyclerView) findViewById(R.id.rvItems);
        rvItems.setHasFixedSize(true);
        llManager = new LinearLayoutManager(this);
        rvItems.setLayoutManager(llManager);

        adapter = new SortedListAdapter(getLayoutInflater(),
                new Person(1, 34.5f),
                new Person(2, 12.5f),
                new Person(4, 0.5f),
                new Person(5, 0.5f),
                new Person(3, 99.5f)
        );

        rvItems.setAdapter(adapter);
        rvItems.setHasFixedSize(true);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                adapter.updateItem(new Person(4, 15.5f));
                Toast.makeText(MainActivity.this, "Updated", Toast.LENGTH_SHORT).show();
            }
        }, 1000);
    }
}

Here is activity_main.xml file.

<?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">

    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rvItems"/>

</LinearLayout>

Here is sorted_item.xml file.

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

Building a RecyclerView with Kotlin

Let's learn building a RecyclerView using Kotlin.

In your build.gradle of your project add the following:

buildscript {
    ext.kotlin_version = '1.1.51'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
    }
}

Kotlin Android Extensions is a compiler plugin that offers a convenient way of accessing views defined in XML via a property-like syntax.

Now in your build.gradle of your app add the following:

apply plugin: 'kotlin-android-extensions'

...

dependencies {
    ...
    implementation 'com.android.support:design:26.1.0'
    implementation 'com.android.support:cardview-v7:26.1.0'
}

Add RecyclerView to your activity_main.xml.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rvItems"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.constraint.ConstraintLayout>

Next step is to create our RecyclerViewAdapter.kt*. It is pretty much same as we usually do in Java.

class ItemAdapter(val items: ArrayList<Item>, val onClick: (Item) -> Unit) : RecyclerView.Adapter<ItemAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemAdapter.ViewHolder {
        val v = LayoutInflater.from(parent.context).inflate(R.layout.rv_item, parent, false)
        return ViewHolder(v, onClick)
    }

    override fun onBindViewHolder(holder: ItemAdapter.ViewHolder, position: Int) {
        holder.bindItems(items[position])
    }

    override fun getItemCount(): Int {
        return items.size
    }

    class ViewHolder(itemView: View, val onClick: (Item) -> Unit) : RecyclerView.ViewHolder(itemView) {
        fun bindItems(item: Item) {
            with(itemView) {
                itemView.tvName.text = item.name
                itemView.setOnClickListener{onClick(item)}
            }
        }
    }
}

with is a useful function included in the standard Kotlin library. It basically receives an object and an extension function as parameters, and makes the object execute the function. This means that all the code we define inside the brackets acts as an extension function of the object we specify in the first parameter, and we can use all its public functions and properties, as well as this. Really helpful to simplify code when we do several operations over the same object.

Following is the layout for item.

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:contentPadding="10dp">

    <TextView
        android:id="@+id/tvName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="12sp"/>

</android.support.v7.widget.CardView>

Now we will add the data class or you can say POJO class in Kotlin. A data class is a special class in Kotlin that provides you with default behaviors for all your Object methods like toString(), hashCode(), equals() and copy().

data class Item(var name: String)

Now everything is ready. Our adapter, model class, item layout and the main layout is ready. Now we have to attach the adapter to your RecyclerView.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        rvItems.layoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
        rvItems.addItemDecoration(LinearLayoutSpaceItemDecoration(16))

        val items = ArrayList<Item>()
        items.add(Item("Item 1"))
        items.add(Item("Item 2"))
        items.add(Item("Item 3"))
        items.add(Item("Item 4"))

        rvItems.adapter = ItemAdapter(items)
    }
}

Let's define LinearLayoutSpaceItemDecoration for space between items.

class LinearLayoutSpaceItemDecoration(var spacing: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        if (outRect != null && parent != null) {
            var position = parent.getChildAdapterPosition(view)
            outRect.left = spacing
            outRect.right = spacing
            outRect.bottom = spacing
            if (position < 1) outRect.top = spacing
        }
    }
}

Useful links

comments powered by Disqus