Hide the view in recyclerview's item - android

i facing a problem in recyclerview's item.
My Adapter's code :
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
Profile item = mListChatting.get(position);
Log.d("TAG", "CEK : " + viewable);
if(viewable==true){
holder.mFormBookingan.setVisibility(View.GONE);
holder.mDetailBookingan.setVisibility(View.VISIBLE);
}else{
//assume that one way is show first as default
holder.mViewOneWay.setVisibility(View.VISIBLE);
holder.mViewRoundTrip.setVisibility(View.GONE);
holder.mOneOway.setBackgroundResource(R.drawable.round_just_left_white_focus);
holder.mRoundTrip.setBackgroundResource(R.drawable.state_pressed_booking_button_left);
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewable = true;
Log.d("TAG", "CEK 2 : " + viewable);
}
});
}
Like my code above, I want to hide mFormBookingan after mSendBooking has pressed. mFormBookingan never show anymore until user calls it again.
I have tried with a lot of ways but still can't find like what i need. After i press mSendBooking the form hide, but when i send new item to recyclerview, the from mFormBookingan that has been hide, appears again.
My Question, how to hide mFormBookingan forever? Until user call it again.
Thank in advance, i will appreciate anyone who help me for this one.

I not sure what the clear situation you want.
But if you want to set View invisible you can try this code then check it.
You need to add ismFormBookingVisible in viewHolder class as a boolean attribute.
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
Profile item = mListChatting.get(position);
Log.d("TAG", "CEK : " + viewable);
if(holder.ismFormBookingVisible==true){
holder.mFormBookingan.setVisibility(View.GONE);
holder.mDetailBookingan.setVisibility(View.VISIBLE);
}else{
//assume that one way is show first as default
holder.mViewOneWay.setVisibility(View.VISIBLE);
holder.mViewRoundTrip.setVisibility(View.GONE);
holder.mOneOway.setBackgroundResource(R.drawable.round_just_left_white_focus);
holder.mRoundTrip.setBackgroundResource(R.drawable.state_pressed_booking_button_left);
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
holder.ismFormBookingVisible = false;
Log.d("TAG", "CEK 2 : " + viewable);
}

Try this :
Create a boolean in your model class "Profile" to keep track of visibility of button : say boolean isBookingVisible;
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
Profile item = mListChatting.get(position);
if(!item.isBookingVisible){
holder.mFormBookingan.setVisibility(View.GONE);
holder.mDetailBookingan.setVisibility(View.VISIBLE);
}else{
//assume that one way is show first as default
holder.mViewOneWay.setVisibility(View.VISIBLE);
holder.mViewRoundTrip.setVisibility(View.GONE);
holder.mOneOway.setBackgroundResource(R.drawable.round_just_left_white_focus);
holder.mRoundTrip.setBackgroundResource(R.drawable.state_pressed_booking_button_left);
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
item.isBookingVisible = false;
//Use notiyItemChanged(position); or notifyDataSetChanged(); here as per your selection criterion
Log.d("TAG", "CEK 2 : " + viewable);
}
});
}

You may have to call notifyDataSetChanged() after changing the value of viewable.

onBindViewHolder will be called when you notify your data set.
So you need to save the viewable in mListChatting. When you click the button, change the viewable in the mListChatting.
And then, change the code in onBindViewHolder
holder.mFormBookingan.setVisibility(item.getViewable() ? View.VISIBLE : View.GONE);

On refresh android will destroy the view and create new view with new adapter data. So you have to track the current state (visibility) of mFormBookingan. You can use a simple visibility list. When mFormBookingan state (visibility) change update it in visibility list so that whenever the list is refreshed, you can use it to check and set the last state (visibility) of your mFormBookingan. Here is an example
private ArrayList<Boolean> isVisible;
public MyAdapter(ArrayList<Boolean> isVisible){
// initial state list of mFormBookingan for each row of list
this.isVisible = isVisible;
}
public void onBindViewHolder(final MyViewHolder holder, final int position) {
if (isVisible.get(position)) {
holder.mFormBookingan.setVisibility(View.VISIBLE);
}else {
holder.mFormBookingan.setVisibility(View.GONE);
}
holder.mSendBooking.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (holder.mFormBookingan.getVisibility() == View.GONE){
holder.mFormBookingan.setVisibility(View.VISIBLE);
isVisible.set(position, true);
}else {
holder.mFormBookingan.setVisibility(View.GONE);
isVisible.set(position, false);
}
}
});
}
when you click mSendBooking the mFormBookingan visibility will change and it will remain same after sending new item to recyclerview.

