[Android] Expand a RecyclerView in Four Steps

转自:https://www.bignerdranch.com/blog/expand-a-recyclerview-in-four-steps/?utm_source=Android+Weekly&utm_campaign=8f0cc3ff1f-Android_Weekly_165&utm_medium=email&utm_term=0_4eb677ad19-8f0cc3ff1f-337834121

The Expandable RecyclerView library is a lightweight library that simplifies the inclusion of expandable dropdown rows in your RecyclerView. In it, you have two types of views. The parent view is the one visible by default, and it contains the children. The child view is the one that is expanded or collapsed, and will appear when a parent item is clicked.

In this post, we will implement the Expandable RecyclerView in the CriminalIntent application from our Android programming guide. We’ll be showing a more detailed view of each crime from the main list fragment.

You can view the source code for the library and two samples on GitHub, or read the Javadocs.

Not familiar with RecyclerView? Bill Phillips has written two excellent blog posts on the subject.

Let’s Get it Working

Our completed demo will look like this:

Expandable CriminalIntent

Start by adding these two dependencies to your app’s build.gradle file:

dependencies {
    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.bignerdranch.android:expandablerecyclerview:1.0.3'
}

All expanding and collapsing functionality is handled in the adapter, meaning that your RecyclerView is just a stock RecyclerView. All you’ll need to do to set up the Expandable RecyclerView in a layout is add a stock RecyclerView to your activity or fragment’s XML layout.

1. The ViewHolders

First, let’s create the XML layouts for our parent and child views. Our parent layout is going to include the title of the crime and a dropdown arrow to display an animation when the item is expanded or collapsed. We’ll call this layout list_item_crime_parent.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#dbdbda">

    <TextView
        android:id="@+id/parent_list_item_crime_title_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textStyle="bold"
        android:text="Crime Title" />

    <ImageButton
        android:id="@+id/parent_list_item_expand_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_margin="8dp"
        android:src="@android:drawable/arrow_down_float" />

</RelativeLayout>

Now, onto the child layout. The child is going to contain a TextView that shows the date of the crime and a checkbox that we can click when the crime is solved. We’ll call this layout list_item_crime_child.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#cbcbcb">

    <TextView
        android:id="@+id/child_list_item_crime_date_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="Crime Date" />

    <CheckBox
        android:id="@+id/child_list_item_crime_solved_check_box"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="8dp"
        android:text="Solved" />

</RelativeLayout>

Now that we have both of our layouts ready, let’s set up each ViewHolder. We will create a class called CrimeParentViewHolder that extends ParentViewHolder and another class called CrimeChildViewHolder that extends ChildViewHolder.

Let’s start with CrimeParentViewHolder. Go ahead and create a public TextView and ImageButton variable for our two views, like so:

public class CrimeParentViewHolder extends ParentViewHolder {

    public TextView mCrimeTitleTextView;
    public ImageButton mParentDropDownArrow;

    public CrimeParentViewHolder(View itemView) {
        super(itemView);

        mCrimeTitleTextView = (TextView) itemView.findViewById(R.id.parent_list_item_crime_title_text_view);
        mParentDropDownArrow = (ImageButton) itemView.findViewById(R.id.parent_list_item_expand_arrow);
    }
}

Now, onto our CrimeChildViewHolder. We’re again going to create two public variables for our views, but this time one will be a TextView and one will be a CheckBox:

public class CrimeChildViewHolder extends ChildViewHolder {

    public TextView mCrimeDateText;
    public CheckBox mCrimeSolvedCheckBox;

    public CrimeChildViewHolder(View itemView) {
        super(itemView);

        mCrimeDateText = (TextView) itemView.findViewById(R.id.child_list_item_crime_date_text_view);
        mCrimeSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.child_list_item_crime_solved_check_box);
    }
}

Now let’s set up our parent and child objects.

2. Parents and Their Children

For our CriminalIntent example, the data that we want to display in our list items are fields of our Crime object. Since we have both a parent and a child layout, the best practice is to create a child object, separate from the parent, to hold the data that will be displayed in the child.

In this case, our parent object will be the Crime object itself. Let’s make our Crime object implement ParentObject. Implement the getter and setter methods with a list variable:

public class Crime implements ParentObject {

    /* Create an instance variable for your list of children */
    private List<Object> mChildrenList;

    /**
     * Your constructor and any other accessor
     *  methods should go here.
     */

    @Override
    public List<Object> getChildObjectList() {
        return mChildrenList;
    }

    @Override
    public void setChildObjectList(List<Object> list) {
        mChildrenList = list;
    }
}

The expandable RecyclerView allows for multiple children or none, so the children need to be added as a list. You can either add the children directly in the getChildObjectList method we implemented by returning your List of children, or you can add them when we create the list of items by calling setChildObjectList with the list of respective children. I will be doing the latter in this demo.

Our child object will hold two values, a String for the date and a boolean for the solved flag:

public class CrimeChild {

    private Date mDate;
    private boolean mSolved;

    public CrimeChild(Date date, boolean solved) {
        mDate = date;
        mSolved = solved;
    }

    /**
     * Getter and setter methods
     */
}

We’ll populate each parent object with children in the final step. Now, let’s get the adapter working!

3. The Adapter

