Select Page

The RecyclerView could stand to be the most implemented API change that Google unveiled as part of Android L. One reason for that is that the ListView is used on almost every single application you use every day. Second, the RecyclerView is in the support library so developers targeting previous versions of Android will still be able to utilize it’s benefits.

CardView is an added surprise for the support library. Google Now really emphasized Google’s move to the card as a basic building block for their applications. With the addition of realtime shadows in L a basic card view with some “fake” shadows is critical for maintaining backwards compatibility while still using some Material design principles.

In this tutorial we will add a RecyclerView to display a list of CardViews in our application.

Full source available on Github.

The Basic RecyclerView

Note: In the current version of the SDK it may be required that you add RecyclerView and CardView to your gradle build.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:recyclerview-v7:21.+'
    compile 'com.android.support:cardview-v7:21.+'
}

At the most basic level using a RecyclerView is very similar to implementing a ListView. First you need to add it to your layout. In the case of the example application we will add the RecycleView directly into the layout for the CountryActivity.

<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=".CountryActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".CountryActivity"
        />
</RelativeLayout> 

Not much to see here.

In the CountryActivity we will inflate the view as we normally would a ListView. It is here that you may notice some subtle differences.

@Override
protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_country);

    mRecyclerView = (RecyclerView)findViewById(R.id.list);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mRecyclerView.setItemAnimator(new DefaultItemAnimator());

    mAdapter = new CountryAdapter(CountryManager.getInstance().getCountries(), R.layout.row_country, this);
    mRecyclerView.setAdapter(mAdapter);
}

Ignore the CountryAdapter for now. The important thing to notice at this point is the addition of the setLayoutManager and setItemAnimatorrequirement. Failing to set the layout manager will cause a big fat error.

So what are these guys for? Essentially the LinearLayoutManager is responsible for how the items appear in the list. While there currently is just one kind of LayoutManger it is a pretty safe assumption that there will be at minimum a GridLayoutManager by the time L reaches production. There are already several extensions of LayoutManager that can help you if you need a grid or staggered grid right now. the TwoWayView is a pretty good place to start.

The DefaultItemAnimator handles the animation duties for adding, removing, and moving items from the list. With ListViews animating these kinds of things was doable but not very intuitive. Currently subleasing theDefaultItemAnimator is not very straight forward but I would expect that as the libraries are finalized we may see both more animators bundled with the API and better documentation for correctly extended the animators that are supplied.

Dealing the CardViews (get it? Dealing the CARDviews.. ill see myself out)

CardViews are a simple idea with some very subtle differences depending on the version of Android under which they are deployed. The realtime shadows that will be a staple in L and the Material theme are created by a realtime light source. Because previous versions of Android will not support this functionality (due to the lack of a RenderThread) they must be generated beforehand and “faked”. In the image below the difference are not obvious but do not expect them to be a 100% match across OS versions in your final release.

resized-cardviews-1

 

Realtime shadows are inherently more configurable and flexible. Thus if you animate a cardiview’s Z axis the shadow will change dynamically on L but not at all on previous versions (assuming you manage to translate the z axis on previous versions which is not as easy as it is on L).

Our basic row for the demo is as follows (disregard like of dimension file usage.. for display purposes only!)

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_margin="5dp"
    card_view:cardCornerRadius="5dp"
    android:layout_height="match_parent">

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

        <ImageView
            android:id="@+id/countryImage"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:scaleType="centerCrop"
            android:tint="@color/photo_tint"
            android:layout_centerInParent="true"
            />

        <TextView
            android:id="@+id/countryName"
            android:gravity="center"
            android:background="?android:selectableItemBackground"
            android:focusable="true"
            android:clickable="true"
            android:layout_width="match_parent"
           android:layout_height="100dp"
           android:textSize="24sp"
           android:layout_centerInParent="true"
           android:textColor="@android:color/white"
           />

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

Not much to see. Notice that you can adjust the corner radius by setting thecard_view:cardCornerRadius attribute.

The Adapter Is The Key

So far so good. Not much has changed and there is not a lot of real new complexity. Things do get a lot different in the Adapter department though. If you have done much Android development (and reading this post I will assume you have done some) it is likely (hopefully very likely!) that you came accrues the best practice known as the ViewHolder .

Given the amount of interviews I have given to junior Android developer resources and given the amount of code I have reviewed on projects I assume that chances are you may NOT have heard of the ViewHolder pattern. Click the link. Read. I will wait.

Now that you understand what a ViewHolder is you will instantly recognize a fundamental change in the RecyclerView vs the old ListView. Where once the ViewHolder was recommended, now it is baked into the SDK itself. The View Holder for the demo looks like this:

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView countryName;
        public ImageView countryImage;

        public ViewHolder(View itemView) {
            super(itemView);
            countryName = (TextView) itemView.findViewById(R.id.countryName);
            countryImage = (ImageView)itemView.findViewById(R.id.countryImage);
        }

    }

It is also important for the adapter class to extend theRecyclerView.Adapter class. In doing so you also supply the class of the ViewHolder like so:RecyclerView.Adapter<CountryAdapter.ViewHolder> .

The other change that varies from the ListView is that rather than checking for a tag on a view before deciding to create a new instance instead of reusing an old one, the API takes care of all the logic. There are now to methods to implement for view construction and reuse. The first isonCreateViewHolder :

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(rowLayout, viewGroup, false);
        return new ViewHolder(v);
    }

In this method you will always create a new view with a specified layout and attach it to a ViewHolder. It is possible that you have multiple layouts within your view so this method could easily grow in complexity.

The second method is onBindViewHolder which looks like so:

@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
    Country country = countries.get(i);
    viewHolder.countryName.setText(country.name);
    viewHolder.countryImage.setImageDrawable(mContext.getDrawable(country.getImageResourceId(mContext)));
}

For our demo we are getting a drawable and a text string and setting it on the ViewHolder instance variables.

As a reminder here is the Old Way (different project):

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View v = convertView;
        ViewHolder viewHolder;

        if(v == null){
            LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = inflater.inflate(R.layout.tweet_list_item, null);

            viewHolder = new ViewHolder();
            viewHolder.twitterUserImage = (NetworkImageView)v.findViewById(R.id.twitterUserImage);
            viewHolder.userNameTextView = (TextView)v.findViewById(R.id.usernameTextView);

            v.setTag(viewHolder);

        } else {
            viewHolder = (ViewHolder) v.getTag();
        }

            viewHolder.twitterUserImage.setImageUrl(tweet.getUserImageUrl(),   ageCacheManager.getInstance().getImageLoader());
            viewHolder.userNameTextView.setText("@" + tweet.getUsername());
    }

    return v;
}

Putting it all together

The source for this project can be found on Github

Wrapup

In the next part of this series we will implement RippleDrawables and investigate the various ways that they can be customized to fit our application.