I'm trying to change the background image of a certain button when clicked in a recyclerView. The button's background image is changing properly but also changing the background of the button found in the 8th..16th..etc row as well. (I am currently populating data in the recycler view using a for-loop for testing)
My code is
#Override
public void onBindViewHolder(final myFirstAdapter.ViewHolder holder, int position) {
firstlistitem listItem = listItems.get(position);
holder.itemView.setTag(position);
holder.btnBookMark.setTag(position);
holder.btnBookMark.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
holder.btnBookMark.setBackgroundResource(R.drawable.bookmarkred);
}
});
}
The background is changing because, its a RecyclerView and it recyclers the view and reuses it. Hence, when you click on 8th position and scroll to bottom, it is reusing the item at 8 position with custom background for other items. Because of this you are getting custom background for later items on scroll.
If you have many items which will have changed background do this.
class ViewHolder extends RecyclerView.ViewHolder{
boolean newBackground = false;
Button button
ViewHolder(View itemView){
...
button.setOnClickListner((v) - > newBackground = true);
}
}
Then you can check the value of newBackground and set the background in onBindViewHolder()
If you have only one item of which the background will change at a time then you can declare a field in the Adapter and check that to change the background.
class MyAdapter extends RecyclerView.Adapter<ViewHolder>{
int newBackgroundPos = -1;
#Override
public final void onBindViewHolder(ViewHolder holder, int position) {
holder.button.setOnClickListener((v) -> newBackGroundPos = position);
if(position == newBackgroundPos)
holder.itemView.setBackground(newBackground);
else
holder.itemView.setBackground(normalBackground);
}
}
Related
I am trying to implement a list of files that can be selected from the RecyclerView Adapter class. While I understand it is not a good idea, I feel if I am able to accomplish this from within said class, it would be really helpful in the future.
My list item (Each individual item view for the RecyclerView) has the following structure:
|--------|----------------|
| ICON | DATA |
|--------|----------------|
Problem:
When a file is selected (by touching the icon portion of a file item), I change the background of that item to another color to denote that it has been selected.
However, when I scroll down to about 25 items later, another file has the same background color even though it's not selected (it does not show up in Log.d as being selected, nor was it in the test ArrayList that was used to store selected files).
It seems as though the item is only retaining the background change of the previous occupant.
Solution attempts:
Previously, only the variables related to each list item were declared in the RecyclerView ViewHolder class and all changes were made in the onBindViewHolder method. Now, all changes to be made have been moved to the ViewHolder class inside a method called bind. There was no change in behavior.
If I set the default background image during the very first step in onBindViewHolder, the behavior changes such that the items do not retain changes of previous occupants. However, on scrolling back, the background change for the target item reverts to the default background image.
Code:
public class RVA extends RecyclerView.Adapter<RVA.RVH>
{
private LayoutInflater inf;
private ArrayList<File> items;
// The var below is used to track the no. of selected items
// globally within the RVA class.
private int numberOfSelectedItems = 0;
public RVA(ArrayList<File> _items)
{
items = _items;
}
#Override
public RVA.RVH onCreateViewHolder(ViewGroup parent, int viewType)
{
inf = LayoutInflater.from(parent.getContext());
return new RVH(inf, parent);
}
#Override
public void onBindViewHolder(RVA.RVH holder, int position)
{
File listItem = items.get(position);
// 'binding' each file element to a respective host container.
holder.bind(listItem);
}
#Override
public int getItemCount()
{
return items.size();
}
#Override
public long getItemId(int position)
{
return position;
}
// The ViewHolder class.
// Initially it was just declared as class.
// There was no change observed after the 'final' modifier was added.
final class RVH extends RecyclerView.ViewHolder
{
private Context context;
private LinearLayout itemSelector;
private ImageView itemIcon;
private TextView itemName;
private TextView itemSize;
public RVH(LayoutInflater inf, ViewGroup parent)
{
super(inf.inflate(R.layout.list_item, parent, false));
context = parent.getContext();
// This is the SECOND outermost LinearLayout of each file item View.
// It was previously the parent Layout, but there was no difference due to change.
itemSelector = itemView.findViewById(R.id.item_selector);
// This is the icon ImageView.
itemIcon = itemView.findViewById(R.id.item_icon);
// These are the data TextViews.
itemName = itemView.findViewById(R.id.item_id);
itemSize = itemView.findViewById(R.id.item_size);
}
// The 'bind' method that registers changes.
public void bind(File fileItem)
{
String listItemName = fileItem.getName();
itemName.setText(listItemName);
//---- These are just changes to the icons depending on type. Works fine.
if(fileItem.isDirectory())
{
itemIcon.setImageResource(R.drawable.directory_icon);
itemSize.setText("Directory");
}
else
{
itemSize.setText(fileItem.length() + " B");
if(listItemName.endsWith(".jpg") || listItemName.endsWith(".jpeg") || listItemName.endsWith(".png") || listItemName.endsWith(".gif"))
{
Glide.with(context).load(fileItem).centerCrop().into(itemIcon);
}
else
{
itemIcon.setImageResource(R.drawable.file_icon);
}
}
//---- END
//---- This is the code which handles opening files according to type. Works fine.
itemSelector.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
if(numberOfSelectedItems == 0)
{
if(!itemSize.getText().toString().endsWith(" B"))
{
Intent loadListItemIntent = new Intent(context, DirectoryViewActivity.class);
loadListItemIntent.putExtra("ITEMPATH", fileItem.getPath());
context.startActivity(loadListItemIntent);
}
else
{
if(itemName.getText().toString().endsWith(".jpg") || itemName.getText().toString().endsWith(".jpeg") || itemName.getText().toString().endsWith(".png") || itemName.getText().toString().endsWith(".gif"))
{
Glide.with(context).load(fileItem).centerCrop().into(itemIcon);
Intent loadListItemIntent = new Intent(context, ImageActivity.class);
loadListItemIntent.putExtra("ITEMPATH", fileItem.getPath());
context.startActivity(loadListItemIntent);
}
else
{
Toast.makeText(context, "File needs proper application.", Toast.LENGTH_SHORT).show();
}
}
}
}
});
//---- END
//---- !!! THIS SECTION is where the problem manifests.
itemIcon.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View v)
{
if(itemIcon.getTag().toString().equals("not_selected"))
{
// Incrementing based on selection.
++numberOfSelectedItems;
// Using a tag to identify/ denote whether item is selected.
itemIcon.setTag("selected");
// Changing the background & disabling file opening while in selection mode.
itemSelector.setBackgroundResource(R.drawable.list_item_selected);
itemSelector.setClickable(false);
itemSelector.setLongClickable(false);
// I use this odd method to send a message to the host Activity
// that we have entered selection mode & that the Activity should
// display some option buttons on the Action Bar.
if(context instanceof DirectoryViewActivity)
{
((DirectoryViewActivity)context).addSelectedItem(fileItem);
if(numberOfSelectedItems == 1)
{
((DirectoryViewActivity)context).setSelectionMode();
}
}
}
else
{
// Decrementing based on deselection.
--numberOfSelectedItems;
// Overwiting the tag to identify/ denote item is now unselected.
itemIcon.setTag("not_selected");
// Background changed back to default & file opening re-enabled.
itemSelector.setClickable(true);
itemSelector.setLongClickable(true);
itemSelector.setBackgroundResource(R.drawable.list_item_background);
// I use this method to send a message to the host Activity
// that we have exited selection mode & that the Activity should
// remove related option buttons from the Action Bar.
if(context instanceof DirectoryViewActivity)
{
((DirectoryViewActivity)context).removeSelectedItem(fileItem);
if(numberOfSelectedItems == 0)
{
((DirectoryViewActivity)context).voidSelectionMode();
}
}
}
}
});
}
}
}
This is because RecyclerView does not create views for all of your items in the list it create enough ViewHolder to fill up the screen and few more and when you scroll the old ViewHolder are bind to some other data in the adapter that is when the onBindViewHolder() is called , so basically what is happening here is you are setting the background of current ViewHolder on the screen and when you scroll the same ViewHolder in bind to the new data.
I think you have to check in the onBindViewHolder whether or not this is the item to which you want to set the background and then take the decision remove it if the item is not selected in the dataset set background if it is selected.
How do I implement if-statements with a custom viewholder without causing that view to appear multiple times in other items (duplication) as I scroll?
I have a RecyclerView with a custom viewholder that works as expected. However, I have a tiny image icon within each list item that should only appear if one of the list item's textView is filled out. When I try to implement an if-statement in the custom viewholder, it causes that view to duplicate when I scroll the RecyclerView.
And by the way, If I use
#Override
public int getItemViewType(int position) {
return position;
}
It prevents the duplication, but it also causes an animation issue with "swipe to dismiss" functionality. It's no longer smooth but appears to blink or glitch a little bit. Below is my ViewHolder:
private class CustomViewHolder extends RecyclerView.ViewHolder {
ImageView candidateMainImage;
ImageView careerIcon;
TextView candidateCareerText;
TextView candidateBioText;
CustomViewHolder(View itemView) {
super(itemView);
candidateMainImage = itemView.findViewById(R.id.imageview_swipe_profile_container);
careerIcon = itemView.findViewById(R.id.imageview_swipe_career_icon);
candidateCareerText = itemView.findViewById(R.id.textview_swipe_career_display);
candidateBioText = itemView.findViewById(R.id.textview_swipe_bio_display);
mContext = itemView.getContext();
}
void bind(SwipeCandidate candidate) {
StorageReference candidateImageReference = candidate.getCandidateImageReference();
GlideApp.with(candidateMainImage)
.load(candidateImageReference)
.into(candidateMainImage);
String textCareer = candidate.getCandidateCareer();
candidateCareerText.setText(textCareer);
candidateCareerText.setVisibility(View.VISIBLE);
String bio = candidate.getCandidateBio();
candidateBioText.setText(bio);
candidateBioText.setVisibility(View.VISIBLE);
if (!TextUtils.isEmpty(candidate.getCandidateCareer())) {
careerIcon.setVisibility(View.VISIBLE); // This gets duplicated into unwanted items
}
}
}
ViewHolders are recycled between items, so you need to reset the state back to the default if candidate.getCandidateCareer() is not empty:
if (!TextUtils.isEmpty(candidate.getCandidateCareer())) {
careerIcon.setVisibility(View.VISIBLE); // This gets duplicated into unwanted items
} else {
careerIcon.setVisibility(View.GONE); // or whatever your default is
}
I have two recyclerview in one layout. The vertical recyclerview shows all the user and the horizontal recyclerview shows the selected user from vertical recyclerview. The vertical recyclerview has a checkbox(clickable=false) to know that the user picks it and to do that I created an interface for vertical recyclerview, an ItemCheck and for the horizontal I created an interface of onItemClick. So my plan is when user click in vertical recyclerview it will add the item on the horizontal view and when the user unCheck it, the item will be remove to the horizontal recyclerview. And in the horizontal recyclerview, when the user clicks the item, the item will be remove in the selected user and in the vertical recyclerview it will uncheck the checkbox. This is the code for doing that.
EDIT:
In the Activity:
private void setUpAdapter() {
mUsersAdapter = new PickMemberAdapter(PickMemberActivity.this, mUserNameList,
mUserDescList, mUserPicList, new PickMemberAdapter.RecyclerViewItemClick() {
#Override
public void OnItemCheckClickListener(PickMemberAdapter.UsersViewHolders holder,
String name, int position) {
String userKey = mUserKey.get(position);
if (!holder.mCheckBox.isChecked()) {
holder.mCheckBox.setChecked(true);
mSelectedUser.add(userKey);
mSelectedName.add(name);
} else {
holder.mCheckBox.setChecked(false);
mSelectedUser.remove(userKey);
mSelectedName.remove(name);
}
mUsersAdapter.notifyDataSetChanged();
mSelectedAdapter.notifyDataSetChanged();
Toast.makeText(PickMemberActivity.this, mSelectedUser.toString()
+ "\n" + mSelectedName.toString(), Toast.LENGTH_LONG).show();
}
});
mSelectedAdapter = new SelectedUserAdapter(mSelectedName,
new SelectedUserAdapter.RecyclerViewUnselect() {
#Override
public void ItemRemoveClick(String name, int position) {
String userKey = mUserKey.get(position);
mSelectedUser.remove(userKey);
mSelectedName.remove(name);
mUsersAdapter.notifyDataSetChanged();
mSelectedAdapter.notifyDataSetChanged();
Toast.makeText(PickMemberActivity.this, mSelectedUser.toString()
+ "\n" + mSelectedName.toString(), Toast.LENGTH_LONG).show();
}
});
mSearchList.setAdapter(mUsersAdapter);
mSelectedUserList.setAdapter(mSelectedAdapter);
}
In the Adapter
public void onBindViewHolder(#NonNull final UsersViewHolders holder, int position) {
holder.setName(mUserNameList.get(position));
holder.setDesc(mUserDescList.get(position));
holder.setImage(mUserPicList.get(position));
holder.mCheckBox.setOnCheckedChangeListener(null);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mListener.OnItemCheckClickListener(holder,
mUserNameList.get(holder.getAdapterPosition()), holder.getAdapterPosition());
}
});
}
public interface RecyclerViewItemClick {
void OnItemCheckClickListener(UsersViewHolders holder, String name, int position);
}
Now my problem is the checkbox is either not checking or checking another position.
It should look like this (from Messenger Lite app)
You are updating data of RecyclerView so you should notify adapter. Call notifyDataSetChanged() after you update list.
mSearchList.notifyDataSetChanged();
mSelectedUserList.notifyDataSetChanged();
I solved this problem to maintain separate boolean for check and uncheck in user model and user it onBindViewHolder to update view.
I have a RecyclerView with each element representing an event. I want to let the user select events by clicking it. Once selected, the event(s) and a report button will be colored:
UI before performing a click: click here.
UI After performing a click: click here.
It's pretty simple and allegedly works; I set an OnClickListener for each ViewHolder which is responsible for coloring the item, and when fired it's triggering another event in the owning activity named onOccurrenceSelected, which is responsible for changing the button's state.
However, when scrolling through the RecyclerView's items, other irrelevant items are colored like their OnClickListener was triggered (though it wasn't), and when scrolling back the selected event is colored as not selected. While this is happening, the only event that's supposed to color the items is not triggered.
Any explanation for such behavior? Thanks!
EDIT: Here are some relevant code from the adapter:
private List<Occurrence> mDataSet;
private Activity activity;
public <OccurrencesActivity extends OnOccurrenceSelectedListener> OccurrencesAdapter(OccurrencesActivity occurrencesActivity, List<Occurrence> occurrences) {
this.activity = (android.app.Activity) occurrencesActivity;
mDataSet = occurrences;
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
Occurrence instance = mDataSet.get(position);
...
setOnClickListener(holder, instance);
}
private void setOnClickListener(final ViewHolder holder, final Occurrence occurrence) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (!occurrence.isSelected()) {
holder.itemView.setBackgroundColor(App.getContext().getResources().getColor(R.color.turquoise));
holder.titleTextView.setTextColor(App.getContext().getResources().getColor(R.color.white));
holder.statusTextView.setTextColor(App.getContext().getResources().getColor(R.color.white));
holder.dateTextView.setTextColor(App.getContext().getResources().getColor(R.color.white));
holder.timeTextView.setTextColor(App.getContext().getResources().getColor(R.color.white));
} else {
holder.itemView.setBackgroundColor(App.getContext().getResources().getColor(R.color.white));
holder.titleTextView.setTextColor(App.getContext().getResources().getColor(R.color.turquoise));
holder.statusTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
holder.dateTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
holder.timeTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
}
occurrence.setSelected(!occurrence.isSelected());
((OnOccurrenceSelectedListener)activity).onOccurrenceSelected(mDataSet);
}
});
}
Recyclerview always resuse views while scrolling so you have to store selected positions into temporary arraylist and then keep condition check into onBindViewHolder that whether that particular position is already exists in arraylist or not? I updated your adaper. find the below changes with comment
private List<Occurrence> mDataSet;
private Activity activity;
//Added here temporary ArrayList
private ArrayList<String> mSelectedPosition = new ArrayList<String>;
public <OccurrencesActivity extends OnOccurrenceSelectedListener> OccurrencesAdapter(OccurrencesActivity occurrencesActivity, List<Occurrence> occurrences) {
this.activity = (android.app.Activity) occurrencesActivity;
mDataSet = occurrences;
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
//Set ViewTag
holder.itemView.setTag(position);
//Check everyposition during view binding process
if(mSelectedPosition.contains(String.valueOf(position))){
holder.itemView.setBackgroundColor(App.getContext().getResources().getColor(R.color.white));
holder.titleTextView.setTextColor(App.getContext().getResources().getColor(R.color.turquoise));
holder.statusTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
holder.dateTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
holder.timeTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
}else{
holder.itemView.setBackgroundColor(App.getContext().getResources().getColor(R.color.white));
holder.titleTextView.setTextColor(App.getContext().getResources().getColor(R.color.turquoise));
holder.statusTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
holder.dateTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
holder.timeTextView.setTextColor(App.getContext().getResources().getColor(R.color.grey));
}
Occurrence instance = mDataSet.get(position);
...
setOnClickListener(holder, instance);
}
private void setOnClickListener(final ViewHolder holder, final Occurrence occurrence) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
// Get Position
int position = (int) view.getTag();
//Remove SelectedPosition if Already there
if(mSelectedPosition.contains(position))
mSelectedPosition.remove(String.valueOf(position));
else
mSelectedPosition.add(String.valueOf(position));
notifyDataSetChanged();
//Not sure about this lines
occurrence.setSelected(!occurrence.isSelected());
((OnOccurrenceSelectedListener)activity).onOccurrenceSelected(mDataSet);
}
});
}
Its the default behaviour of recyclerview. it will recycle/reuse views which are not in use currently. If you want to save the state which is colored or not. Then save a parameter in your List<Object> per position. and as per position in onBindViewHolder method use that position to change the color.
Try by Setting Tag to your item in onBindViewHolder of Adapter
holder.yourItem.setTag(position);
And then Inside the onClickListener,Just save that position in shared Pref. if it's selected, whenever you set adapter then before setting values just check that is it selected or not based on shared Pref. and perform action for same.
public void onClick(View view) {
if (!occurrence.isSelected()) {
//save position in share pref.
}
}
Please have a look at related Question. I got to know some hint from comments and now putting it in a new way.
I am using a RecyclerView with StaggeredGridView adapter to display the data. How do I get to know that how many items needed to be loaded for current devices to fit the whole screen?
process
Determine the screen size
Determine how many items needed to be loaded to fit full screen
Fetch data from Server with number of items
Display them
When user scroll down device fetch same amount of items and so on.
Question
I am not able to understand How to get done with first two points.
Code for Adapter
public class StaggeredGridAdapter extends RecyclerView.Adapter<StaggeredGridAdapter.StaggeredGridView> {
private Context context;
private String INTENT_VALUE = "warehouse";
private List<Warehouse> warehouses = new ArrayList<Warehouse>();
private AppDelegate app;
int size;
public StaggeredGridAdapter(Context context) {
this.context = context;
app = (AppDelegate) context;
}
public void addItems(List<Warehouse> response) {
size = response.size();
warehouses = response;
}
#Override
public StaggeredGridView onCreateViewHolder(ViewGroup parent, int viewType) {
View layoutView = LayoutInflater.from(parent.getContext()).inflate(R.layout.grid_item, parent, false);
StaggeredGridView staggeredGridView = new StaggeredGridView(layoutView);
return staggeredGridView;
}
#Override
public void onBindViewHolder(StaggeredGridView holder, int position) {
holder.textView.setText(warehouses.get(position).getFace());
}
#Override
public int getItemCount() {
return size;
}
class StaggeredGridView extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView textView;
public StaggeredGridView(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.img_name);
itemView.setOnClickListener(this);
}
#Override
public void onClick(View v) {
Intent intent = new Intent(context, WareHouseDetailActivity.class);
intent.putExtra(INTENT_VALUE, warehouses.get(getAdapterPosition()));
v.getContext().startActivity(intent);
}
}
}
code for filling data into adapter
mRecyclerView = (RecyclerView) mActivity.findViewById(R.id.staggering_grid);
mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
mAdapter = new StaggeredGridAdapter(mContext);
mAdapter.addItems(response);
mRecyclerView.setAdapter(mAdapter);
As to find the Height and Width, you can use the DisplayMetrics.
final DisplayMetrics displayMetrics = this.getApplicationContext().getResources().getDisplayMetrics();
This displayMetrics will give Height, Width and Density. Use the height of the single View (or row) and the total height to calculate the number of items.
Why do you want the Exact number of items, if it's a server call you are making, you could just make a rough estimate on the number of items based on the device size. Also, I think it's better to keep the number of items sent by server a constant and not dependent on the device height. If you received more than the number of items you can fit in the screen, well, the user can scroll to see them anyways.
Rather than trying to work out how much you'll need to load, you could set it up to automatically load more when you're close to the end of the list.
Consider this method:
#Override
public void onBindViewHolder(StaggeredGridView holder, int position) {
holder.textView.setText(warehouses.get(position).getFace());
}
This is called when the item is ready to be displayed on the screen. By doing something like if(position > getItemCount() - 5{ // load more } then you could be automatically loading more as and when they're required.