Android: RecyclerView content messed up after scrolling [closed] - android

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
I'm using RecyclerView to display a list of marks, and each mark of the value is shown as a CardView. But some contents of the cards are lost after the scrolling the RecyclerView down and scrolling back, as shown in the two screenshots below. The contents in the red rectangle is lost after scrolling.
BEFORE THE SCROLLING;
AFTER THE SCROLLING;
I'm wondering whether or not it's a bug of RecyclerView and find no solution after Googling for it.
All views are invisible except the title, their visibilities are depends on the mark's value.
Does anyone know why this would happen?

After battling with this same issue for about 24 hours, I found a solution that worked for me. The key was using the setIsRecyclable() method of RecyclerView.ViewHolder class.
Here is a section of my onBindViewHolder() code.
#Override
public void onBindViewHolder(final MyViewHolder holder, int position) {
final DataSource dataSource = dataSourceList.get(position);
holder.setIsRecyclable(false);
holder.name.setText(dataSource.getName());
holder.active.setChecked(dataSource.getActive());
String logoStr = dataSource.getLogo();
//Logo
/**
* Do all the logo insertion stunts here
*/
/**
* Register the changes to the Switch
*/
holder.active.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener(){
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked){
dataSource.setActive(isChecked);
}
});
}

onBindHolder() is called several times as recycler needs a view, unless there's a new one when view type is changed.
So each time you set visibility in child views, other views states are also changing.
Whenever you scroll up and down, these views are getting re-drawn with wrong visibility options.
Solution :
You have a setValue method check values and set to view. If neccessary it calls another method "showView". You need to implement else statement (which is value is 0 or null) and hideView there...
void setValue(Object value, TextView textView, TableRow row, View seperator) {
if (value != null) {
if (!isEmpty(value.toString())) {
textView.setText(String.valueOf(value));
showViews(row, seperator);
}
} else
hideViews(row, seperator);
}
private void showViews(TableRow row, View seperator) {
row.setVisibility(View.VISIBLE);
seperator.setVisibility(View.VISIBLE);
}
private void hideViews(TableRow row, View seperator) {
row.setVisibility(View.INVISIBLE); // if there is a empty space change it with View.GONE
seperator.setVisibility(View.INVISIBLE);
}

