Viewpager inside recyclerview adapter

viewpager with recyclerview in android example
recyclerview viewpager android github
recyclerview inside vertical viewpager
recyclerview inside viewpager2
android recyclerview header viewpager
viewpager in recyclerview item
nested viewpager android
recyclerview in viewpager fragment

I am working on an app where there is a list of item and each item consist of a viewpager and below the viewpager you have other widgets (text, button....etc). I have to load url of images from the server and show the images inside my viewpager

Here is my Viewpager adapter:

public class ImageAdapter extends PagerAdapter {

private Context mContext;
private ArrayList<String> imagePaths;
private static final String TAG = ImageAdapter.class.getName();
// constructor
public FullScreenImageAdapter(Context context, ArrayList<String> imagePaths){
    this.mContext = context;
    this.imagePaths = imagePaths;
}

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

@Override
public Object instantiateItem(final ViewGroup container, final int position) {

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    final View viewLayout = inflater.inflate(R.layout.full_screen_image, container, false);

    final ImageView image = (ImageView) viewLayout.findViewById(R.id.image);
    final ProgressBar progressBar = (ProgressBar)viewLayout.findViewById(R.id.pBar);

    String url = BuildConfig.URL_API_IMAGES;
    final SparseArray<Object> mBitmapMap = new SparseArray<>();

    Glide.with(mContext)
         .load(url + imagePaths.get(position)).asBitmap()
         .placeholder(R.drawable.car)
         .dontAnimate()
         .into(new BitmapImageViewTarget(image){
             @Override
             public void onLoadStarted(Drawable placeholder){
                 super.onLoadStarted(placeholder);
                 image.setImageDrawable(placeholder);
             }

             @Override
             public void onResourceReady(Bitmap resource,
                     GlideAnimation<? super Bitmap> glideAnimation){
                 super.onResourceReady(resource, glideAnimation);
                 mBitmapMap.put(position, resource);
                 progressBar.setVisibility(View.INVISIBLE);
                 image.setImageBitmap(resource);
             }

             @Override
             public void onLoadFailed(Exception e, Drawable errorDrawable){
                 super.onLoadFailed(e, errorDrawable);
                 progressBar.setVisibility(View.INVISIBLE);
             }
         });

    container.addView(viewLayout);
    return viewLayout;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    try{
        container.removeView((RelativeLayout) object);
        Glide.clear(((View) object).findViewById(R.id.image));
        unbindDrawables((View) object);
        object = null;
    }catch (Exception e) {
        Log.w(TAG, "destroyItem: failed to destroy item and clear it's used resources", e);
    }
}

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();
    }
}

@Override
public boolean isViewFromObject(View view, Object object){
    return view == ((RelativeLayout) object);
}

}

Here is a portion of my recycler view item:

<RelativeLayout
    android:id="@+id/rl_pictureLayout"
    android:layout_width="match_parent"
    android:layout_marginLeft="@dimen/dimen_5"
    android:layout_marginRight="@dimen/dimen_5"
    android:layout_marginTop="@dimen/dimen_5"
    android:layout_height="@dimen/sliderLayoutHeight">

    <ViewPager
        android:id="@+id/vp_photos"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <me.relex.circleindicator.CircleIndicator
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_marginBottom="50dp"
        android:layout_height="40dp"
        android:layout_alignParentBottom="true"
        />

    <ImageButton
        android:id="@+id/ib_bookmark"
        android:background="@color/transparent"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:layout_marginTop="@dimen/dimen_16"
        android:layout_marginRight="@dimen/dimen_16"
        android:layout_marginEnd="@dimen/dimen_16"
        android:src="@drawable/ic_bookmark_border_white_24dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <TextView
        android:id="@+id/tv_picture_counter"
        android:textColor="@color/white"
        android:textAllCaps="true"
        android:fontFamily="sans-serif"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginLeft="@dimen/dimen_16"
        android:layout_marginStart="@dimen/dimen_16"
        android:layout_marginBottom="@dimen/dimen_24"
        />

    <TextView
        android:id="@+id/tv_damagedPhotos"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:fontFamily="sans-serif"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_marginEnd="@dimen/dimen_16"
        android:layout_marginRight="@dimen/dimen_16"
        android:layout_marginBottom="@dimen/dimen_24"
        android:background="@color/transparent"
        android:drawableRight="@drawable/ic_photo_camera_red_24dp"
        android:drawableEnd="@drawable/ic_photo_camera_red_24dp"
        android:drawablePadding="@dimen/dimen_5"
        />