Related

Change the background image of a RecyclerView item on click

I have a RecylerView which contains a textView as individual item. Based on the requirement, the background image of the textView will be changed on click after checking the present image.
In simple:
onClick textView TV--> check for the background image of the textview TV--> check if the background image of TV is a, then make it b and viceversa
Please find my code below:
viewHolder.addRemove.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(viewHolder.clicked==0)
{
viewHolder.addRemove.setBackgroundResource(R.mipmap.added);
viewHolder.clicked=1;
groupSlcArray.add(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
}
else
{
viewHolder.addRemove.setBackgroundResource(R.mipmap.addslcgroup);
viewHolder.clicked=0;
groupSlcArray.remove(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
}
}
});
I am unable to find anyway to get the background resource of the textView for checking.Need your help.This question might seem too simple for some. So please before Outvoting, try to give a solution because I have tried all the possible solutions that I found.Thanks in advance
Try following code :
holder.textview.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (holder.textview.getBackground() == getResources().getDrawable(R.drawable.a)){
holder.textview.setBackground(R.drawable.b);
}
else{
holder.textview.setBackground(R.drawable.a);
}
}
});
You have to understand about getView.
Sometimes If you scroll in ListView, This is not an exact Color
So I would like to recommand following ways like this:
1. Add Code on getView
if (groupSlcArray.contain(odel.getName().trim())) {
viewHolder.addRemove.setBackgroundResource(R.mipmap.addslcgroup);
} else {
viewHolder.addRemove.setBackgroundResource(R.mipmap.added);
}
2. Change OnClickListener Event Code
viewHolder.addRemove.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick (View v){
if (groupSlcArray.contain(odel.getName().trim())) {
viewHolder.addRemove.setBackgroundResource(R.mipmap.addslcgroup);
groupSlcArray.remove(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
} else {
viewHolder.addRemove.setBackgroundResource(R.mipmap.added);
groupSlcArray.add(model.getName().trim());
Log.d("groupArr", Arrays.toString(groupSlcArray.toArray()));
}
}
});
Here is a solution that you can use for change background image of each RecyclerView item.
For example, currently, each row of your RecyclerView is a Item, you need to create 1 more field name textViewBackGround for hold the back ground of each row like this
public class Item{
...
public int textViewBackGround = R.drawable.a; // default image a
}
Then in your adapter
public class MyRecyclerViewAdapter extends RecyclerView.Adapter < MyRecyclerViewAdapter.CustomViewHolder > {
private List < Item > itemList;
private Context mContext;
public MyRecyclerViewAdapter(Context context, List < Item > itemList) {
this.itemList = itemList;
this.mContext = context;
}
#Override
public void onBindViewHolder(CustomViewHolder viewHolder, int i) {
Item item = itemList.get(i);
viewHolder.textView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// don't need to get the background of your TextView then change the background, just need to check the datasource. Later on, you absolutely will know that check the datasource for change background of item is a correct way not check the background resource
if (item.textViewBackGround == R.drawable.a) {
viewHolder.textView.setBackgroundResource(R.drawable.b);
item.textViewBackGround = R.drawable.b;
} else {
viewHolder.textView.setBackgroundResource(R.drawable.a);
item.textViewBackGround = R.drawable.a;
}
}
});
viewHolder.textView.setBackgroundResource(item.textViewBackGround); // need to set the background image for `TextView` everytime bind ViewHolder because RecyclerView item will recycle when you scroll (if you don't understand it, you can remove this line, run your app, scroll up/down and you will understand it)
...
}
class CustomViewHolder extends RecyclerView.ViewHolder {
protected TextView textView;
public CustomViewHolder(View view) {
super(view);
...
}
}
}

