home.


Tagged: android-recyclerview


Android: RecyclerView with animated expandable sections

In previous posts, we’ve created a RecyclerView with section headings and a click listener. Look for posts under the android-recyclerview tag.

We continue to use our data object, SectionOrRow, that defines if the data is a section or a row. But we will change three lines in the adapter. These changes will define our list size and the list item by taking into account the hidden positions.

In getItemCount() we will get the size from SectionOrRow.getSizeExcludingHidden(mData). And in getItemViewType() and onBindViewHolder() we will get the data item by calling SectionOrRow.getUnderlyingItem(mData, position).

Outside our adapter, we will now look for clicks on the section headings. If we get one, we will check if it’s expanded or not, and if it is we will call a method that hides or shows our rows:

adapter.setClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Get the position of the view
        int visiblePosition = YOUR_RECYCLER_VIEW.indexOfChild(v);
        // Get the item it represents
        StringSectionOrRow item = SectionOrRow.getUnderlyingItem(data, visiblePosition);
        // if the section is not hidden, hide the section, and if it is, show it.
        if(!item.isRow() && !item.getSectionHidden()) {
            int numberHidden = SectionOrRow.hideUntilNextSection(data, visiblePosition, true);
            adapter.notifyItemRangeRemoved(numberHidden+1, numberHidden);
        } else if(!item.isRow() && item.getSectionHidden()) {
            int numberShown = SectionOrRow.hideUntilNextSection(data, visiblePosition, false);
            adapter.notifyItemRangeInserted(numberShown+1, numberShown);
        }
    }
});

SectionOrRow.hideUntilNextSection(data, visiblePosition, true) will look at our list of data, and set all the rows until the next section as hidden. The last parameter defines if we’re going to set it to hidden or not.

adapter.notifyItemRangeInserted(visiblePosition+1, hidden) tells our recycler view to animate the insertion of our views, or to animate their delection. That is, when we set our underlying data to hidden, we want the recycler view to animate their removal.

Our old SectionOrRow class now has two new booleans: hidden and sectionCollapsed. These define if we should show or hide the row and if the section is collapsed.

The first new method in this class is the one used to size the list including all the hidden rows. Its implementation is straight forward.

public static int getSizeExcludingHidden(List<SectionOrRow> rows) {
    int size = 0;
    for (StringSectionOrRow r : rows) {
        if(!r.hidden) size++;
    }
    return size;
}

The second is used internally only. You give it the position which does not include all the hidden cells. The adapter will give you this value. It will then return the position including the hidden rows.

We loop over all the underlying items, and if such is not hidden we increment a visibleRow integer. Eventually this will match the position we’re looking for, and we return the underlying position:

private static int getUnderlyingRowPosition(List<StringSectionOrRow> rows, int visiblePosition) {
    int underlyingRow = 0, visibleRow = 0;
    for (; underlyingRow < rows.size(); underlyingRow++) {
        StringSectionOrRow stringSectionOrRow = rows.get(underlyingRow);
        if(!stringSectionOrRow.hidden && visibleRow==visiblePosition) break;
        if(!stringSectionOrRow.hidden) visibleRow++;
    }
    return underlyingRow;
}

The next method is the one where we get the item to display. We give it the position in the adapter that does not include all the hidden cells. And, via the above, it returns the underlying row for the adapter to display:

public static SectionOrRow getUnderlyingItem(List<StringSectionOrRow> rows, int visiblePosition) {
    return rows.get(getUnderlyingRowPosition(rows, visiblePosition));
}

Our final function is the one we use to set the rows to be hidden or shown. We give it the row objects the adapter is given, the position of our section header (as found in the above click listener) and then define if we want to either hide or show the rows:

public static int hideUntilNextSection(List<StringSectionOrRow> rows,
                                       int visiblePosition,
                                       boolean hide) {
    // What's he underlying row data
    int underlying = StringSectionOrRow.getUnderlyingRowPosition(rows, visiblePosition);
    SectionOrRow section = rows.get(underlying);
    // Say that's now collapsed (or fully visible)
    section.setSectionCollapsed(hide);
    underlying++; // Go to the next row
    // Let's now either show or hide those values
    // until we reach the next section
    int rowsHidden = 0;
    for (int i = underlying; i < rows.size();i++,rowsHidden++) {
        StringSectionOrRow r = rows.get(i);
        if(!r.isRow) break;
        else {
            r.setHidden(hide);
        }
    }
    return rowsHidden;
}

Now when the recycler view sizes itself, it takes into account the hidden cells, and when it display an item, it does the same.

When one its section heading is clicked, it checks if it’s already collapsed, if it is it sets all the rows under that section to visible, and the animates their insertion.

Equally, if the section is not collapsed, it sets all the rows to hidden, and animates their removal.

android android-recyclerview

Android: On item click listener for RecyclerView Adapter

RecyclerView.Adapter, unlike its ListView colleague, does not have a item click listener.

You can, however, use a normal View.OnClickListener and then use indexOfChild to get the position of the view in the recycler view.

Add the callback setter to your adapter:

public void setClickListener(View.OnClickListener callback) {
    mClickListener = callback;
}

And in your onCreateViewHolder set that:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.some_layout, parent, false);
    RecyclerView.ViewHoldre holder = new SomeViewHolder(v);
    holder.itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mClickListener.onClick(view);
        }
    });
    return holder;
}