</RelativeLayout>

<RelativeLayout
    android:id="@+id/rl_timerLayout"
    android:layout_below="@id/rl_pictureLayout"
    android:layout_marginTop="@dimen/dimen_5"
    android:layout_alignLeft="@id/rl_pictureLayout"
    android:layout_alignStart="@id/rl_pictureLayout"
    android:layout_alignRight="@id/rl_pictureLayout"
    android:layout_alignEnd="@id/rl_pictureLayout"
    android:layout_marginBottom="@dimen/dimen_10"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_timer"
        style="@style/text_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true"
        android:textColor="@color/ColorBlack"
        android:textStyle="bold"
        />

    <TextView
        android:id="@+id/tv_highest_bid"
        style="@style/text_item"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        />

    <Button
        android:id="@+id/btn_bid"
        style="@style/button"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:textAllCaps="false"
        android:background="@drawable/curved_border_bgd_red"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:stateListAnimator="@null"
        android:text="@string/btn_text_bid"
        />
</RelativeLayout>

Here is my (partial) viewholder class inside the recyclerview adapter

static class ViewHolder extends RecyclerView.ViewHolder
{
    @BindView(R.id.indicator)
    CircularIndicator mCircleIndicator;
    @BindView(R.id.vp_photos)
    ViewPager mViewPagerPhotoView;

    public ViewHolder(View itemView)
    {
        super(itemView);
        ButterKnife.bind(this, itemView);
    }
}

In onBindViewHolder this is what i've got (as far as the viewpager is concerned):

Item item = mDataSet.get(position);
mImageAdapter = new ImageAdapter(mContext, item.getUrlPhotos());
holder.mViewPagerPhotoView.setAdapter(mImageAdapter);
holder.mViewPagerPhotoView.setOffscreenPageLimit(mImageAdapter.getCount() - 1);
holder.mCircleIndicator.setViewPager(holder.mViewPagerPhotoView);

Right now my reclerview has 10 items that I load from the server. Thing is, I noticed that when I scroll down say to the 7th item of my list, everything seems fine but when I scroll up back to see say the 1st item of my recycler list, the images found in viewpager of the 1st item tend to disappear and are (temporarily) replaced by the placeholder image i.e the viewpager is completely emptied and I temporarily have the placeholder; if the item is still visible on my screen the images for the viewpager a loaded again from the server.. I picked Glide because it's well known for it's caching capabilities. Moreover, a viewpager can have upto 20 IMAGES !!...

Does anyone have an idea on how to solve this ?? Am I doing anything wrong ?? Any help will be appreciated.....

The "problem" is that the items of the recyclerview will be recycled. You never save the loaded images. If you scroll down and up, onBindViewHolder will be called and a new adapter instance will be created so the images will be loaded again and again on every call of onBindViewHolder. The solution is to save the adapter object (in a map -> position, adapter) and bind it inside of onBindViewHolder.

ImageAdapter adapter = adaptermap.get(postion);
int adapterPosition = 0;
if(adapter == null) {
  adapter = new ImageAdapter(mContext, item.getUrlPhotos());
  adaptermap.put(position, adapter);
  adapterPosition = viewPageStates.get(position);
}

holder.mViewPagerPhotoView.setAdapter(adapter);
holder.mViewPagerPhotoView.setCurrentItem(adapterPosition);

You also have to save the position of the viewpager.

Put this as a member:

// you also can use a SparseIntArray for better performance
// if the itemcount is less than a few hundret
public HashMap<Integer, Integer> viewPageStates = new HashMap<>();

Also add this in your recyclerview adapter:

@Override
public void onViewRecycled(YourViewHolderClass holder) {
  int position = holder.getAdapterPosition();
  viewPageStates.put(position, holder.mViewPagerPhotoView.getCurrentItem());
  super.onViewRecycled(holder);
}

