Adapter / LayoutManager / LinearLayoutManager / Material Design / RecyclerView

Material – Part 5

The the previous article we began the migration from ListView to RecyclerView by moving our Adapter implementation to RecyclerView.Adapter. In this article we’ll complete the migration and also find one small, but easily resolved problem along the way.

Previously we used android.R.layout.list layout, but this has ListView baked in to it, se we need to define a new layout containing a RecyclerView instead:

<?xml version="1.0" encoding="utf-8"?>
<merge 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/list"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

  <TextView
    android:id="@+id/empty"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:textAppearance="@style/TextAppearance.AppCompat.Title" />

</merge>

Next we need to update FeedListActivity to use this layout:

public class FeedListActivity extends ActionBarActivity
        implements FeedConsumer, FeedAdapter.ItemClickListener {
    private static final String DATA_FRAGMENT_TAG = DataFragment.class.getCanonicalName();

    private RecyclerView recyclerView;

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

        recyclerView = (RecyclerView) findViewById(R.id.list);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);

        DataFragment dataFragment = (DataFragment) getFragmentManager().findFragmentByTag(DATA_FRAGMENT_TAG);
        if (dataFragment == null) {
            dataFragment = (DataFragment) Fragment.instantiate(this, DataFragment.class.getName());
            dataFragment.setRetainInstance(true);
            FragmentTransaction transaction = getFragmentManager().beginTransaction();
            transaction.add(dataFragment, DATA_FRAGMENT_TAG);
            transaction.commit();
        }
    }
    .
    .
    .
}

We implement FeedAdapter.ItemClickListener so that we receive callbacks when list items are clicked. Then we set the content View to our new layout, and look up the RecyclerView from the layout.

The next thing that we do is create a LayoutManager which is responsible for positioning the list items within RecyclerView. LayoutManagers are a powerful new feature of RecyclerView which separates the layout logic from RecyclerView so that we can create much more complex layouts by using different LayoutManager implementations such as staggered grids. For now we’ll just stick to a standard vertical ListView-style layout, so we’ll use LinearLayoutManager and set its orientation to VERTICAL.

Next we need to update our existing setFeed() method to match the new constructor of FeedAdapter and set the adapter on our RecyclerView instance:

public class FeedListActivity extends ActionBarActivity
        implements FeedConsumer, FeedAdapter.ItemClickListener {
    .
    .
    .
    public void setFeed(Feed feed) {
        FeedAdapter adapter = new FeedAdapter(feed.getItems(), this);
        recyclerView.setAdapter(adapter);
    }
    .
    .
    .
}

Finally we need to implement the ItemClickHandler interface so that we handle list item clicks:

public class FeedListActivity extends ActionBarActivity
        implements FeedConsumer, FeedAdapter.ItemClickListener {
    .
    .
    .
    @Override
    public void itemClicked(Item item) {
            Intent detailIntent = new Intent(FeedListActivity.this, FeedDetailActivity.class);
            detailIntent.putExtra(FeedDetailActivity.ARG_ITEM, item);
            startActivity(detailIntent);

    }
    .
    .
    .
}

That’s us pretty much done, we can now run this:

This looks pretty close to what we had before, but if you look closely you may notice that we no longer get any visual feedback when the user clicks on a list item.

The reason for this is that ListView has a selector overlay built in, but in RecyclerView we need to manage the selector ourselves. One way of doing this would be using RecyclerView.ItemDecoration. However the documentation for this suggests that it intended more for things like separators between items, so instead we’ll add a simple overlay to our item layout:

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

  <LinearLayout
    android:id="@+id/feed_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:orientation="vertical"
    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:paddingRight="?android:attr/listPreferredItemPaddingRight">

    <TextView
      android:id="@+id/feed_item_title"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginTop="@dimen/margin"
      android:textColor="@color/text_dark"
      android:textAppearance="@style/TextAppearance.AppCompat.Title" />

    <TextView
      android:id="@+id/feed_item_description"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:textColor="@color/text_medium"
      android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

    <TextView
      android:id="@+id/feed_item_date"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_marginBottom="@dimen/margin"
      android:textColor="@color/text_light"
      android:gravity="end"
      android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

  </LinearLayout>

  <View
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/selectableItemBackground" />
</FrameLayout>