Now, outside the adapter, you can fetch the position like so:

YOUR_RECYCLER_ADAPTER.setClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        int pos = YOUR_REYCLER_VIEW.indexOfChild(v);
        ...
    }
});

And then pos will have the index of the view.

This is better than having the position passed through in the callback, since any callback binding may have the incorrect position, if the internal position of the views change.

android android-recyclerview

Android: Section headers with RecyclerView and multiple view types

Android RecyclerView doesn’t have section headers natively. Mainly because this is an iOS-centric feature and often looks odd in Android. However, sometimes your employers don’t care!

We achieve this effect, however, with RecyclerView view types.

Firstly, instead of passing a String list, for example, to your recycler view adapter, pass a list of objects that specify whether the data should be used as a section header or not. Here’s an example of that.

// In a fuller example, this would probably hold more data than just strings.
public class SectionOrRow {

    private String row;
    private String section;
    private boolean isRow;

    public static SectionOrRow createRow(String row) {
        SectionOrRow ret = new SectionOrRow();
        ret.row = row;
        ret.isRow = true;
        return ret;
    }

    public static SectionOrRow createSection(String section) {
        SectionOrRow ret = new SectionOrRow();
        ret.section = section;
        ret.isRow = false;
        return ret;
    }

    public String getRow() {
        return row;
    }

    public String getSection() {
        return section;
    }

    public boolean isRow() {
        return isRow;
    }
}

We create one of these objects with SectionOrRow.createRow("a normal row") or SectionOrRow.createSection("a section header"), and the isRow boolean is set accordingly.

Once you’ve created a list of these objects let’s now send them to our recycler view:

public class MyRecycler extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    private List<SectionOrRow> mData;

    public MyRecycler(List<SectionOrRow> data) {
        mData = data;
    }

    ...

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

    ...

  }

This is standard, with the data passed in and the item count coming from the data.

Next let’s define the getItemViewType method, which tells our recycler view there will be two types, a row type and a section type, and we’ll specify which position these are in by looking at the isRow boolean in our above data object:

@Override
public int getItemViewType(int position) {
    super.getItemViewType(position);
    SectionOrRow item = mData.get(position);
    if(!item.isRow()) {
        return 0;
    } else {
        return 1;
    }
}

Now there’s two type types, we’ll need two types of views in our onCreateViewHolder. In our case, we’re just using the standard simple_list_item_1 for both but with a blue text background for the section. Your rows and section headers will obviously be more complex:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if(viewType==0) {
        View v = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
        v.findViewById(android.R.id.text1).setBackgroundColor(Color.BLUE);
        return new SectionViewHolder(v);
    } else {
        View v = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
        return new RowViewHolder(v);
    }
}

This also shows we have two different view holders. Because our layout is so simple, these are pretty much the same, but in a fuller example they would be different:

public class RowViewHolder extends RecyclerView.ViewHolder{
    private TextView textView;
    public RowViewHolder(View itemView) {
        super(itemView);
        textView = (TextView) itemView.findViewById(android.R.id.text1);
    }
}

public class SectionViewHolder extends RecyclerView.ViewHolder{
    private TextView textView;
    public SectionViewHolder(View itemView) {
        super(itemView);
        textView = (TextView) itemView.findViewById(android.R.id.text1);
    }
}

Finally, let’s display the row in onBindViewHolder. Depending on whether it’s a row or not, we cast the holder to appropriate type, and give it some data that we managed to pass in.

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    SectionOrRow item = mData.get(position);
    if(item.isRow()) {
        RowViewHolder h = (RowViewHolder) holder;
        h.textView.setText(item.getRow());
    } else {
        SectionViewHolder h = (SectionViewHolder) holder;
        h.textView.setText(item.getSection());
    }
}

And that’s the basics of putting sections in your RecyclerView with view types.

android android-recyclerview

Android: Animating adding and removing from a recycler view

Recycler views animate the adding and removing of elements automatically. Let’s say you have a standard recyclerview like one we created in a previous post.

Ensure the row data, ArrayList<String> stringList for example, is passed in as a recycler adapter constructor parameter. The size of the recycler adapter should be from that row data.

Now, outside the recycler view, you can add and remove from stringList via the standard methods, stringList.remove(0) and stringList.add(0, "new data").

On addition, run yourAdapater.notifyItemInserted(0) so it knows to animation insertion. You also need to scroll to that item, else it will just be inserted about the current item, which may be the top item: yourRecyclerView.scrollToPosition(0).

On deletion, it’s simpler. Just do: yourAdapter.notifyItemRemoved(0). No need to scroll anywhere, this time.

android android-recyclerview

Android: Restore position of RecycleView after new data via setAdapter()

Let’s say you’re refreshing your list of items you want to see in a RecyclerView.

If you run setAdapter() with the new list of data, it will remove your old list position.

However, if you–when the the system knows you’ve already set an adapter–then call notifyDataSetChanged on the adapter then all will be well.

For instance, in the method that gives it new data, after you’ve set that new data to mYourList in this example:

if(recyclerView.getAdapter()==null) {
  bd.recView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
  bd.recView.setAdapter(new YourAdapter(mYourList));
} else {
  recyclerView.getAdapter().notifyDataSetChanged();
}
android android-recyclerview

Page 1 of 2
Next