Important:

My solution will only work if you don't add / remove / sort items because we save adapter positions. If you also need it you have to modify the maps on add / delete / sort etc

Viewpager as a item in RecyclerView � Issue #210 � airbnb/epoxy , Yes I am trying to use the ViewPager inside a vertical RecyclerView. The EpoxyModel is binding the PagerAdapter for the ViewPager in the� If you wish to connect your ViewPager with new support library TabLayout, it is easily one with: tabLayout.setupWithViewPager(viewPager); Finally, if you will update your "fragmented" ViewPager, don't try to reset the adapter, as fragments are managed not with adapter, but with FragmentManager.

Well the problem is about image. You said "a viewpager can have upto 20 IMAGES", I don't know what do you mean by upto 20. ViewPager design to be unlimited item unless you load all item at one which doesn't make sense.

So as you describe, I guess the problem is your image is being recycle that why when you scroll up you won't see your image again until Glide load the image and render it again.

So the solution is not quite simple to deal but first let me explain about image and viewpager. Let said you have 10 items in recyclerview adapter and recyclerview loaded only 5 items as necessary. So now the minimum total image that need to be in the memory is 5 x 2 (Each viewpager need at least 2 images) equal 10 images in total. This is huge and it depend on your image, if your image is quite large you will run out of memory and that when Glide will destroy some image to make space for the new coming. Hope you can see the impact of image being loaded.

Now my suggestion:

  1. Make sure your Glide has diskcache configuration, this way your image will not loaded from the server one it loaded as long as the diskcache still has available space.
  2. Shrink your image to fit your view. Example original image is 1440x320 and your view is only 1080x240 then it better to load 1080x240 into memory. Or you can even make it a little smaller than 1080x240.
  3. Let Glide control the cache with builder.setMemoryCache(new LruResourceCache(yourSizeInBytes))

You can however make use of RecyclerView.ViewCacheExtension to manage your own view recycler but I don't see it would help. Moreover you can also implement cache view for Pager adapter, ViewPager deos not recycler any view so when ViewPager need a View it simply call instantiateItem and View will be created.

sunil676/RecyclerViewViewpager: View pager inside recyclerView , View pager inside recyclerView in android. Contribute to sunil676/ RecyclerViewViewpager development by creating an account on GitHub. The RecyclerView inside the Fragments will have 3 or 4 types of ViewModels. In general the nested scrolling should be an Epoxy Model whose view is the viewpager/recyclerview. That model passes on the data to the pager and the pager can initialize its subviews.

Try to keep ImageAdapter's instance in ViewHolder:

static class ViewHolder extends RecyclerView.ViewHolder
{
    @BindView(R.id.indicator)
    CircularIndicator mCircleIndicator;
    @BindView(R.id.vp_photos)
    ViewPager mViewPagerPhotoView;
    ImageAdapter mAdapter;

    public ViewHolder(View itemView)
    {
        super(itemView);
        ButterKnife.bind(this, itemView);

        mAdapter = new ImageAdapter();
        mViewPagerPhotoView.setAdapter(mAdapter);
    }

    //Add bind() method which will be called in onBindViewHolder()
    public void bind(ArrayList<String> imagePaths) {
        //... maybe some other bindings

        mAdapter.init(imagePaths);
        mViewPagerPhotoView.setOffscreenPageLimit(mAdapter.getCount() - 1);
        mCircleIndicator.setViewPager(mViewPagerPhotoView);
    }
}

Create init(ArrayList<String> imagePaths) method in your adapter:

void init(ArrayList<String> paths) {
      this.imagePaths = paths;
      notifyDataSetChanged();
}

It should help. If it didn't helped then something is wrong in your using of Glide.

ViewPager inside RecyclerView - android-fragments - iOS, I have RecyclerView with about 20 viewpagers (viewpager with fragment) I want to display different pictures inside this viewpagers with the hel of fragments. In summary, to convert a ViewPager adapter class for use with ViewPager2, you must make the following changes: Change the superclass to RecyclerView.Adapter for paging through views, or FragmentStateAdapter for paging through fragments. Change the constructor parameters in fragment-based adapter classes. Override getItemCount() instead of

