androidandroid-fragmentsandroid-recyclerviewandroid-viewpagerfragmentstatepageradapter

ViewPager inside RecyclerView as row item


I need to have an ViewPager (similar to an horizontal gallery) inside an RecyclerView which display an list vertically. Each row of the RecyclerView will have an ViewPager which will allow to swipe between some images. The ViewPager will also support click events which will be propagated to the parent RecyclerView.

Currently, I have the following implementation:

List adapter:

@Override
public void onBindViewHolder(MyHolder holder, int position) {
    super.onBindViewHolder(holder, position);

    Item listItem = get(position);

    ...

    GalleryAdapter adapter =
                    new GalleryAdapter(getActivity().getSupportFragmentManager(),
                                                         item.mediaGallery);
    holder.imageGallery.setAdapter(adapter);

    ...
}

Gallery adapter:

public class GalleryAdapter extends FragmentStatePagerAdapter {

    private final List<Item.Gallery> mItems;
    @Bind(R.id.gallery_item)
    ImageView galleryView;

    public SearchResultsGalleryPagerAdapter(FragmentManager fm, @NonNull ArrayList<Item.Gallery> mediaGallery) {
        super(fm);

        mItems = mediaGallery;
    }

    @Override
    public Fragment getItem(int position) {
        GalleryFragment fragment = GalleryFragment.newInstance(mItems.get(position));
        ...
        return fragment;
    }

    @Override
    public int getCount() {
        return null == mItems ? 0 : mItems.size();
    }

    @Override
    public int getItemPosition(Object object) {
        //return super.getItemPosition(object);
        return PagerAdapter.POSITION_NONE;
    }
}

Gallery fragment:

public class GalleryFragment extends Fragment {

    private static final String GALLERY_ITEM_BUNDLE_KEY = "gallery_item_bundle_key";

    @Bind(R.id.gallery_item)
    ImageView mGalleryView;

    private Item.Gallery mGalleryItem;

    // Empty constructor, required as per Fragment docs
    public GalleryFragment() {}

    public static SearchResultsGalleryFragment newInstance(Item.Gallery galleryItem) {
        GalleryFragment fragment = new GalleryFragment();

        // Add the item in the bundle which will be set to the fragment
        Bundle bundle = new Bundle();
        bundle.putSerializable(GALLERY_ITEM_BUNDLE_KEY, galleryItem);
        fragment.setArguments(bundle);

        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mGalleryItem = (Item.Gallery) getArguments().getSerializable(GALLERY_ITEM_BUNDLE_KEY);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_gallery_item, container, false);
        ButterKnife.bind(this, view);

        displayGalleryItem();

        return view;
    }

    private void displayGalleryItem() {
        if (null != mGalleryItem) {
            Glide.with(getContext()) // Bind it with the context of the actual view used
                 .load(mGalleryItem.getImageUrl()) // Load the image
                 .centerCrop() // scale type
                 .placeholder(R.drawable.default_product_400_land) // temporary holder displayed while the image loads
                 .crossFade()
                 .into(mGalleryView);
        }
    }
}

The issue I'm having is that the fragments of the ViewPager aren't created and displayed correctly. Sometimes they appear after an manual scroll (but not always), in most of the cases they don't appear at all.

Does anyone have an idea on what I've implemented wrong?


