28

I have created a very simple project, displaying 28 images with StaggeredGridLayoutManager by recyclerview. but as I scroll the recyclerview it moves items for example from left to right or swap the column of left and right.

codes:

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;


public class MainActivity extends Activity {


    String mImageDir;
    private RecyclerView mRecyclerView;
    private StaggeredGridLayoutManager mLayoutManager;



    MyRecyclerAdapter myRecyclerAdapter;
    List<ImageModel> mImageList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);   
        mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_rootview);
        mLayoutManager = new StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL);     
        mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);        
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setHasFixedSize(false);
        mImageList = new ArrayList<ImageModel>();
        for (int i = 1; i < 29 ; i++) {
            ImageModel img = new ImageModel();
            img.setTitle("Image No " + i);
            int drawableResourceId = this.getResources().getIdentifier("image"+String.valueOf(i), "drawable", this.getPackageName());
            img.setResId(drawableResourceId);
            mImageList.add(img);

        }
        myRecyclerAdapter = new MyRecyclerAdapter(MainActivity.this,mImageList);        
        mRecyclerView.setAdapter(myRecyclerAdapter);

    }

} 

And the adapter:

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


    private List<ImageModel> mItems;
    Context mContext;

    public MyRecyclerAdapter(Context context,List<ImageModel> objects) {
        mContext = context;
        mItems = objects;

    }

    static class ViewHolder extends RecyclerView.ViewHolder{
        public  ImageView mImageView;
        public  TextView mTextView;
        public View rootView;
        public ViewHolder(View itemView) {
            super(itemView);
            rootView = itemView;
            mImageView =(ImageView)itemView.findViewById(R.id.image);
            mTextView =(TextView)itemView.findViewById(R.id.title);
        }
    }

    @Override
    public int getItemCount() {

        return mItems.size();
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {

        ImageModel item = mItems.get(position); 
        Picasso.with(mContext).load(item.getResId()).into(holder.mImageView);
        holder.mTextView.setText(item.getTitle());
    }


    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int arg1) {
        LayoutInflater inflater =    
                (LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        View convertView = inflater.inflate(R.layout.item, parent, false);
        return new ViewHolder(convertView);

    }

}

and a sample moving item:

http://i.imgur.com/FUapm2K.gif?1

if you play (scroll up and down) you can discover more interesting animation :-)

How to prevent that and having stable layout like an ordinary listview?

Edit

@Override
public void onBindViewHolder(ViewHolder holder, int position) {

    ImageModel item = mItems.get(position);
    RelativeLayout.LayoutParams rlp = (RelativeLayout.LayoutParams)holder.mImageView.getLayoutParams();
    float ratio = item.getHeight()/item.getWidth();
    rlp.height = (int)(rlp.width * ratio);
    holder.mImageView.setLayoutParams(rlp);
    Picasso.with(mContext).load(item.getResId()).into(holder.mImageView);
    holder.mTextView.setText(item.getTitle());
}
mmlooloo
  • 18,937
  • 5
  • 45
  • 64
  • What did you end up doing? I have the same problem and answer by @yigit sounds good, but a code sample would immensely help. Where do you store this data and how to restore it? What unique ID do you use? – TruMan1 Jul 01 '15 at 14:18

8 Answers8

21

This is happening because SGLM does not keep any w/h information about the views. So each time a View is rebound, it gets the place holder size first and then the final size when the image is loaded.

Loading the actual image w/ different size (than place holder) triggers a new layout request, and during that layout request, SGLM detects that there will be gaps in the UI (or some item w/ higher position appears below an item w/ lower position) thus re-orders items w/ an animation.

You can avoid it by setting your place holder image to the dimensions of the actual image. If you don't have it ahead of time, you can save them after the image is loaded for the first-time and use it in the next onBind call.

