Abhishek
Abhishek

Reputation: 2370

RecyclerView's row is shuffling and change Images : android

My issue is when Recycler View is scrolling Up and Down

Here how i am setting My Recycler view in Fragment.

private void setRecyclerView() {
    recyclerView.setHasFixedSize(true);
    StaggeredGridLayoutManager layoutManager =
            new StaggeredGridLayoutManager(2, 1);
    recyclerView.setLayoutManager(layoutManager);
    adapter = new TwitterTweetAdapter(getActivity());

    // setting every thing false in item animator 
    recyclerView.setItemAnimator(new RecyclerView.ItemAnimator() {

        @Override
        public void runPendingAnimations() {

        }

        @Override
        public boolean animateRemove(RecyclerView.ViewHolder viewHolder) {
            return false;
        }

        @Override
        public boolean animateAdd(RecyclerView.ViewHolder viewHolder) {
            return false;
        }

        @Override
        public boolean animateMove(RecyclerView.ViewHolder viewHolder, int i, int i2, int i3, int i4) {
            return false;
        }

        @Override
        public boolean animateChange(RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder viewHolder2, int i, int i2, int i3, int i4) {
            return false;
        }

        @Override
        public void endAnimation(RecyclerView.ViewHolder viewHolder) {

        }

        @Override
        public void endAnimations() {

        }

        @Override
        public boolean isRunning() {
            return false;
        }
    });
    recyclerView.setAdapter(adapter);
}

Here is My Adapter Class

In My Adapter I am setting One Header

//My ViewHolders

class Header extends RecyclerView.ViewHolder {

    @Bind(R.id.feed_head)
    CardView feedHeader;
    @Bind(R.id.feed_header_count)
    TextView userCount;
    @Bind(R.id.feed_header_text)
    TextView userText;

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

public static class RowViewHolder extends RecyclerView.ViewHolder {

    @Bind(R.id.feed_row_view)
    CardView feedRow;
    @Bind(R.id.feed_image)
    ImageView feedImage;
    @Bind(R.id.video_play_icon)
    ImageButton playButton;
    @Bind(R.id.feed_description)
    TextView feedDescription;
    @Bind(R.id.feed_user_image)
    ImageView userImage;
    @Bind(R.id.feed_user_name)
    TextView userName;
    @Bind(R.id.feed_time)
    TextView feedDate;
    @Bind(R.id.feed_progress_bar)
    ProgressBar progressBar;

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

// declaration of variables

private static final int TYPE_HEADER = 0;
private static final int TYPE_FEED = 1;

Context context;
private Activity activity;
private List<MockData> dataLists;
public static List<MockData> dataListsupdated;

int userSearchCount;

// constructor

public FeedAdapter(Activity activity) {
    this.activity = activity;
}

// setting data list

public void setDataList(List<MockData> dataLists, int userSearchCount, float density) {
    this.dataLists = dataLists;
    this.density = density;
    this.userSearchCount = userSearchCount;
}

// onCreateViewHolder

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
    if (viewType == TYPE_HEADER) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.feed_header, viewGroup, false);
        return new Header(view);
    } else {
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.feed_row, viewGroup, false);
        return new RowViewHolder(v);
    }
}

// onBindViewHolder i am bind my view with data.

if (holder instanceof Header) {
        Header header = (Header) holder;
        if (userSearchCount == 20) {
            header.userCount.setText(R.string.twitter_default_count);
        } else {
            header.userCount.setText(userSearchCount);
        }
        header.userText.setText(R.string.user);
        header.feedHeader.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(activity, TwitterSearchActivity.class);
                activity.startActivity(intent);
            }
        });

    } else {
        if (holder instanceof RowViewHolder) {
            final RowViewHolder rowViewHolder = (RowViewHolder) holder;
            final MockData responseList = dataLists.get(position);

            rowWidth = rowViewHolder.feedImage.getWidth();
            rowViewHolder.playButton.setVisibility(View.GONE);

            if (responseList.text.startsWith("RT")) {
                rowViewHolder.feedRow.setVisibility(View.GONE);
                FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
                params.setMargins(0, 0, 0, 0);
                rowViewHolder.feed.setLayoutParams(params);
            } else {
                setRowImage(rowViewHolder, responseList);
                rowViewHolder.feedDescription.setText(responseList.text);

                rowViewHolder.userImage.setVisibility(View.VISIBLE);
                Glide.with(activity)
                        .load(responseList.user.profileImageUrlHttps)
                        .into(rowViewHolder.userImage);

                rowViewHolder.userName.setText(responseList.user.screenName);

                Date date = new Date(responseList.createdAt);
                DateFormat dateFormat = android.text.format.DateFormat.getDateFormat(activity.getApplicationContext());
                rowViewHolder.feedDate.setText(dateFormat.format(date));

                rowViewHolder.feedRow.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        startActivity(position);
                    }
                });
            }
        }
    }

}

// setting Row Image.

private void setRowImage(RowViewHolder rowViewHolder, Tweet responseList) {
    if (responseList.entities.media != null) {
        MediaEntity mediaEntity = responseList.entities.media.get(0);
        if (mediaEntity.mediaUrlHttps != null) {
            int height = mediaEntity.sizes.medium.h;
            int width = mediaEntity.sizes.medium.w;

            int ratio = width / 143;
            int newHeight = (int) ((height / ratio) * density);

            Glide.with(activity)
                    .load(mediaEntity.mediaUrlHttps)
                    .override(width, newHeight)
                    .placeholder(R.color.colorAccent)
                    .into(rowViewHolder.feedImage);

        } else {
            rowViewHolder.feedImage.setImageDrawable(null);
        }
    } else {
        rowViewHolder.feedImage.setImageDrawable(null);
    }
}