onBindHolder called several times as Recycler View needs a view unless new one. So each time you set visilibity in child views, other views states are also changes.
Whenever you scroll up and down, these views are getting re-drawed with wrong visibility options so always specify both the conditions cause recycler view doesn't know the previous state/conditions/values of our widgets.
Solution :
If in If block you set visibility of any android widget.setVisibility(View.Gone) then in else block you have to set it's visibility opposite vwith widget.setVisibility(View.Visible) to overcome the above problem.
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
viewHolder.tvName.setText(ModelCategoryProducts.name.get(i));
viewHolder.tvPrice.setText("Rs."+String.format("%.2f", Float.parseFloat(ModelCategoryProducts.price.get(i))));
if(ModelCategoryProducts.special_price.get(i).equals("null")) {
viewHolder.tvSpecialPrice.setVisibility(View.GONE); // here visibility is gone and in else it's opposite visibility i set.
viewHolder.tvPrice.setTextColor(Color.parseColor("#ff0000"));
viewHolder.tvPrice.setPaintFlags(0);// here paint flag is 0 and in else it's opposite flag that i want is set.
}else if(!ModelCategoryProducts.special_price.get(i).equals("null")){
viewHolder.tvPrice.setTextColor(Color.parseColor("#E0E0E0"));
viewHolder.tvSpecialPrice.setVisibility(View.VISIBLE);
viewHolder.tvSpecialPrice.setText("Rs." + String.format("%.2f", Float.parseFloat(ModelCategoryProducts.special_price.get(i))));
viewHolder.tvPrice.setPaintFlags(viewHolder.tvPrice.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
if (!ModelCategoryProducts.image_url.get(i).isEmpty()) {
Picasso.with(context)
.load(ModelCategoryProducts.image_url.get(i))
.into(viewHolder.ivProduct);
}
viewHolder.setClickListener(new ItemClickListener() {
#Override
public void onClick(View view, int position, boolean isLongClick) {
if (isLongClick) {
// Toast.makeText(context, "#" + position + " - " + ModelCategoryProducts.name.get(position) + " (Long click)", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "#" + position + " - " + ModelCategoryProducts.name.get(position), Toast.LENGTH_SHORT).show();
Intent i = new Intent(context, ProductDetail.class);
i.putExtra("position",position);
i.putExtra("flagHlvCheck", 5);
context.startActivity(i);
}
}
});
}

If you are facing problems with the the visibility issue for ex. if u have set the visibility of the view based on condition , but if you scroll down, the views get refreshed and the visibility of the view inside that recycler view item gets changes and hence violating the condition.
Here's a solution:
1. First assign tag to that view whose visibility you want to maintain.
holder.myImageView.setTag(myTeamLists.get(position));
MyDTOClass checkWetherToShow=(MyDTOClass)holder.myImageView.getTag();
2. Now apply your condition and toggle the visibility
if (checkWetherToShow.getHasToShowImage()){
holder.myImageView.setVisibility(View.VISIBLE);
}else{
holder.myImageView.setVisibility(View.GONE);
}
The key to the answer here is don't forget the else part.

This is due to the views being reused when scrolling occurs. To fix this you will need to reset any views that have been made visible for other cells (YUZME in your example).
Inside setValue(Object value, TextView textView, TableRow row, View seperator) simply make all txtVize* hidden again.
Recycler view starts off with 3 views:
[0] FIZ104
[1] MAT102
[2] REK361
When the view is scrolled to the bottom views [0] and [1] are recycled. When you scroll back to the top view [2] is used to display the data contained in FIZ104 and MAT102 and any changes made for REK361 are still there.

Related

How to smooth scroll RecyclerView if item is not fully visible

I want to implement this
.
I want to check if the clicked item is fully visible and if it's not I would like to smoothly scroll upwards/downwards. I have a GridLayoutManager with 3 columns. The items are all of the same size, and are just ImageViews.
I am able to get the RecyclerView to scroll with:
#Override
public void onClick(View view) {
int adapterPosition = RecHolder.this.getAdapterPosition();
mRecyclerView.scrollToPosition(adapterPosition);
...
}
But it's not a "scroll", it's very laggish and way too quick.
If I try to use mRecyclerView.smoothScrollToPosition(adapterPosition); the result is the same. Exactly the same movement, there is no visible difference.
Don't bother testing to see if it's not completely visible. Just insert a command to scroll to that position. Since you didn't post any of your code I can't specifically say how you would do it. I personally have my adapter create an intent and my activity handles the intent. If that's what you're doing then you can include the getAdapterPosition() as an extra like this (vh is my ViewHolder).
vh.mImageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
SelectItemGridAdapter.ContentViewHolder vh = (SelectItemGridAdapter.ContentViewHolder) (((View)v.getParent()).getTag());
Intent intent = new Intent(ACTION_SELECT_Item);
intent.putExtra(Constants.MSG_TYPE, SELECT_ITEM_TAPPED);
intent.putExtra(SELECT_ITEM_TAPPED_ID, vh.viewModel.mItemId);
intent.putExtra(SELECT_ITEM_TAPPED_POS, vh.getAdapterPosition());
LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
}
});
Then the reveiver in the activity can get the SELECT_ITEM_TAPPED_POS value...
int pos = intent.getIntExtra(SelectItemGridAdapter.SELECT_ITEM_TAPPED_POS, -1);
if (pos > -1)
rv.scrollToPosition(pos);
HTH, Mike

RecyclerView keeps repeating cached (?) subview and not looking to onBindViewHolder

