1

I have a RecyclerView adapter. Within the onBindViewHolder, I check how many images each item passed to the adapter has. Depending on the number of images, I want to load/inflate a child layout into the main layout.

For example:

  • If the item has 1 image, I need to inflate images_one.xml into the main layout
  • If the item has 2 images, I need to inflate images_two.xml into the main layout
  • etc, up to 8 images (images_eight.xml)

Here is the main layout, main_layout.xml:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    // INFLATE CHILD LAYOUT HERE (images_one.xml, images_two.xml, etc.)

    <TextView
        android:id="@+id/desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

</LinearLayout>

And here is one of the child layouts that need to be inflated into the main layout, images_two.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/image_one"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

    <ImageView
        android:id="@+id/image_two"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />

</LinearLayout>

And lastly, here is my RecyclerView adapter:

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

    private static Context context;
    private List<Message> mDataset;

    public RecyclerAdapter(Context context, List<Message> myDataset) {
        this.context = context;
        this.mDataset = myDataset;
    }

    public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener, View.OnClickListener {
        public TextView title;
        public TextView desc;

        public ViewHolder(View view) {
            super(view);
            view.setOnCreateContextMenuListener(this);

            title = (TextView) view.findViewById(R.id.title);
            desc = (TextView) view.findViewById(R.id.desc);
        }
    }

    @Override
    public RecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.main_layout, parent, false);
        ViewHolder vh = new ViewHolder((LinearLayout) view);

        return vh;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Message item = mDataset.get(position);

        holder.title.setText(item.getTitle());
        holder.desc.setText(item.getDesc());

        int numImages = item.getImages().size();

        if (numImages == 1) {
            // Inflate images_one.xml into main_layout.xml
        } else if (numImages == 2) {
            // Inflate images_two.xml into main_layout.xml
        } else if (numImages == 3) {
            // Inflate images_three.xml into main_layout.xml
        }
        // ETC...
    }

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

}

What's the best way of implementing this?

user5590200
  • 33
  • 1
  • 6

1 Answers1

2

The way Android intends ViewHolders to be used is for views to be created/inflated in onCreateViewHolder() then filled with adapter data in onBindViewHolder(). This is important because onCreateViewHolder() is only called when there is nothing to recycle.

Unlike ListView, RecyclerView is actually recycling ViewHolders rather than Views. So trying to inflate views in onBindViewHolder() is not going to work.

Notice that viewType parameter in onCreateViewHolder()? That's the key.

First you need to override getItemViewType() in your adapter. Let's take a shortcut here; since the view type is an int, let's just use the number of images in the item as the view type:

    @Override
    public int getItemViewType(int position) {
        //  ... return the number of images for data at position
    }

(Normally I would define final ints like VIEW_TYPE_ONE_IMAGE etc. to be returned, which is the proper way to do it.)

How does the adapter use the view types? If your data at position 0 has a view type of 4, then the RecyclerView will only pass a ViewHolder created in onCreateViewHolder() with viewType == 4 to onBindViewHolder() with position == 0.

Now your onCreateViewHolder() might look like this:

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.main_layout, parent, false);
        return new ViewHolder(view, viewType);
    }

(FYI: The return type for onCreateViewHolder() should be your ViewHolder type, not RecyclerViews.)

Notice that I added a viewType parameter to your ViewHolder constructor:

    public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener, View.OnClickListener {
        public TextView title;
        public TextView desc;
        public ImageView img1;
        public ImageView img2;

        public ViewHolder(View view, int viewType) {
            super(view);
            view.setOnCreateContextMenuListener(this);

            title = (TextView) view.findViewById(R.id.title);
            desc = (TextView) view.findViewById(R.id.desc);

            ViewGroup placeholder = (ViewGroup) view.findViewById(R.id.placeholder);

            // here you can use the viewType to help you figure 
            // out which child views you need references for
            switch (viewType) {
            case 1:
                // ...
                break;

            case 2:
                LayoutInflater.from(parent.getContext()).inflate(R.layout.images_two, placeholder);       // this combines inflate() and addView()
                img1 = placeholder.findViewById(R.id.image_one);
                img2 = placeholder.findViewById(R.id.image_two);
                break;

            // ...
            }
        }
    }

