Claude Hangui
Claude Hangui

Reputation: 427

Viewpager inside recyclerview adapter

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.....

Upvotes: 4

Views: 10923

Answers (3)

beeb
beeb

Reputation: 1615

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

Upvotes: 3

Sergio
Sergio

Reputation: 30735

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.

Upvotes: 1

vsatkh
vsatkh

Reputation: 189

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.

Upvotes: 2

Related Questions