recyclerview item changes button style after scroll

I have a recyclerview with simple items - an item has an image, title and a button. When the user clicks on the button it needs to change its layout -> indicating that button is clicked (similar to checkbox functionality).
Problem is that when I click on a button, for example the second item, it behaves weirdly when I scroll - multiple items are toggled or the original one is untoggled. You can check it out in image here:
GIF PREVIEW
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
holder.button.setOnClickListener(view -> {
toggleButtonStyle((Button)view);
});
}
public void toggleButtonStyle(Button toggle)
{
Context ctx = toggle.getContext();
if (toggle.isActivated()) {
toggle.setActivated(false);
toggle.setBackground(ContextCompat.getDrawable(ctx, R.drawable.btn_purple_corners));
toggle.setTextColor(ContextCompat.getColor(ctx, R.color.purple_light));
} else {
toggle.setActivated(true);
toggle.setBackground(ContextCompat.getDrawable(ctx, R.drawable.btn_purple));
toggle.setTextColor(ContextCompat.getColor(ctx, R.color.white));
}
}
You need to specify position for a onBindView method, because it's called multiple times. If you are using AutoValue immutable types, you can try to make an Array of booleans where you'll be storing toggled states (true/false). Then in onBindViewMethod check a value of current position and style a button.
private boolean[] toggledChoices = new boolean[yourListWithItems.size()];
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (toggledChoices[position]) {
toggleButtonStyle(true, holder.toggleButton);
} else {
toggleButtonStyle(false, holder.toggleButton;
}
holderVote.bToggleItem.setOnClickListener(v -> {
if (!toggledChoices[position]) {
toggledChoices[position] = true;
} else {
toggledChoices[position] = false;
}
});
}
I had some strange behavior too in the past.
For me it was solved by simply calling
toggle.invalidate();
after your if/else block. This forces the redraw. Try it, I was surprised too, that this fixed it for me.
Check Out my code
#Override
public void onBindViewHolder(GifGridAdapter.ViewHolder holder, final int position) {
final DataModel model = arrayList.get(position);
if (model.isToggle()) {
//Code for changing the button background color and text color
} else {
//Code for changing the button background color and text color
}
holder.ivGif.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
model.setToggle(!model.isToggle());
notifyDataSetChanged();
}
});
}

RecyclerView getAdapterPosition() returns -1 on a callback so I can't show the new appearance for the item