Solution

  • I have managed to get around this issue by using PagerAdapter directly.

    import android.content.Context;
    import android.support.annotation.NonNull;
    import android.support.v4.view.PagerAdapter;
    import android.util.Log;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ImageView;
    import com.bumptech.glide.Glide;
    import com.bumptech.glide.load.DecodeFormat;
    import com.peoplepost.android.R;
    import com.peoplepost.android.common.listener.ItemClickSupport;
    import com.peoplepost.android.network.merv.model.Product;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * <p>
     * Custom pager adapter which will manually create the pages needed for showing an slide pages gallery.
     * </p>
     * Created by Ionut Negru on 13/06/16.
     */
    public class GalleryAdapter extends PagerAdapter {
    
        private static final String TAG = "GalleryAdapter";
    
        private final List<Item> mItems;
        private final LayoutInflater mLayoutInflater;
        /**
         * The click event listener which will propagate click events to the parent or any other listener set
         */
        private ItemClickSupport.SimpleOnItemClickListener mOnItemClickListener;
    
        /**
         * Constructor for gallery adapter which will create and screen slide of images.
         *
         * @param context
         *         The context which will be used to inflate the layout for each page.
         * @param mediaGallery
         *         The list of items which need to be displayed as screen slide.
         */
        public GalleryAdapter(@NonNull Context context,
                                                @NonNull ArrayList<Item> mediaGallery) {
            super();
    
            // Inflater which will be used for creating all the necessary pages
            mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    
            // The items which will be displayed.
            mItems = mediaGallery;
        }
    
        @Override
        public int getCount() {
            // Just to be safe, check also if we have an valid list of items - never return invalid size.
            return null == mItems ? 0 : mItems.size();
        }
    
        @Override
        public boolean isViewFromObject(View view, Object object) {
            // The object returned by instantiateItem() is a key/identifier. This method checks whether
            // the View passed to it (representing the page) is associated with that key or not.
            // It is required by a PagerAdapter to function properly.
            return view == object;
        }
    
        @Override
        public Object instantiateItem(ViewGroup container, final int position) {
            // This method should create the page for the given position passed to it as an argument.
            // In our case, we inflate() our layout resource to create the hierarchy of view objects and then
            // set resource for the ImageView in it.
            // Finally, the inflated view is added to the container (which should be the ViewPager) and return it as well.
    
            // inflate our layout resource
            View itemView = mLayoutInflater.inflate(R.layout.fragment_gallery_item, container, false);
    
            // Display the resource on the view
            displayGalleryItem((ImageView) itemView.findViewById(R.id.gallery_item), mItems.get(position));
    
            // Add our inflated view to the container
            container.addView(itemView);
    
            // Detect the click events and pass them to any listeners
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (null != mOnItemClickListener) {
                        mOnItemClickListener.onItemClicked(position);
                    }
                }
            });
    
            // Return our view
            return itemView;
        }
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            // Removes the page from the container for the given position. We simply removed object using removeView()
            // but could’ve also used removeViewAt() by passing it the position.
            try {
                // Remove the view from the container
                container.removeView((View) object);
    
                // Try to clear resources used for displaying this view
                Glide.clear(((View) object).findViewById(R.id.gallery_item));
                // Remove any resources used by this view
                unbindDrawables((View) object);
                // Invalidate the object
                object = null;
            } catch (Exception e) {
                Log.w(TAG, "destroyItem: failed to destroy item and clear it's used resources", e);
            }
        }
    
        /**
         * Recursively unbind any resources from the provided view. This method will clear the resources of all the
         * children of the view before invalidating the provided view itself.
         *
         * @param view
         *         The view for which to unbind resource.
         */
        protected void unbindDrawables(View view) {
            if (view.getBackground() != null) {
                view.getBackground().setCallback(null);
            }
            if (view instanceof ViewGroup) {
                for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
                    unbindDrawables(((ViewGroup) view).getChildAt(i));
                }
                ((ViewGroup) view).removeAllViews();
            }
        }
    
        /**
         * Set an listener which will notify of any click events that are detected on the pages of the view pager.
         *
         * @param onItemClickListener
         *         The listener. If {@code null} it will disable any events from being sent.
         */
        public void setOnItemClickListener(ItemClickSupport.SimpleOnItemClickListener onItemClickListener) {
            mOnItemClickListener = onItemClickListener;
        }
    
        /**
         * Display the gallery image into the image view provided.
         *
         * @param galleryView
         *         The view which will display the image.
         * @param galleryItem
         *         The item from which to get the image.
         */
        private void displayGalleryItem(ImageView galleryView, Item galleryItem) {
            if (null != galleryItem) {
                Glide.with(galleryView.getContext()) // Bind it with the context of the actual view used
                     .load(galleryItem.getImageUrl()) // Load the image
                     .asBitmap() // All our images are static, we want to display them as bitmaps
                     .format(DecodeFormat.PREFER_RGB_565) // the decode format - this will not use alpha at all
                     .centerCrop() // scale type
                     .placeholder(R.drawable.default_product_400_land) // temporary holder displayed while the image loads
                     .animate(R.anim.fade_in) // need to manually set the animation as bitmap cannot use cross fade
                     .thumbnail(0.2f) // make use of the thumbnail which can display a down-sized version of the image
                     .into(galleryView); // Voilla - the target view
            }
        }
    }
    

    And the updated onBindViewHolder() of the parent RecyclerView:

    @Override
    public void onBindViewHolder(MyHolder holder, int position) {
        super.onBindViewHolder(holder, position);
    
        Item listItem = get(position);
    
        ...
    
        GalleryAdapter adapter =
                        new GalleryAdapter(getActivity(), product.mediaGallery);
        // Set the custom click listener on the adapter directly
        adapter.setOnItemClickListener(new ItemClickSupport.SimpleOnItemClickListener() {
            @Override
            public void onItemClicked(int position) {
                // inner view pager page was clicked
            }
        });
        // Set the adapter on the view pager
        holder.imageGallery.setAdapter(adapter);
    
        ...
    }
    

    I noticed a little increase in memory usage, but the UI is very fluid. I guess some further optimizations can be made on how many pages are kept and how they are destroyed and restored.

    I hope this helps others in a similar situation.