yigit
  • 37,683
  • 13
  • 72
  • 58
  • I have tried using `getViewType()` but I still have the same issue maybe I am doing something wrong, do you know any sample code of `SGLM`? – mmlooloo Jan 15 '15 at 18:09
  • This has nothing to do w/ getViewType(). You need to save image dimensions locally so that, next time you rebind the view, you can set the correct dimensions on the ImageView to avoid resizing. – yigit Jan 16 '15 at 04:28
  • Your using Picasso so why don't you use a place holder image. That way if you use one that's the Dane size as your downloaded images you shouldn't get jumpy animation. – James andresakis Jan 16 '15 at 04:54
  • No, you are getting the dimensions from the image view, which does not have the final image dimensions anyways so that does not help. For each image, when it is loaded, you need to get the dimensions from the ImageView and save them so that next time you reload the image, you can set them on the ImageView. – yigit Jan 16 '15 at 05:00
  • For now I am just loading images from my drawable folder, so `item.getHeight()` and `item.getWidth()` have correct dimension of my images, now what should I do ? any sample code of SGLM ? – mmlooloo Jan 16 '15 at 05:07
  • I suggest to put a "loading" item at the bottom, that will trigger loading the extra images, and only get their resolution, which is how you will get to know the dimensions you want to use for the views. – android developer Mar 20 '15 at 23:04
  • I tried it and still jumps around when scrolling up. I don't think it's that easy actually. It's not just the sizes of the thumbnails, but it has to remember the placement of the all cards above it and not just the ones that come into view, because the cards are unevenly aligned it has no idea what is coming before. Same thing happens when scrolling down but that feels naturally as the page expands, not when going up though. It's much more complex than it seems. – TruMan1 Jul 01 '15 at 15:18
  • Truman, do you have a sample app that reproduces the issue? If your views don't resize and your adapter contents do not change, it should never jump. – yigit Jul 01 '15 at 15:23
  • I have to isolate it since the project is huge and proprietary. Here is the snippet in case you spot something I'm missing? http://pastie.org/10268417. I'm storing the sizes in `onCreateViewHolder` where they are available. Then restoring them in `onBindViewHolder` if found. – TruMan1 Jul 01 '15 at 16:22
  • @yigit you are right. I removed images from my staggered view so it can just render variable sized text views and no jumping. Still figuring the best way to handle remote images, placeholder resizing, bind time, etc. – TruMan1 Jul 02 '15 at 14:27
  • BTW, just checked your adapter. Your layout listener won't record correct values because at the time view is laid out, you don't have real bitmap anyways. If the URL does not have the dimensions, you have to handle it in your image loading logic. – yigit Jul 02 '15 at 14:36
  • Here have a look at my answer with github project: [link](http://stackoverflow.com/questions/34273295/android-recyclerview-staggeredgrid-items-change-position-when-scrolling-top/36759012#36759012) – YLS Apr 21 '16 at 03:40
  • Note that SGLM will always try to fill rows, so if you have variable-span items, when scrolling up after a reflow (the spans are flushed), it will always mess up in cases where there are gaps because spans are row-wrapped. – Pierre-Luc Paour Dec 09 '16 at 17:58
  • Thanks for the process explanation in such details. – Parth Patel Aug 11 '20 at 12:06
20

What worked for me was to disable all animation on the recycle view when using StaggeredGridLayoutManager.

mRecyclerView.setItemAnimator(null);

You can create your own animator if you only want to restrict moving animations and keep the add and remove item animations.

Sanchit
  • 460
  • 3
  • 14
18

You can try this

    StaggeredGridLayoutManager manager = new StaggeredGridLayoutManager(2, OrientationHelper.VERTICAL);
    manager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
    mRecyclerView.setLayoutManager(manager);

after you set this, you'll find there is a blank at the top when you scroll to the top. continue to set this

    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            ((StaggeredGridLayoutManager)recyclerView.getLayoutManager()).invalidateSpanAssignments();
        }
    });

It's work for me, I get this way somewhere. I hope it can help you!

TC.Martin
  • 181
  • 1
  • 3
5

Change this line

mLayoutManager.setGapStrategy(
        StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 

to this line

mLayoutManager.setGapStrategy(
        StaggeredGridLayoutManager.GAP_HANDLING_NONE);           
Matthias Robbers
  • 15,689
  • 6
  • 63
  • 73
  • No luck when I am downloading images from internet, still a lot of buggy animations occurs. Have you implemented anything with `StaggeredGridLayoutManager` – mmlooloo Dec 30 '14 at 20:26
5

Add the following line at the end of the method: holder.mImageView.requestLayout();

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    ...
    holder.mImageView.requestLayout();
}

This fixed the issue for me.

1

Maybe this is a more efficient way:

mBrandRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            if(newState == RecyclerView.SCROLL_STATE_IDLE){
                mBrandRecyclerView.invalidateItemDecorations();
            }
        }
    });

For more information: https://github.com/ibosong/CommonItemDecoration

Bos
  • 365
  • 2
  • 13
0

First set gap strategy like following code :

mLayoutManager = new StaggeredGridLayoutManager(SPAN_COUNT, StaggeredGridLayoutManager.VERTICAL);
                            mLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);

and then add your item to mItems and then use:

mAdapter.notifyItemInserted(mItems.size() - 1);

this method is better than using:

mAdapter.notifyDataSetChanged();
Android
  • 1,420
  • 4
  • 13
  • 23
0

Set your recyclerview's height fixed and item height and width wrap-content for staggered-layout-manager