Each item on my RecyclerView has a button that has three states: OPEN, LOADING, and CLOSED.
Initially all the buttons are in the OPEN state. When a button is clicked, the state is changed to LOADING and a network call is performed in the background. After the network call succeeds, the button state should be changed to CLOSED.
So in my adapter I used the following:
holder.button.setOnClickListener(v -> {
holder.state = LOADING;
notifyItemChanged(holder.getAdapterPosition()); /* 1 */
callNetwork(..., () -> {
/* this is the callback that runs on the main thread */
holder.state = CLOSED;
notifyItemChanged(holder.getAdapterPosition()); /* 2 */
});
});
The LOADING state is always visualized correctly at /* 1 */ because getAdapterPosition() gives me the correct position.
However, the CLOSED state of the button is never visualized, because getAdapterPosition at /* 2 */ always returns -1.
I might understand getAdapterPosition() wrongly in this case.
How do I refresh the appearance of an item on a callback?
From the docs:
Note that if you've called notifyDataSetChanged(), until the next
layout pass, the return value of this method will be NO_POSITION
NO_POSITION is a constant whose value is -1. This might explain why you are getting a return value of -1 here.
In any case, why don't you find the position of the model in the underlying dataset and then call notifyItemChanged(int position)? You could save the model as a field in the holder.
For example:
public class MyHolder extends RecyclerView.ViewHolder {
private Model mMyModel;
public MyHolder(Model myModel) {
mMyModel = myModel;
}
public Model getMyModel() {
return mMyModel;
}
}
holder.button.setOnClickListener(v -> {
holder.state = LOADING;
notifyItemChanged(holder.getAdapterPosition());
callNetwork(..., () -> {
/* this is the callback that runs on the main thread */
holder.state = CLOSED;
int position = myList.indexOf(holder.getMyModel());
notifyItemChanged(position);
});
});
Alternatively you can just ignore if the position is -1, like this:
holder.button.setOnClickListener(v -> {
holder.state = LOADING;
int preNetworkCallPosition = holder.getAdapterPosition();
if (preNetworkCallPosition != RecyclerView.NO_POSITION) {
notifyItemChanged(preNetworkCallPosition);
}
callNetwork(..., () -> {
/* this is the callback that runs on the main thread */
holder.state = CLOSED;
int postNetworkCallPosition = holder.getAdapterPosition();
if (postNetworkCallPosition != RecyclerView.NO_POSITION) {
notifyItemChanged(postNetworkCallPosition);
}
});
});
getAdapterPosition(); It will always return -1 when recyclerview makes layout calculations. You are calling this methods inside ViewHolder.. It means RecyclerView is doing calculations.
If you need position inside click actions of view, call it in the public void onClick(final View v) method for example:
"#Override
public void onBindViewHolder(#NonNull final ViewHolder holder, final int position) {
final Students user = mUsers.get(position);
holder.Name.setText(user.getFullname());
holder.Index.setText(user.getIndex_number());
if (user.getThumbnail().equals("default")) {
holder.profile_image.setImageResource(R.drawable.profile_pic);
} else {
Picasso.get().load(user.getThumbnail())
.placeholder(R.drawable.profile_pic)
.into(holder.profile_image);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
**list_user_id = mUsers.get(position).getId();**
Intent Sub = new Intent(mContext, UserProfileActivity.class);
Sub.putExtra("user_id1", list_user_id);
mContext.startActivity(Sub);
BUT NOT
getAdapterPosition(); It will always return -1 when recyclerview makes layout calculations. You are calling this methods inside ViewHolder.. It means RecyclerView is doing calculations.
If you need position inside click actions of view, call it in the public void onClick(final View v) method for example:
#Override
public void onBindViewHolder(#NonNull final ViewHolder holder, final int position) {
final Students user = mUsers.get(position);
holder.Name.setText(user.getFullname());
holder.Index.setText(user.getIndex_number());
**list_user_id = mUsers.get(position).getId();**
if (user.getThumbnail().equals("default")) {
holder.profile_image.setImageResource(R.drawable.profile_pic);
} else {
Picasso.get().load(user.getThumbnail())
.placeholder(R.drawable.profile_pic)
.into(holder.profile_image);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
Intent Sub = new Intent(mContext, UserProfileActivity.class);
Sub.putExtra("user_id1", list_user_id);
mContext.startActivity(Sub);

RecyclerView doesn't mantain changes

I'm creating an application for Android, and in this moment i want just create a selectable list using a RecyclerView, for this, when a user clicks on an item, I put its id in an array and I change its icon.
The problem is that, when I scroll down and then I scroll up again or when I change fragment, the array still contains all the id of the items, but the icons comes back as before, they don't mantain the new image that I set.
This is the onBindViewHolder metod, this method is the only in the whole project which change the imageview :
List<Integer> selected = new LinkedList<>();
#Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mBoundString = mValues.get(position);
holder.mTextView.setText(mValues.get(position));
final int pos = position;
holder.mView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final Context context = v.getContext();
Animation to_middle = AnimationUtils.loadAnimation(v.getContext(), R.anim.to_middle);
final Animation from_middle = AnimationUtils.loadAnimation(v.getContext(), R.anim.from_middle);
to_middle.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
holder.mImageView.setImageResource(R.drawable.ic_action_navigation_check);
holder.mImageView.clearAnimation();
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
holder.mImageView.setAnimation(to_middle);
holder.mImageView.startAnimation(to_middle);
selected.add(pos);
}
});
if(!selected.contains(position)) {
/* I checked and this method it's not called when
an item is selected, so the following instruction
isn't the cause of my problem, anyway the image
comes back as before */
Glide.with(holder.mImageView.getContext())
.load(R.drawable.ic_default)
.fitCenter()
.into(holder.mImageView);
}
}
I'm sure that R.drawable.ic_default isn't used elsewhere, so which method is changing the image?
Thank you
if(selected.contains(position)) { // Check if already selected
Glide.with(holder.mImageView.getContext())
.load(R.drawable.ic_action_navigation_check) // set selected drawable here.
.fitCenter()
.into(holder.mImageView);
}
You are checking that "if not selected set image to default" but you need to check "if selected set image to selected image".