We’re going need to implement a custom adapter that extends ExpandableRecyclerAdapter. Inside our adapter, we will implement a few methods so we can populate the data in our ViewHolders.

First, let’s create a class and call it CrimeExpandableAdapter. Make it extend ExpandableRecyclerAdapter and give it our two ViewHolder type parameters in the order of <parent view holder, child view holder>. Our header will look like this:

public class CrimeExpandableAdapter extends ExpandableRecyclerAdapter<CrimeParentViewHolder, CrimeChildViewHolder> {

You’ll need to implement the four inherited methods, onCreateParentViewHolder, onBindParentViewHolder, onCreateChildViewHolder and onBindChildViewHolder. Go ahead and implement the default constructor that will take in a Context and a List of ParentObjects.

In our constructor, let’s get access to the LayoutInflater and call it mInflater. This will be used in our onCreateParentViewHolder and onCreateChildViewHolder to inflate the layouts we created earlier. In our constructor, add mInflater = LayoutInflater.from(context);. This will create a layout inflater for us.

Now, let’s create our views and add them to the custom ViewHolders we made earlier. In onCreateParentViewHolder, let’s replace the return null line with these two lines:

View view = mInflater.inflate(R.layout.list_item_crime_parent, viewGroup, false);
return new CrimeParentViewHolder(view);

Let’s now do the same as we did in onCreateParentViewHolder, but adjust for our CrimeChildViewHolder. Again, replace return null with these two lines, but this time in onCreateChildViewHolder:

View view = mInflater.inflate(R.layout.list_item_crime_child, viewGroup, false);
return new CrimeChildViewHolder(view);

To finish off our adapter, let’s work on our onBindViewHolder methods, starting with onBindParentViewHolder. In onBindParentViewHolder, three variables are passed in: the viewholder we created in onCreateParentViewHolder, the position of the item and the parent object associated with that position. We’re going to need to cast the passed parent object to our parent object type (which, as you recall, is our Crime object). Let’s make our onBindParentViewHolder look like this:

public void onBindParentViewHolder(CrimeParentViewHolder crimeParentViewHolder, int i, Object parentObject) {
    Crime crime = (Crime) parentObject;
    crimeParentViewHolder.mCrimeTitleTextView.setText(crime.getTitle());
}

Finally, we will set up our CrimeChildViewHolder. Recall that our CrimeChildViewHolder contains a TextView for a date and a CheckBox to indicate whether the crime is solved. Our onBindChildViewHolder will look like this:

public void onBindChildViewHolder(CrimeChildViewHolder crimeChildViewHolder, int i, Object childObject) {
    CrimeChild crimeChild = (CrimeChild) childObject;
    crimeChildViewHolder.mCrimeDateText.setText(crimeChild.getDate().toString());
    crimeChildViewHolder.mCrimeSolvedCheckBox.setChecked(crimeChild.isSolved());
}

Now our adapter is ready to roll. Let’s finish this all up!

4. Tying it all together

Let’s head back to our main fragment, where we are hosting the RecyclerView. Make sure you find the RecyclerView in your layout and set its layout manager to a new LinearLayoutManager.

I went ahead and created a simple method to generate each of our Crime objects and attach their children. You can add this method in your Fragment:

private ArrayList<ParentObject> generateCrimes() {
    CrimeLab crimeLab = CrimeLab.get(getActivity());
    List<Crime> crimes = crimeLab.getCrimes();
    ArrayList<ParentObject> parentObjects = new ArrayList<>();
    for (Crime crime : crimes) {
        ArrayList<Object> childList = new ArrayList<>();
        childList.add(new CrimeChild(crime.getDate(), crime.isSolved()));
        crime.setChildObjectList(childList);
        parentObjects.add(crime);
    }
    return parentObjects;
}

Another option is to create a new list of children directly in your Crime object and set that to be the childrenList. A third option would be to create a list of children in getChildObjectList and return that list.

Now we can add the following lines to our onCreateView method:

CrimeExpandableAdapter mCrimeExpandableAdapter = new CrimeExpandableAdapter(getActivity(), generateCrimes());
mCrimeExpandableAdapter.setCustomParentAnimationViewId(R.id.parent_list_item_expand_arrow);
mCrimeExpandableAdapter.setParentClickableViewAnimationDefaultDuration();
mCrimeExpandableAdapter.setParentAndIconExpandOnClick(true);

Note that the list you pass into your adapter must be of the type ParentObject.

The setCustomParentAnimationView allows for the arrow in the parent layout to rotate on expand/collapse and setParentClickableAnimationDefaultDuration gives it a default rotation time of 200 milliseconds. setParentAndIconExpandOnClick ensures that we can click both the parent and the arrow to expand the item. You can always remove these if you don’t want an animation or custom triggering view.

Finally, set the RecyclerView’s adapter to finish it up:

mCrimeRecyclerView.setAdapter(mCrimeExpandableAdapter);

That’s it! You now should have a RecyclerView that expands and collapses and has a nice rotation animation to go along with it.

Want to improve it or see more?

The library is open source, so visit the project’s GitHub page to see all the source code, then send a pull request if there are new features you’d like to add.

*Ryan Brooks was an Android intern this summer. Apply now to join our team as an intern in Fall 2015.

发表评论

电子邮件地址不会被公开。 必填项已用*标注