You don't necessarily need viewType in the constructor if you are using the same ids in all your different views; you can just check if they're null in onBindViewHolder().


If you have many different view types, one great way to organize the create/bind code is to make your ViewHolder subclass abstract, further subclass it into a different type for each view type, and then return an instance of the appropriate type in onCreateViewHolder(). Declare an abstract method like public void bind(YourItemType item) in your abstract type and implement in your subclasses, then your onBindViewHolder() method is just:

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.bind(data.get(position));  // ... or however you access an item at position
    }

...and creating looks like:

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View view = null;

        switch (viewType) {
        case 1:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.images_one, parent, false);
            return new OneImageViewHolder(view);

        case 2:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.images_two, parent, false);
            return new TwoImageViewHolder(view);

        // ... etc. etc.
        }
    }

Don't forget, you may be passed a recycled ViewHolder in onBindViewHolder(), so you have to assume all your views are "dirty" and set every single child view to a known state for the data at position.

kris larson
  • 30,387
  • 5
  • 62
  • 74
  • This is one hell of answer, but I have a concern. What has happened to `main_layout.xml`? It looks like, with your method, I need to create 8 different "main layouts" (one for each number of images), where each layout is exactly the same except for the image part? Is that correct? – user5590200 Nov 22 '15 at 07:07
  • 1
    No. I just wrote some simplified code for the answer. The idea is that with the view type, you can create different views based on what the list item needs. I will update the answer to include the main_layout.xml. – kris larson Nov 22 '15 at 11:30
  • 1
    Okay, I updated the code, but not certain about specifics. Anyhow, the strategy is to inflate your main view, inflate your images view based on the view type, then add the images view to the main view. So no matter how you've split up the views, there is a way to achieve your goal using view types. – kris larson Nov 22 '15 at 23:35
  • This looks like it will do what I need. When I get bacl to my computer, I'll give it a go and get back to you. Thanks again. – user5590200 Nov 23 '15 at 07:03
  • The only issue I'm having is that it seems to be adding the image layouts to the end of the main layout. How can I inflate the child layouts into the `placeholder` view in the main layout, rather than the end of the main layout? – user5590200 Nov 25 '15 at 01:07
  • I've done this and it works, but it doesn't feel right that I'm using `findViewById` in `onCreateViewHolder` when I've already got `placeholder` in the `ViewHolder`. I'm not sure how I would call `placeholder` though. http://pastebin.com/8E4Pc9nF – user5590200 Nov 25 '15 at 02:02
  • inflate() normally adds the view. The boolean false parameter at the end of inflate() means "use the parent for layout params, but don't add the view to the parent". You can take off this parameter and do the inflate and addView in a single step. – kris larson Nov 25 '15 at 02:36
  • I used `viewGroup.addView(view, 1)` in my code to add the view to the second position in the view group (0 is first position). If that works, you wouldn't need to use `findViewById()`. Wasn't sure if that would work, though. I don't see any problem with `findViewById()` because the images view is always put in the correct spot. – kris larson Nov 25 '15 at 02:41
  • I plan to add more views to the main_layout, in which case it would become more complex to get the exact position for `addView`. Is there a way to refer to the `placeholder` from `ViewHolder` rather than having to reuse `findViewById`? – user5590200 Nov 25 '15 at 02:49
  • In other words, is it possible to modify my code without having to make a new variable for the placeholder? http://pastebin.com/8E4Pc9nF – user5590200 Nov 25 '15 at 03:04
  • Not sure I understand your question. Updating my answer to show another possibility. – kris larson Nov 25 '15 at 16:23