// View condition to set header and row.

@Override
public int getItemViewType(int position) {
    if (isPositionHeader(position))
        return TYPE_HEADER;
    return TYPE_FEED;
}

private boolean isPositionHeader(int position) {
    return position == 0;
}

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

}

Upvotes: 9

Views: 8128

Answers (4)

Fathima km
Fathima km

Reputation: 2699

Override these 2 methods in your recyclerview adapter and change the code of getItemViewType as below.

 @Override
        public long getItemId(int position) {
            return position;
        }

    @Override
    public int getItemViewType(int position) {
             if (isPositionHeader(position)) {
        return TYPE_HEADER;
            }
             else {
               return position;
             }
           }

I also had the same problem. Do this. This will definitely solves your problem.

Upvotes: 0

PirateApp
PirateApp

Reputation: 6212

If you see the source code of the RecyclerView over HERE navigate to line 3028 which contains these lines

/**
 * Returns a unique key to be used while handling change animations.
 * It might be child's position or stable id depending on the adapter type.
 */
long getChangedHolderKey(ViewHolder holder) {
    return mAdapter.hasStableIds() ? holder.getItemId() : holder.mPosition;
}

and at line 5279 in the source file that contains this

/**
     * Return the stable ID for the item at <code>position</code>. If {@link #hasStableIds()}
     * would return false this method should return {@link #NO_ID}. The default implementation
     * of this method returns {@link #NO_ID}.
     *
     * @param position Adapter position to query
     * @return the stable ID of the item at position
     */
    public long getItemId(int position) {
        return NO_ID;
    }

it clearly indicates that overriding setHasStableIds needs to be followed by overriding getItemId method. To have each item inside your RecyclerView display the correct item when you scroll up and down getItemId cannot return a position or worse be left off. Make sure you return a unique long value that identifies each row in your data set.

What could be this unique value?

  1. A unique timestamp of when your twitter post was made if you have it?
  2. A computed unique long value such as hash over one or more items from your data set that would yield a unique value.

This is important for both animations and displaying the proper row at the correct position. Hope that helps fix your issue

Upvotes: 2

Tunji_D
Tunji_D

Reputation: 3687

The issue you have is that you are not implementing getItemID, or you are not implementing it properly.

If you set setHasStableIds to true and you do not implement getItemId to return a unique long for each item in the list, the RecyclerView WILL duplicate items. This is because the default implementation of getItemID returns -1 for all items in the list, therefore, when onBindViewHolder is called after a view is recycled, it's going to return a cached view of the last thing that ViewHolder displayed.

An easy way for you to implement this is just to return the hashcode for the object in the list bound to that ViewHolder.

@Override
public long getItemId(int position) {     
    Object listItem = listItems.get(position);
    return listItem.hashCode();  
}

Obviously, this means for that particular object, you must have overriden it's equals() and hashCode() methods to be unique for each object. A good idea is a String showing when it was created if from a database concatenated with the object's name.

Upvotes: 2

PirateApp
PirateApp

Reputation: 6212

I have a very similar Adapter in my app where I display feed from Facebook instead of Twitter, First of all, switch to Glide instead of Picasso since it has gives better caching and image loading with RecyclerView, check THIS. So coming to your problem now, call setHasStableIds(true) on your Adapter in the 1st step, override the getItemId method to return a suitable long from your Adapter, since you are using a feed, you can compute a hash say for the postId or some field that unique identifies each post within a feed. My feed has an optional image too which is why I have these 2 methods

public void setProfilePicture(String uri) {

        //As per the solution discussed here http://stackoverflow.com/questions/32706246/recyclerview-adapter-and-glide-same-image-every-4-5-rows
        if (uri != null) {
            Glide.with(mContext)
                    .load(uri)
                    .asBitmap()
                    .transform(new CropCircleTransform(mContext))
                    .into(mProfilePicture);
        } else {
            Glide.clear(mProfilePicture);
            mProfilePicture.setImageResource(R.drawable.com_facebook_profile_picture_blank_square);
        }
    }

    public void setPostPicture(String uri) {

        //As per the solution discussed here http://stackoverflow.com/questions/32706246/recyclerview-adapter-and-glide-same-image-every-4-5-rows
        if (uri != null) {
            Glide.with(mContext)
                    .load(uri)
                    .asBitmap()
                    .transform(new CropTransformation(mContext, mPostImageWidth, mPostImageHeight))
                    .into(mPostPicture);
        } else {
            Glide.clear(mPostPicture);
            mPostPicture.setImageDrawable(null);
        }
    }

and then all you gotta do is call these methods from your onBindViewHolder to set the Image. Post is my object that contains the details of a single post such as name, userid , image url for the person s picture and image url for the post picture which is optional. Let me know if you encounter any issues further.

@Override
    public void onBindViewHolder(ItemHolder holder, int position) {
        Post post = mResults.get(position);
        holder.setUserName(post.getUserName());
        holder.setUpdatedTime(post.getUpdatedTime());
        holder.setMessage(post.getMessage(), mState, position);
        holder.setPostPicture(post.getPicture());
        holder.setProfilePicture(post.getUserPicture());
        // Check for an expanded view, collapse if you find one

    }

Upvotes: 3

Related Questions