I have created a RecyclerView with alternating row color like this:
Whenever I delete an row from the list say for example I delete row whose product name is cookies my list gets updated like this:
as you can see the updated list no longer supports alternating row color. The simple solution would be to change the background color of next View (row) after deleting the current View. For that I first need a reference of next View but as a beginner in android I don't know how to get it.
Adapter:
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder>{
private List<Model> originalList;
Adapter(List<Model> list){ originalList = list; }
#Override
public void onBindViewHolder(final Adapter.ViewHolder holder, final int position) {
Model list = originalList.get(position);
if (position % 2 == 1)
holder.itemView.setBackgroundColor(Color.parseColor("#e9e9e9"));
final View holder.nextItemView = ? // how to get reference to next View here
holder.product.setText(list.getName());
holder.price.setText(String.valueOf(list.getPrice()));
holder.quantity.setText(String.valueOf(list.getQuantity()));
holder.options.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
showPopupMenu(holder.options, nextItemView, position);
}
});
}
private void showPopupMenu(View options, final View view, final int position){
final PopupMenu popup = new PopupMenu(options.getContext(), options);
MenuInflater inflater = popup.getMenuInflater();
inflater.inflate(R.menu.options_menu, popup.getMenu());
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
#Override
public boolean onMenuItemClick(MenuItem item) {
String menuItem = item.toString();
if (menuItem.equals("Delete")){
originalList.remove(position);
notifyItemRemoved(position);
int count = originalList.size();
if (count != 0){
int color = Color.TRANSPARENT;
Drawable background = view.getBackground();
if (background instanceof ColorDrawable)
color = ((ColorDrawable) background).getColor();
if (color == Color.parseColor(#e9e9e9))
color = Color.TRANSPARENT;
view.setBackground(color);
}
}
return true;
}
});
popup.show();
}
}
options is a ImageButton on the click of which I show a popup menu with item labelled as Delete on click of which the row gets deleted from the list.
Quick fix you can call notifyDataSetChanged() instead of notifyItemRemoved(position)
and add an else part
if (position % 2 == 1)
holder.itemView.setBackgroundColor(Color.parseColor("#e9e9e9")); // gray
else
holder.itemView.setBackgroundColor(Color.parseColor("#ffffff")); // white
but this solution will do heavy operations if your list contains a lot of items
the solution which I recommend is using ListAdapter with DiffUtil which will trigger the operation for the only modified items , you can find a sample for it here link1 ,link2, link3
when an item removed. background of all items after that need to be updated.
Method 1
so after you remove item with position of p from list of itemList and notify it you have to do this.
if (itemList.size - p > 0)
notifyItemRangeChanged(p, itemList.size - p)
Method 2
at this point it should work but it can be more optimized. you need just change background but now it will call onBindViewHolder for each items after p.
you can use payload to just change background and do nothing more.
add a const value:
const val PAYLOAD_BACKGROUND = 10
change first method code into this. it says that only update background.
if (itemList.size - p > 0)
notifyItemRangeChanged(p, itemList.size - p, PAYLOAD_BACKGROUND)
override this method
onBindViewHolder(holder, position, payloads)
after you implemented this method the previous onBindViewHolder method will not call anymore. and you have to do all of that inside this method.
onBindViewHolder(holder, position, payloads) {
if (payloads.contains(PAYLOAD_BACKGROUND) || payloads.isEmpty()) {
// set background color
}
if (payloads.isEmpty()) {
// do anything else that you were doing inside onBindViewHolder
}
}
if payloads is not empty it means this is a partial update. but if payloads is empty means this is a complete update like the old onBindViewHolder
Related
I am actually making some visibility changes to items that are clicked of the recycler view. But when the user clicks on one object and then clicks on the other object then the previous object should come to its initial state.
The manager.findViewByPosition(position) is working fine if the view is in focus of the screen but I am not able to get the view if the element is not in current focus.
For example:- the user clicks on 1st(position) item then clicks on the last position then the findViewByPosition returns a null.
Please help and let me know if there is some other way of doing it.
The expected result should be the view of the last item to be refreshed but it's not happening for the views that are not in the current focus of the screen.
Below is my code snippet. Updated with what you suggested.
public class BodyPartWithMmtRecyclerView extends
RecyclerView.Adapter<BodyPartWithMmtRecyclerView.ViewHolder>
{
//variables defined.
int selectedPosition = -1;
static class ViewHolder extends RecyclerView.ViewHolder {
//All the view items declared here.
ViewHolder(View view) {
super(view);
//All the views are defined here.
}
}
public BodyPartWithMmtRecyclerView(List<BodyPartWithMmtSelectionModel> bodyPartsList, Context context){
//array list initialization and shared preference variables initialization
}
public BodyPartWithMmtRecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
//Creating a new view.
}
public void onBindViewHolder(#NonNull final BodyPartWithMmtRecyclerView.ViewHolder holder, #SuppressLint("RecyclerView") final int position) {
BodyPartWithMmtSelectionModel bodyPartWithMmtSelectionModel = bodyPartsList.get(position);
holder.iv_bodypart.setImageResource(bodyPartWithMmtSelectionModel.getIv_body_part());
holder.tv_body_part_name.setText(bodyPartWithMmtSelectionModel.getExercise_name());
if(selectedPosition!=position && selectedPosition!=-1){
//updated the elements view to default view. Like made the visibility and other changes here.
}
//some click listeners on the sub-elements of the items. Like textviews, spinner, etc
holder.iv_bodypart.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((BodyPartSelection)context).setFabVisible();
if(selectedPosition!=-1){
((BodyPartSelection)context).visibilityChanged(selectedPosition,position);
/*here what I was doing is whenever the user clicks on an item I check weather a previous item is clicked or not then if yes then I send the position to a function that makes it to default but the issue was that if the item is not in the focus of the screen the findViewByPosition returns null.*/
}
selectedPosition = position;
bodypartSelected = holder.tv_body_part_name.getText().toString();
holder.iv_bodypart.setVisibility(View.INVISIBLE);
holder.rl_left_right.setVisibility(View.VISIBLE);
}
});
//and other listeners below
}
#Override
public int getItemCount() {
return bodyPartsList==null?0:bodyPartsList.size();
}
#Override
public int getItemViewType(int position) {
return position;
}
}
VisibilityChanged function
public void visibilityChanged(int position, int clicked){
View view = manager.findViewByPosition(position);
if(view!=null) {
Log.i("inside","visibility change");
ImageView imageView = view.findViewById(R.id.bodypartImage);
//other elements and changing the visibility of elemets to default.
}
}
I have updated my code based on the snippet you updated. Please don't change the visibility condition if-else I have added with any different logic which I saw in your code snippet. As you did, it will not update both selected and default view as RecyclerView reuse the view layout. So if the condition is not proper, you may see multiple items as selected or some other types of unwated behaviour.
public void onBindViewHolder(#NonNull final BodyPartWithMmtRecyclerView.ViewHolder holder, #SuppressLint("RecyclerView") final int position) {
BodyPartWithMmtSelectionModel bodyPartWithMmtSelectionModel = bodyPartsList.get(position);
holder.iv_bodypart.setImageResource(bodyPartWithMmtSelectionModel.getIv_body_part());
holder.tv_body_part_name.setText(bodyPartWithMmtSelectionModel.getExercise_name());
if(selectedPosition == position){
//updated the elements view to SELECTED VIEW. Like made the visibility and other changes here.
} else {
//updated the elements view to default view. Like made the visibility and other changes here.
}
//some click listeners on the sub-elements of the items. Like textviews, spinner, etc
holder.iv_bodypart.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((BodyPartSelection)context).setFabVisible();
/Comment by Hari: Don't try to change the visibility of default as it will be done automatically after calling notifyDataSetChanged(). */
if(selectedPosition!=-1){
((BodyPartSelection)context).visibilityChanged(selectedPosition,position);
/*here what I was doing is whenever the user clicks on an item I check weather a previous item is clicked or not then if yes then I send the position to a function that makes it to default but the issue was that if the item is not in the focus of the screen the findViewByPosition returns null.*/
/*Comment by Hari: This snippet is valuable which is missing as you are getting null issue here.
However Don't try to change the visibility of default as it will be done automatically after calling notifyDataSetChanged(). */
}
selectedPosition = position;
bodypartSelected = holder.tv_body_part_name.getText().toString();
holder.iv_bodypart.setVisibility(View.INVISIBLE);
holder.rl_left_right.setVisibility(View.VISIBLE);
//Keep this as last statement in onClick
notifyDataSetChanged();
}
});
//and other listeners below
}
Let me know your further response.
Based on #Hari N Jha's Answer.
Call notifyDataSetChanged() when you update anything. E.g
int selectedPosition = -1;
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
//....
if(position == selectedPosition) {
//Add background color change of your layout or as you want for selected item.
} else {
//Add background color change of your layout or as you want for default item.
}
notifyDataSetChanged(); //Call notifyDataSetChanged() here after done all the stufs
//...
}
I have a RecyclerView with a Horizontal LinerLayout. It displays numbers from 10 to 1, that is used to rate something.
When I select 10 and scroll back to 1 and select 1. I have to update the UI to remove selection on 10 and update selection on 1. But, when I use findViewHolderForAdapterPosition() to remove the selection on 10 it gives me a NullPointerException
I am getting the position in the ViewHolder with getAdapterPosition().
Then, I use that position to get the ViewHolder by calling findViewHolderForAdapterPosition() on my recycler view object and update the UI to remove the selection from 10.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
vh.textRating.setBackgroundResource(R.drawable.rating_background_selected_orange);;
With some tests, I found out when I try to do the same thing without scrolling it works fine. However, only when I am scrolling it gives me a NullPointerException
How do I fix this?
As requested here is some important code from Adapter class.
#Override
public void onBindViewHolder(RatingRecyclerAdapter.ViewHolder holder, int position) {
String itemText = itemList.get(position);
holder.textRating.setText(itemText);
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView textRating;
public ViewHolder(View itemView) {
super(itemView);
textRating = (TextView) itemView.findViewById(R.id.text_rating);
textRating.setOnClickListener(ratingClickListener);
}
private final View.OnClickListener ratingClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (callback != null) {
callback.onClickRating(v, position);
}
}
};
}
Activity Class
#Override
public void onClickRating(View view, int position) {
RatingRecyclerAdapter.ViewHolder vh;
int color;
int previousPosition = mAdapter.getSelectedPosition(); //Get previously clicked postion if any.
if (previousPosition == Constants.NO_ITEM_SELECTED) {
// An item was selected first time
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color);
return;
}
if (position == previousPosition) // Same item was selected
return;
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setUnselectedRatingResource(vh, color); // Remove the previous selected item drawables.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color); // Set the new selected item drawables. Setting some background to indicate selection.
}
As Sevastyan has written in the comment, the RecyclerView immediately recycles the view as soon as the item is out of the screen. So if we call findViewHolderForAdapterPosition() for a view which is outside the screen we get a null value. (I am not confirming this is the actual case. But, this is what it seems to me.)
So I created a class that stores all the data about an item in the RecyclerView and stored all the colours and value of that item in the class. And when we are populating the view, set the all the colours based on data stored in that class.
PS: I THANK Sevastyan for not giving me the answer directly. But, only giving me the reason for getting that Exception.
If your view is out of the screen, it can be recycled OR cached.
In case it's recycled, you can handle in onViewRecycled() method or setup the view again inside onBind() when the view becomes visible (you can save the state on the object of your list if needed).
In case it's not recycled (onViewRecycled method not called for that position), it's probably cached. You can set the cache size to zero to prevent this state from happening.
recycler.setItemViewCacheSize(0)
I am using default project for Android TV. Following is the code for creating cards in my BrowseFragment:
private void loadRows() {
List<Movie> list = MovieList.setupMovies();
ListRowPresenter mListRowPresenter = new ListRowPresenter();
mRowsAdapter = new ArrayObjectAdapter(mListRowPresenter);
mListRowPresenter.setRowViewSelected(/*HOW TO GET VIEWHOLDER HERE?*/, false);
CardPresenter cardPresenter = new CardPresenter();
int i;
for (i = 0; i < NUM_ROWS; i++) {
if (i != 0) {
Collections.shuffle(list);
}
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(cardPresenter);
for (int j = 0; j < NUM_COLS; j++) {
listRowAdapter.add(list.get(j % 5));
}
HeaderItem header = new HeaderItem(i, MovieList.MOVIE_CATEGORY[i]);
mRowsAdapter.add(new ListRow(header, listRowAdapter));
}
setAdapter(mRowsAdapter);
}
I am doing this as I don't want to make first card of row get selected when I launch app. It should only get selected after user press down button on Dpad. If I can't do it this way, what should I do to get mentioned behavior?
You can setRowViewSelected by subclassing ListRowPresenter and overriding initializeRowViewHolder(RowPresenter.ViewHolder holder)
#Override
protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) {
super.initializeRowViewHolder(holder);
setRowViewSelected(holder, false);
}
But I don't think you can unselect all items in BrowseFragment using this approach.
Try setting your ItemViewSelectedListener after your data is loaded instead of setting in onActivityCreated to have all items unselected on initial launch.
Possible reason why top left item of row will always get selected by default and you cannot have all unselected items on initial launch:
BrowseFragment's onItemSelected method (line 1372-1382) on initial launch calls mMainFragmentRowsAdapter.getSelectedPosition()
#Override
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
int position = mMainFragmentRowsAdapter.getSelectedPosition(); //<--
if (DEBUG) Log.v(TAG, "row selected position " + position);
onRowSelected(position);
if (mExternalOnItemViewSelectedListener != null) { //<--
mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
rowViewHolder, row);
}
}
where getSelectedPosition() always returns 0
(line 483-485)
public int getSelectedPosition() {
return 0;
}
It also calls mExternalOnItemViewSelectedListener.onItemSelected where mExternalOnItemViewSelectedListener is the ItemViewSelectedListener that you set in MainFragment of your app.
So on initial launch, 0th item in 0th row gets selected as a default selected item but if you delay setting mExternalOnItemViewSelectedListener this call will not reach your item selected listener the first time.
you can use this callback method .
void onRowViewSelected (RowPresenter.ViewHolder vh,
boolean selected)
Called when the given row view changes selection state. A subclass may override this to respond to selected state changes of a Row. A subclass may make visual changes to Row view but must not create animation on the Row view.
mListRowPresenter.setRowViewSelected(vh, false);
why you are deselecting initially ? i didn't get your Question Clearly can you please explain what you want to do Exactly ??
I have a RecyclerView list of CardViews with a defaultbackground of white. I set up a OnLongClickListener to select the CardView and load a DialogFragment to confirm deletion for the item (CardView) and change the background color to red.
Everything is working correctly except the first CardView created in the list is already showing a red background even though the user has not OnLongClicked the CardView. Thereafter, the newest CardView added always shows the red background even when the user has not yet OnLongClicked the CardView. What am I missing here?
background_selector.xml:
...
<!-- Normal state. -->
<item android:drawable="#color/list_contact_item_default"
android:state_pressed="false"
android:state_selected="false" />
<!-- Selected state. -->
<item android:drawable="#color/item_selected"
android:state_pressed="false"
android:state_selected="true" />
</selector>
list_contact_tem.xml:
<android.support.v7.widget.CardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/singlecard_view1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
card_view:cardCornerRadius="6dp"
card_view:cardElevation="4dp" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/background_selector">
...
Adapter file:
public class ContactListAdapter extends RecyclerView.Adapter<ContactListAdapter.ContactHolder>{
...
private int selectedPos;
#Override
public ContactHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_contact_item, parent, false);
final ContactHolder contactHolder = new ContactHolder(view);
// Attach a LongClick listener to the items's (row) view.
contactHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
// Save the new selected position.
selectedPos = contactHolder.getAdapterPosition(); // get the item position.
if (selectedPos != RecyclerView.NO_POSITION) {
if (recyclerItemClickListener != null) {
recyclerItemClickListener.onItemLongClick(selectedPos, contactHolder.itemView);
// Temporarily save the last selected position
int lastSelectedPosition = selectedPos;
// Update the previous selected row
notifyItemChanged(lastSelectedPosition);
notifyItemChanged(selectedPos);
}
}
return true;
}
});
return contactHolder;
}
#Override
public void onBindViewHolder(ContactHolder holder, int position) {
final Contact contact = contactList.get(position);
if(position == selectedPos) {
holder.itemView.setSelected(true);
} else {
holder.itemView.setSelected(false);
}
holder.thumb.setImageBitmap(letterBitmap);
holder.name.setText(contact.getName());
holder.phone.setText(contact.getPhone());
}
You need to declare an external array to keep track of the items selected. Let us have an array which have the same size as the list.
private int[] selectedArray = new int[yourList.size()];
// Now initialize the Array with all zeros
for(int i=0; i<selectedArray.length; i++)
selectedArray[i] = 0;
Now inside your adapter, when an item is clicked, set 1 in the proper position in the selectedArray. So that, we can keep track which item is selected now.
Now modify your adapter code as follows
// Attach a LongClick listener to the items's (row) view.
contactHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
// Update the selectedArray. Set 1 for the selected item
// and 0 for the others.
updateSelectedArray(contactHolder.getAdapterPosition());
if (selectedPos != RecyclerView.NO_POSITION) {
if (recyclerItemClickListener != null) {
recyclerItemClickListener.onItemLongClick(selectedPos, contactHolder.itemView);
// Repopulate the list here
notifyDatasetChanged();
}
}
return true;
}
});
And in your onBindViewHolder
final int SELECTED = 1;
if(selectedArray[position] == SELECTED) {
holder.itemView.setSelected(true);
} else {
holder.itemView.setSelected(false);
}
Your updateSelectedArray() may look like this
private void updateSelectedArray(int position) {
for(int i=0; i<selectedArray.length; i++){
if(i == position) selectedArray[i] = 1;
else selectedArray[i] = 0;
}
}
And after you delete an item from the list, you need to reset the selectedArray` as the item is no longer available in your list.
So your deleteFromList function may look like:
private void deleteFromList(int position)
{
yourList.remove(position);
selectedArray = new int[yourList.size()];
// Now re-initialize the Array with all zeros
for(int i=0; i<selectedArray.length; i++)
selectedArray[i] = 0;
}
I've taken an array to keep track of the selected item. But you can take an ArrayList or any data structure you like to keep track of it. The idea is to keep track of the selected item properly.
Update
The problem of having randomly selected items in your list is caused by not binding your views properly. So when you delete an item, you need to do the following things.
Reset your selected position in the selectedArray. i.e. selectedPos = -1
Remove the item from the list.
Call notifyDatasetChanged to take the effect of your changes in the
list.
And most importantly don't forget to set the else part. What I meant is
// Pseudo code
if(position == selected) itemView.setSelected(true);
else itemView.setSelected(true); // Don't forget this else
I'm using a RecyclerView to implement a NavigationDrawer.
I got click events working, but I can't figure out how to have the first item selected on App start and following that keep the selected item higlighted even if the drawer is not shown.
All I've been able to find is multi-selection in RecyclerView.
I actually just implemented this in an app I am working on. So this method worked:
First create a variable to track the current selected position at the top of your adapter:
private int selectedItem;
Then in your Adapter constructor initiate the selectedItem value you would like:
public NavDrawerMenuListAdapter(Context context, List<NavDrawerItem> data, NavDrawerMenuListViewHolder.NavDrawerMenuClickInterface listener) {
this.context = context;
mLayoutInflater = LayoutInflater.from(context);
this.navDrawerItems = data;
this.listener = listener;
selectedItem = 0;
}
Here I use 0 as this is the first item in my menu.
Then in your onBindViewHolder(NavDrawerMenuListViewHolder holder, int position) just check whether your selectedItem == position and set the background of some view to a seleted background like so:
if (selectedItem == position) {
holder.single_title_textview.setTextColor(0xff86872b);
holder.nav_drawer_item_holder.setBackgroundColor(Color.DKGRAY);
}
Here I set the text color to green and give the Realativelayout parent a gray background on start. You can, of course, customize this in any way you like.
To implement a selection of an item and keep the state I use the following method:
public void selectTaskListItem(int pos) {
int previousItem = selectedItem;
selectedItem = pos;
notifyItemChanged(previousItem);
notifyItemChanged(pos);
}
This method I usually call from the OnClick() method.
Hope this helps!.