How to retain ViewPager state inside a RecyclerView?, My RecyclerView contains many ViewPagers. The number of pages inside each ViewPager is constant (2). I use PagerAdapter. When I scroll my RecyclerView,� In this way, you can use the RecyclerView.Adapter with VP2 in your Adapter class. RecyclerView is so robust and brings a lot of advantages and it also has modifiable fragment collections and more. These changes below are the reason to choose ViewPager2 over ViewPager.

Infinitive Scrolling ViewPager inside RecyclerView, Adding ViewPager as an item RecyclerView.ViewHolder; Scrolling infinitely the RecyclerView inside ViewPager. You can read this blog post. Base class providing the adapter to populate pages inside of a ViewPager. PagerTabStrip: PagerTabStrip is an interactive indicator of the current, next, and previous pages of a ViewPager. PagerTitleStrip: PagerTitleStrip is a non-interactive indicator of the current, next, and previous pages of a ViewPager. ViewPager

ViewPager2 on the Outside, RecyclerView Inside, The well known ViewPager class is getting a re-write from scratch and it It works with both FragmentStateAdapter and RecyclerView.Adapter. When updating to RecyclerView 1.2.0-alpha02 also update ViewPager2 to prevent Accessibility regressions. New Features Added FragmentTransactionCallback interface for listening to fragment lifecycle changes that happen inside FragmentStateAdapter .

Comments
  • From what I read, you mean to say that in onBindViewHolder I'll have to instantiate twice ImageAdapter: From this statement: ImageAdapter adapter = adaptermap.get(postion); I suppose that the adaptermap must have as value ImageAdapter... i.e adapter.put (position, myImageAdapterInstance)...So at what point will I have to initialize myImageAdapterInstance ?
  • The code above should be in onBindViewHolder so you're right. First, you check if the adapter is already inside the map. If not you have to initialize the new adapter. I forgot that you have to put the new adapter into the map.
  • Let me get this straight: I'll have to initialise ImageAdapter twice in onBindViewHolder . The 1st time will be with an empty constructor: I put the myImageAdapterInstance in adaptermap like this: adaptermap.put(position, new ImageAdapter())...Afterwards (i.e the 2nd time) I will have to insert the snippet you wrote above ??....Is that what you're saying ??
  • No you initialize the imageadapter one time in onBindViewHolder for each position. To understand my code snippet: with adaptermap.get(postion) you ask the adaptermap to get the imageadapter for the given position. But if this adapter is not initialized yet it returns null. Then you have to initialize the adapter and add it to the adaptermap. So when you scroll and onBindViewHolder ask the adaptermap again, it will return the right adapter that you already initialized before.
  • Actually, @beeb, I tried your approach and the images don't get loaded anymore whenever I scroll vertically in my recyclerview....However I can't swipe the viewpager images..irrespective of the item position in my recyclerview..In my viewpager, whenever I try to swipe I keep coming back to the 1st picture in the vp...i.e Iswipe to the 2nd item in the vp; the image appears in a fraction of seconds then by some "magic" the vp currentitem position is always set to 0 and the vp bounces back to the 1st item
  • I did set up a diskcache configuration for Glide and now everything works perfectly...The images once loaded do not disappear anymore on scrolling..Thx
  • I tried this, but the CircleIndicator is not visible once the items are loaded....I mean the indicator is literally invisible.. And I get a crash on trying to swipe on the pictures for one item.. Here is the trace: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setBackgroundResource(int)' on a null object reference at me.relex.circleindicator.CircleIndicator.onPageSelected(CircleIndicator.java:157)
  • I removed the CircularIndicator (not because it caused a crash......but because we didn't needed it at the end)..however, the viewpager swipe doesn't work...Whenever I swipe to the 2nd item the viewpager comes back to the first item
  • Finaly I had it working !!!....It turns out that I didn't need to set any keymap on my viewholder..I configured a cache module as per @ vsatkh below and combined with your idea of keeping ImageAdapter's instance in ViewHolder solved my issue.....Thx mate
  • Hi @ClaudeHangui I know it's been a long time but I hope you may remember your solution. So could you share your code please?I have same problem and I couldn't solve it for days.