Android RecyclerView : notifyDataSetChanged() IllegalStateException

I'm trying to update the items of a recycleview using notifyDataSetChanged().
This is my onBindViewHolder() method in the recycleview adapter.
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
//checkbox view listener
viewHolder.getCheckbox().setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
//update list items
notifyDataSetChanged();
}
});
}
What I want to do is update the list items, after I check a checkbox. I get an illegal exception though: "Cannot call this method while RecyclerView is computing a layout or scrolling"
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:1462)
at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:2982)
at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:7493)
at android.support.v7.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:4338)
at com.app.myapp.screens.RecycleAdapter.onRowSelect(RecycleAdapter.java:111)
I also used notifyItemChanged(), same exception. Any secret way to update to notify the adapter that something changed?
You should move method 'setOnCheckedChangeListener()' to ViewHolder which is inner class on your adapter.
onBindViewHolder() is not a method that initialize ViewHolder.
This method is step of refresh each recycler item.
When you call notifyDataSetChanged(), onBindViewHolder() will be called as the number of each item times.
So If you notifyDataSetChanged() put into onCheckChanged() and initialize checkBox in onBindViewHolder(), you will get IllegalStateException because of circular method call.
click checkbox -> onCheckedChanged() -> notifyDataSetChanged() -> onBindViewHolder() -> set checkbox -> onChecked...
Simply, you can fix this by put one flag into Adapter.
try this,
private boolean onBind;
public ViewHolder(View itemView) {
super(itemView);
mCheckBox = (CheckBox) itemView.findViewById(R.id.checkboxId);
mCheckBox.setOnCheckChangeListener(this);
}
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if(!onBind) {
// your process when checkBox changed
// ...
notifyDataSetChanged();
}
}
...
#Override
public void onBindViewHolder(YourAdapter.ViewHolder viewHolder, int position) {
// process other views
// ...
onBind = true;
viewHolder.mCheckBox.setChecked(trueOrFalse);
onBind = false;
}
You can just reset the previous listener before you make changes and you won't get this exception.
private CompoundButton.OnCheckedChangeListener checkedListener = new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
//Do your stuff
});;
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.checkbox.setOnCheckedChangeListener(null);
holder.checkbox.setChecked(condition);
holder.checkbox.setOnCheckedChangeListener(checkedListener);
}
Using a Handler for adding items and calling notify...() from this Handler fixed the issue for me.
I don't know well, but I also had same problem. I solved this by using onClickListner on checkbox
viewHolder.mCheckBox.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (model.isCheckboxBoolean()) {
model.setCheckboxBoolean(false);
viewHolder.mCheckBox.setChecked(false);
} else {
model.setCheckboxBoolean(true);
viewHolder.mCheckBox.setChecked(true);
}
notifyDataSetChanged();
}
});
Try this, this may help!
protected void postAndNotifyAdapter(final Handler handler, final RecyclerView recyclerView, final RecyclerView.Adapter adapter) {
handler.post(new Runnable() {
#Override
public void run() {
if (!recyclerView.isComputingLayout()) {
adapter.notifyDataSetChanged();
} else {
postAndNotifyAdapter(handler, recyclerView, adapter);
}
}
});
}
Found a simple solution -
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
private RecyclerView mRecyclerView;
#Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mRecyclerView = recyclerView;
}
private CompoundButton.OnCheckedChangeListener checkedChangeListener
= (compoundButton, b) -> {
final int position = (int) compoundButton.getTag();
// This class is used to make changes to child view
final Event event = mDataset.get(position);
// Update state of checkbox or some other computation which you require
event.state = b;
// we create a runnable and then notify item changed at position, this fix crash
mRecyclerView.post(new Runnable() {
#Override public void run() {
notifyItemChanged(position));
}
});
}
}
Here we create a runnable to notifyItemChanged for a position when recyclerview is ready to handle it.
When you have the Message Error:
Cannot call this method while RecyclerView is computing a layout or scrolling
Simple, Just do what cause the Exception in:
RecyclerView.post(new Runnable() {
#Override
public void run() {
/**
** Put Your Code here, exemple:
**/
notifyItemChanged(position);
}
});
At first I thought Moonsoo's answer (the accepted answer) wouldn't work for me because I cannot initialize my setOnCheckedChangeListener() in the ViewHolder constructor because I need to bind it each time so it gets an updated position variable. But it took me a long time to realize what he was saying.
Here is an example of the "circular method call" he is talking about:
public void onBindViewHolder(final ViewHolder holder, final int position) {
SwitchCompat mySwitch = (SwitchCompat) view.findViewById(R.id.switch);
mySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
data.delete(position);
notifyItemRemoved(position);
//This will call onBindViewHolder, but we can't do that when we are already in onBindViewHolder!
notifyItemRangeChanged(position, data.size());
}
}
});
//Set the switch to how it previously was.
mySwitch.setChecked(savedSwitchState); //If the saved state was "true", then this will trigger the infinite loop.
}
The only problem with this, is that when we need to initialize the switch to be on or off (from past saved state, for example), it is calling the listener which might call nofityItemRangeChanged which calls onBindViewHolder again. You cannot call onBindViewHolder when you are already in onBindViewHolder], because you cannot notifyItemRangeChanged if you are already in the middle of notifying that the item range has changed. But I only needed to update the UI to show it on or off, not wanting to actually trigger anything.
Here is the solution I learned from JoniDS's answer that will prevent the infinite loop. As long as we set the listener to "null" before we set Checked, then it will update the UI without triggering the listener, avoiding the infinite loop. Then we can set the listener after.
JoniDS's code:
holder.checkbox.setOnCheckedChangeListener(null);
holder.checkbox.setChecked(condition);
holder.checkbox.setOnCheckedChangeListener(checkedListener);
Full solution to my example:
public void onBindViewHolder(final ViewHolder holder, final int position) {
SwitchCompat mySwitch = (SwitchCompat) view.findViewById(R.id.switch);
//Set it to null to erase an existing listener from a recycled view.
mySwitch.setOnCheckedChangeListener(null);
//Set the switch to how it previously was without triggering the listener.
mySwitch.setChecked(savedSwitchState); //If the saved state was "true", then this will trigger the infinite loop.
//Set the listener now.
mySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
data.delete(position);
notifyItemRemoved(position);
//This will call onBindViewHolder, but we can't do that when we are already in onBindViewHolder!
notifyItemRangeChanged(position, data.size());
}
}
});
}
your CheckBox item is in changing drawable when you call notifyDataSetChanged(); so this exception would be occurred.
Try call notifyDataSetChanged(); in post of your view. For Example:
buttonView.post(new Runnable() {
#Override
public void run() {
notifyDataSetChanged();
}
});
Why not checking the RecyclerView.isComputingLayout() state as follows?
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
private RecyclerView mRecyclerView;
#Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mRecyclerView = recyclerView;
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
viewHolder.getCheckbox().setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (mRecyclerView != null && !mRecyclerView.isComputingLayout()) {
notifyDataSetChanged();
}
}
});
}
}
While item is being bound by the layout manager, it is very likely that you are setting the checked state of your checkbox, which is triggering the callback.
Of course this is a guess because you did not publish the full stack trace.
You cannot change adapter contents while RV is recalculating the layout. You can avoid it by not calling notifyDataSetChanged if item's checked state is equal to the value sent in the callback (which will be the case if calling checkbox.setChecked is triggering the callback).
Use onClickListner on checkbox instead of OnCheckedChangeListener, It will solve the problem
viewHolder.myCheckBox.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (viewHolder.myCheckBox.isChecked()) {
// Do something when checkbox is checked
} else {
// Do something when checkbox is unchecked
}
notifyDataSetChanged();
}
});
I ran into this exact issue! After Moonsoo's answer didn't really float my boat, I messed around a bit and found a solution that worked for me.
First, here's some of my code:
#Override
public void onBindViewHolder(ViewHolder holder, final int position) {
final Event event = mDataset.get(position);
//
// .......
//
holder.mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
event.setActive(isChecked);
try {
notifyItemChanged(position);
} catch (Exception e) {
Log.e("onCheckChanged", e.getMessage());
}
}
});
You'll notice I'm specifically notifying the adapter for the position I'm changing, instead of the entire dataset like you're doing. That being said, although I can't guarantee this will work for you, I resolved the problem by wrapping my notifyItemChanged() call in a try/catch block. This simply caught the exception, but still allowed my adapter to register the state change and update the display!
Hope this helps someone!
EDIT: I'll admit, this probably is not the proper/mature way of handle the issue, but since it doesn't appear to be causing any problems by leaving the exception unhandled, I thought I'd share in case it was good enough for someone else.
Before notifyDataSetChanged() just check that with this method: recyclerView.IsComputingLayout()
Simple use Post:
new Handler().post(new Runnable() {
#Override
public void run() {
mAdapter.notifyItemChanged(mAdapter.getItemCount() - 1);
}
}
});
simply use isPressed() method of CompoundButton in onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
e.g
public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
... //your functionality
if(compoundButton.isPressed()){
notifyDataSetChanged();
}
} });
mostly it happen beacause notifydatasetchanged calling onCheckedchanged event of checkbox and in that event again there is notifydatasetchanged.
to solve it you can just check that checkbox is checked by programatically or user pressed it. there is method isPressed for it.
so wrap whole listner code inside isPressed method. and its done.
holder.mBinding.cbAnnual.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
if(compoundButton.isPressed()) {
//your code
notifyDataSetChanged();
}
});
I suffered with this problem for hour and this is how you can fix it.
But before you began there are some conditions to this solution.
MODEL CLASS
public class SelectUserModel {
private String userName;
private String UserId;
private Boolean isSelected;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserId() {
return UserId;
}
public void setUserId(String userId) {
UserId = userId;
}
public Boolean getSelected() {
return isSelected;
}
public void setSelected(Boolean selected) {
isSelected = selected;
}
}
CHECKBOX in ADAPTER CLASS
CheckBox cb;
ADAPTER CLASS CONSTRUCTOR & LIST OF MODEL
private List<SelectUserModel> userList;
public StudentListAdapter(List<SelectUserModel> userList) {
this.userList = userList;
for (int i = 0; i < this.userList.size(); i++) {
this.userList.get(i).setSelected(false);
}
}
ONBINDVIEW [Please use onclick in place of onCheckChange]
public void onBindViewHolder(#NonNull final StudentListAdapter.ViewHolder holder, int position) {
holder.cb.setChecked(user.getSelected());
holder.cb.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
int pos = (int) view.getTag();
Log.d(TAG, "onClick: " + pos);
for (int i = 0; i < userList.size(); i++) {
if (i == pos) {
userList.get(i).setSelected(true);
// an interface to listen to callbacks
clickListener.onStudentItemClicked(userList.get(i));
} else {
userList.get(i).setSelected(false);
}
}
notifyDataSetChanged();
}
});
}
This is happening because you're probably setting the 'listener' before you configure the value for that row, which makes the listener to get triggered when you 'configure the value' for the checkbox.
What you need to do is:
#Override
public void onBindViewHolder(YourAdapter.ViewHolder viewHolder, int position) {
viewHolder.mCheckBox.setOnCheckedChangeListener(null);
viewHolder.mCheckBox.setChecked(trueOrFalse);
viewHolder.setOnCheckedChangeListener(yourCheckedChangeListener);
}
#Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
holder.textStudentName.setText(getStudentList.get(position).getName());
holder.rbSelect.setChecked(getStudentList.get(position).isSelected());
holder.rbSelect.setTag(position); // This line is important.
holder.rbSelect.setOnClickListener(onStateChangedListener(holder.rbSelect, position));
}
#Override
public int getItemCount() {
return getStudentList.size();
}
private View.OnClickListener onStateChangedListener(final RadioButton checkBox, final int position) {
return new View.OnClickListener() {
#Override
public void onClick(View v) {
if (checkBox.isChecked()) {
for (int i = 0; i < getStudentList.size(); i++) {
getStudentList.get(i).setSelected(false);
}
getStudentList.get(position).setSelected(checkBox.isChecked());
notifyDataSetChanged();
} else {
}
}
};
}
I had the same problem using the Checkbox and the RadioButton. Replacing notifyDataSetChanged() with notifyItemChanged(position) worked. I added a Boolean field isChecked to the data model. Then I updated the Boolean value and in onCheckedChangedListener, I called notifyItemChanged(adapterPosition). This might not be the best way, but worked for me. The boolean value is used for checking whether the item is checked.
override fun bindItemViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
var item: ThingChannels = items[position]
rowActionBinding.xCbChannel.text = item.channelType?.primaryType
rowActionBinding.xTvRoomName.text = item.thingDetail?.room?.name
rowActionBinding.xCbChannel.isChecked = item.isSelected
rowActionBinding.xCbChannel.tag = position
rowActionBinding.xCbChannel.setOnClickListener {
setSelected(it.tag as Int)
if (onItemClickListener != null) {
onItemClickListener!!.onItemClick(position, null)
}
}
}
None of the past answers solve the problem!
The problem with past answers
All of them either avoid the problem by swallowing the change (i.e. not notifying the adapter of the change) if the user is scrolling, which just means that if the user was scrolling when the change was ready, they will never see the change. Or, they suggest using recylerView.post() which just postpones the problem.
The answer
Option #1
Stop the scrolling and then notify the adapter:
recyclerView.stopScroll()
val copy = workingList.toList()
//prevent IndexOutOfBoundsException: Inconsistency detected... (https://stackoverflow.com/questions/41054959/java-lang-indexoutofboundsexception-inconsistency-detected-invalid-view-holder)
workingList.clear()
recyclerViewAdapter?.notifyDataSetChanged()
//set display to correct data
workingList.addAll(copy)
recyclerViewAdapter?.notifyItemRangeInserted(0, workingList.size)
Option #2
For a better user experience, you can let them continue scrolling and listen for when they stop scrolling in order to update the UI, but this method should only be used if you do not plan on writing a function that will accept different RecyclerView instances, because if you add multiple listeners to the same RecyclerView which are all trying to be updated, the app will crash:
if(recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) //notify RecyclerView
else recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int
) {
if(newState == RecyclerView.SCROLL_STATE_IDLE) {
//notify RecyclerView...
val copy = workingList.toList()
//prevent IndexOutOfBoundsException: Inconsistency detected... (https://stackoverflow.com/questions/41054959/java-lang-indexoutofboundsexception-inconsistency-detected-invalid-view-holder)
workingList.clear()
recyclerViewAdapter?.notifyDataSetChanged()
//set to display correct data
workingList.addAll(copy)
recyclerViewAdapter?.notifyItemRangeInserted(0, workingList.size)
}
})
Note: you can use option #2 even if you are writing a util function by keeping a list of previously registered instances and not add a listener if it has already been registered, but it is not "clean coding" to rely on state in a library/util class.
For me problem occurred when I exited from EditText by Done, Back, or outside input touch. This causes to update model with input text, then refresh recycler view via live data observing.
The Problem was that cursor/focus remain in EditText.
When I have deleted focus by using:
editText.clearFocus()
Notify data changed method of recycler view did stop throwing this error.
I think this is one of the possible reason/solutions to this problem. It is possible that this exception can be fixed in another way as it can be caused by totally different reason.

Categories

Resources