I have RecyclerView where every list item has an ImageButton, thee image of which I define in the adapter's onBindViewHolder():
int myVote = getMyVote();
if (myVote != 0) {
Log.d("dbg", myVote + "");
holder.ratingButton.setImageResource(R.drawable.ic_star_grey600_36dp);
}
So ratingButton is a star in the right bottom corner of the list item layout. Its shape is filled with gray color (and accordingly, a log record is pushed) if the condition (myVote != 0) is satisfied.
The problem is that when I scroll the list down I can watch other stars became filled even though I can see the only one record in the log window (for the correct list item). Moreover, this list items with incorrectly changed buttons repeat every 5 rows, and that's what's confusing me. If I changemListView.setItemViewCacheSize(0); the repeat period changes to 3, so we can assume it somehow connected with the RecyclerView's caching and recycling mechanism.
Please, help me to work the problem out. Thanks!
Don't forget to implement
public long getItemId(int position) {}
otherwise, you'll see repeated items in RecyclerView.
Try to change your code to:
if (myVote != 0) {
Log.d("dbg", myVote + "");
holder.ratingButton.setImageResource(R.drawable.ic_star_grey600_36dp);
} else {
holder.ratingButton.setImageResource(int another resource);
}
}
You may also have to write else part of main condition with some another resource like:
if (myVote != 0) {
Log.d("dbg", myVote + "");
holder.ratingButton.setImageResource(R.drawable.ic_star_grey600_36dp);
} else {
holder.ratingButton.setImageResource(int another_resource);
}
It is worked for me.

I have 200 textviews and i want to know which one was pressed then how to change the text