All we are doing here is wrapping our existing LinearLayout in a FrameLayout and adding a simple View which will match the dimension of the LinearLayout. The really cool trick that we do here is set the background of the selector overlay View to ?android:attr/selectableItemBackground. This makes this view behave like a selector – it will respond to touch events automatically!

We can see this in action:

The other really interesting thing about selectableItemBackground is that it will give the correct behaviour on different versions of Android. As we discussed earlier in this series, ripples are not supported pre-Lollipop because they require the render thread (which is only available in Lollipop). So on a Kitkat device we get the normal selector:

So we’ve now replaced our ListView with RecyclerView and the user is completely unaware of the change because the UX has not changed in the slightest. In the next article we’ll look at how we can use the new functionality and flexibility that we have with RecyclerView to do some Material-style things which would be much harder to achieve using ListView.

The source code for this article is available here.

Many thanks to Juhani Lehtimäki and Sebastiano Poggi for proof reads and some invaluable feedback.

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

17 Comments

  1. Hi, I am using exactly the same to have a ripple effect on touch. I added a FrameLayout parent, and I have RelativeLayout and a View with selectableItemBackground. But the ripple effect is not working. I am also changing the background of the RelativeLayout to a constant color programatically.

    What can be the problem? I didn’t do anything else much. Did you experience something like that?

    1. I have just added this to the normal ListView and it works
      android:drawSelectorOnTop=”true”. It is not related to a RecyclerView of course but I wonder what will be the case with RecyclerView.

    2. Impossible to tell without seeing your code. Try using hierachyviewer to find your selectableItemBackground View and make sure it’s filling the item layout. My guess would be that has zero size.

      1. I got this working with a RelativeLayout enclosed by a FrameLayout. Remove the View and inside assign the “selectableItemBackground” style to the inner RelativeLayout.

  2. Why do you need an extra FrameLayout and another view just to add the selectableItemBackground? Can’t you just add the `android:background` to the LinearLayout?

    1. The simple reason is that the selectable area needs to overlay the contents of the `LinearLayout`. If you set the background then it will be behind everything contained within the `LinearLayout`. By using the `FrameLayout` the selectableItemBackground is drawn over the contents of the `LinearLayout`.

      1. A better approach would be exploiting the android:foreground=”?selectableItemBackground” attribute of the FrameLayout. A click listener is required to be set on that FrameLayout in order for it to display the foreground. Saves 1 view count too

  3. I noticed adding the selectableItemBackground will give the ripple effect, but the ripple will always originate from the center; not the origin of touch like in a ListView. Is there a way to fix this?

    1. You’ll probably need to implement the ripple manually if you’re not happy with the one provided by selectableItemBackground. See the series on Ripples for more info

  4. Hi,
    I am facing problem when adding this backgroun
    background=”?android:attr/selectableItemBackground” . I am using the Theme from App compact library. Please help me how to add it properly.. Thanks

    1. Firstly you don’t state what your problem actually is. Secondly, there’s no way I can debug your code for you because a) I simply don’t have the time to offer free developer support, and b) I can’t see it. I would recommend stackoverflow.com of you’re looking for help.

  5. Hello,
    I’ve test your code, the ripple effect works but the hotspot is not correctly used, it is always launched on the center of the view.

    Am I missing something?

    1. That’s how selectableItemBackground works. If you want one which responds to the touch point you’ll need to implement it yourself. See the series on Ripples for further information.

  6. I have a few questions:
    1. How do I set a selector along with the ripple effect, so that I could handle all states? For example, I wish to handle “checked”/”selected” states.
    2.How do I have the ripple effect on CardView ?
    3.How do I have the ripple effect on 9-patch, so that it will fill its content area ?

  7. How to keep the selected item highlight even after the click? For example when clicking on an item, its background color become red and it keeps that background color (red) until another item is clicked. Now that item (just clicked) has a red background color, and so on.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.