I have a problem... have been thinking about it for a while now and been looking on line and still haven't come up with a clear explanation...
I have a number of textviews and have set onClickListeners to each of them.. and when the user clicks on one of them I want them to have the ability to change the text to another set of string array options which I have created progammatically. When the user selects an option the text should change to the option they choose. (I.e. TextView was A now it is B. hope this makes sense.. anyway... )
The current solution was to set a OnClickListener to every TextView and when someone pressed it an individual dialog showed. But I found that if I do this the code would be so long it would take an eternity to code so am hoping someone has a more elegant way of coding such a long process =(
So I guess my question would be... 1) is there a way I can find out which text view was pressed and then change the text of that TextView being pressed within a single method? to save me having to code 1000 alert dialogs...
http://i.stack.imgur.com/LRJGz.png
I would advise you to use a grid view.
You can see which textview was pressed like this:
gridView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View v, int position,
long id) {
//get id
switch (v.getId()) {
case R.id.textView1: ...
}
});
One of the ways to do what you want is to use the text view setTag() and getTag() methods.
On init of a text view use the setTag() to set some value to identify the view.
In the on click event use the getTag() on the view argument to know which view was clicked.
I would suggest holding the textviews in an array, like so:
TextView[] textViewArray = new TextView[textViewCount];
Then using a for loop assign each one a tag of integer - it's position
textViewArray.setTag(i)
And add an onClickListener to each one, again using a for loop:
textviewArray[i].setOnClickListener(etc...)
Then when one is clicked, you can use get the position of view that was clicked. This will require a custom method inside of your:
textviewArray.setOnClickListener(new customOnClickListener())
Where your customOnClickListner is like this:
private class customOnClickListener implements CompoundButton.{
public void OnClick(View view){
int position = (Integer) view.getTag()
///Do more code here - your processing
}
}
Hope that makes sense :)
For your for loops, you could use for(i = 0, i
Use set id for all text, where set the id positive integer(distinct), and then have one on view click listener(set it all) where u catch all text view clicks(downcast view with textview) and in side it put a switch case where you handle clicks on which text view is clicked.
You have to set "onClickListner" on all of of your textview.
For Saving some length of code i would suggest you create a function of your dialogbox, and give some int parameter to it, which would be directly called by the clickListener of textview,
Like ,
int i=0;
......
textView1 = (TextView)findViewById(R.id.yourtextview1);
textView2 = (TextView)findViewById(R.id.yourtextview2);
......
......
// and so on, for your all textviews
#Override
public void onClick(View view) {
if (view.equals(textView1)) {
i = 1;
CustomDialog(i);
}
//Similarly for all your textViews..
..........
Make A function CustomDialog Like
public void CustomDialog(int i){
if(i==1){
//Do something
}
}

Adding items to ListView, maintaining scroll position and NOT seeing a scroll jump

I'm building an interface similar to the Google Hangouts chat interface. New messages are added to the bottom of the list. Scrolling up to the top of the list will trigger a load of previous message history. When the history comes in from the network, those messages are added to the top of the list and should not trigger any kind of scroll from the position the user had stopped when the load was triggered. In other words, a "loading indicator" is shown at the top of the list:
Which is then replaced in-situ with any loaded history.
I have all of this working... except one thing that I've had to resort to reflection to accomplish. There are plenty of questions and answers involving merely saving and restoring a scroll position when adding items to the adapter attached to a ListView. My problem is that when I do something like the following (simplified but should be self-explanatory):
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
Then what the user will see is a quick flash to the top of the ListView, then a quick flash back to the right location. The problem is fairly obvious and discovered by many people: setSelection() is unhappy until after notifyDataSetChanged() and a redraw of ListView. So we have to post() to the view to give it a chance to draw. But that looks terrible.
I've "fixed" it by using reflection. I hate it. At its core, what I want to accomplish is reset the first position of the ListView without going through the rigamarole of the draw cycle until after I've set the position. To do that, there's a helpful field of ListView: mFirstPosition. By gawd, that's exactly what I need to adjust! Unfortunately, it's package-private. Also unfortunately, there doesn't appear to be any way to set it programmatically or influence it in any way that doesn't involve an invalidate cycle... yielding the ugly behavior.
So, reflection with a fallback on failure:
try {
Field field = AdapterView.class.getDeclaredField("mFirstPosition");
field.setAccessible(true);
field.setInt(listView, positionToSave);
}
catch (Exception e) { // CATCH ALL THE EXCEPTIONS </meme>
e.printStackTrace();
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
}
}
Does it work? Yes. Is it hideous? Yes. Will it work in the future? Who knows? Is there a better way? That's my question.
How do I accomplish this without reflection?
An answer might be "write your own ListView that can handle this." I'll merely ask whether you've seen the code for ListView.
EDIT: Working solution with no reflection based on Luksprog's comment/answer.
Luksprog recommended an OnPreDrawListener(). Fascinating! I've messed with ViewTreeObservers before, but never one of these. After some messing around, the following type of thing appears to work quite perfectly.
public void addNewItems(List<Item> items) {
final int positionToSave = listView.getFirstVisiblePosition();
adapter.addAll(items);
listView.post(new Runnable() {
#Override
public void run() {
listView.setSelection(positionToSave);
}
});
listView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if(listView.getFirstVisiblePosition() == positionToSave) {
listView.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
else {
return false;
}
}
});
}
Very cool.
As I said in my comment, a OnPreDrawlistener could be another option to solve the problem. The idea of using the listener is to skip showing the ListView between the two states(after adding the data and after setting the selection to the right position). In the OnPreDrawListener(set with listViewReference.getViewTreeObserver().addOnPreDrawListener(listener);) you'll check the current visible position of the ListView and test it against the position which the ListView should show. If those don't match then make the listener's method return false to skip the frame and set the selection on the ListView to the right position. Setting the proper selection will trigger the draw listener again, this time the positions will match, in which case you'd unregister the OnPreDrawlistener and return true.
I was breaking up my head until I found a solution similar to this.
Before adding a set of items you have to save top distance of the firstVisible item and after adding the items do setSelectionFromTop().
Here is the code:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// for (Item item : items){
mListAdapter.add(item);
}
// restore index and top position
mList.setSelectionFromTop(index, top);
It works without any jump for me with a list of about 500 items :)
I took this code from this SO post: Retaining position in ListView after calling notifyDataSetChanged
The code suggested by the question author works, but it's dangerous.
For instance, this condition:
listView.getFirstVisiblePosition() == positionToSave
may always be true if no items were changed.
I had some problems with this aproach in a situation where any number of elements were added both above and below the current element. So I came up with a sligtly improved version:
/* This listener will block any listView redraws utils unlock() is called */
private class ListViewPredrawListener implements OnPreDrawListener {
private View view;
private boolean locked;
private ListViewPredrawListener(View view) {
this.view = view;
}
public void lock() {
if (!locked) {
locked = true;
view.getViewTreeObserver().addOnPreDrawListener(this);
}
}
public void unlock() {
if (locked) {
locked = false;
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
#Override
public boolean onPreDraw() {
return false;
}
}
/* Method inside our BaseAdapter */
private updateList(List<Item> newItems) {
int pos = listView.getFirstVisiblePosition();
View cell = listView.getChildAt(pos);
String savedId = adapter.getItemId(pos); // item the user is currently looking at
savedPositionOffset = cell == null ? 0 : cell.getTop(); // current item top offset
// Now we block listView drawing until after setSelectionFromTop() is called
final ListViewPredrawListener predrawListener = new ListViewPredrawListener(listView);
predrawListener.lock();
// We have no idea what changed between items and newItems, the only assumption
// that we make is that item with savedId is still in the newItems list
items = newItems;
notifyDataSetChanged();
// or for ArrayAdapter:
//clear();
//addAll(newItems);
listView.post(new Runnable() {
#Override
public void run() {
// Now we can finally unlock listView drawing
// Note that this code will always be executed
predrawListener.unlock();
int newPosition = ...; // Calculate new position based on the savedId
listView.setSelectionFromTop(newPosition, savedPositionOffset);
}
});
}

setSelected(true) not work on custom view

I have created custom View called Color. I use object of Color to fill GridView (same issue is in ListView also).
My task is to let user choose one color and highlight it.
Previously I do something like that but I used only in building widgets and everything worked. This time I use my own.
This cose is for item clicking:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int color = (int)id;
Log.d(TAG, "Selected color: " + id);
view.setSelected(true);
view.invalidate();
}
Color.onDraw:
#Override
public void onDraw(Canvas canvas) {
if (isSelected()) {
Log.d(TAG, "color draw selected");
} else {
Log.d(TAG, "not selected " + color);
}
}
Also I set setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); to this GridView (AbsListView.CHOICE_MODE_MULTIPLE don't help also)
In log I see:
Selected color: 8355711
not selected 16711680
not selected 16743680
not selected 8355711
I am sure, Color items is not recreating more than on time.
I tried change GridView to ListView, user default widget (overrided only onDraw()). Nothing helps. Maybe I forgot something?
In my opinion GridView somehow drop selected status.
If you need more info just tell.
Addition:
I checked with debugger. View with color 8355711 is same object in both functions.
I used setOnItemSelectedListener(this); to track item selection. But nothing happen in this listener.
The method isSelected() is comming from the GridView which is a child of View.
Basically when calling isSelected() you're saying: "is the gridview selected" which is not what you want.
What you want is: "is there any selected view in the grid view ?"
Which could be achieved using getSelectedView()
As the documentation says, you will get a reference to the selected view or null if none is selected.
Also make sur your GridView is properly initialized to handle item selection.
EDIT : Ok i understand that isSelected() is called from the Color view. My first guess is then useless.
But I think you should try to make your item selected using the setSelection() of the GridView object.
Add something like:
myGridView.setSelection(position);
I fix this issue my self by making my own select. Its workaround.
In Color class added:
private boolean selected = false;
public boolean isSelected2() {
return selected;
}
public void setSelected2(boolean selected) {
this.selected = selected;
}
#Override
public void onDraw(Canvas canvas) {
....
if (isSelected2()) {
//draw selected state
} else {
....
}
On ColorChooser grid:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
...
int i = getChildCount();
while (--i >= 0)
((Color)getChildAt(i)).setSelected2(false);
((Color)view).setSelected2(true);
....
}
Reason why I not to override natice setSelected(boolean state) is some other code in grid every time make all items in deselected state. I don't know why, because in other places same more native code work very fine.

Categories

Resources