Get reference to ViewHolder at position in RecyclerView.Adapter - android

I have a RecyclerView with displays a List of items. One Item of this list should be my currentItem (int), that Item should be expanded. For that, I have this method expand() in my ViewHolder. I want to call this method when currentItem == position is.
I thought I could do it like this:
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final WorkoutExcersiseHolder excersiseHolder = (WorkoutExcersiseHolder) holder;
excersiseHolder.expand(currentExcersisePointer, position);
if (currentItem == position) {
excersiseHolder.expand();
}
}
However every Item gets expanded. So is it possible to get a reference to the ViewHolder at only one position in my RecyclerView.Adapter?

You should hide the item when the condition is false.
if (currentItem == position) {
excersiseHolder.expand();
} else {
excersiseHolder.collapse();
}
When you change the currentItem's value you should call notifyItemChanged() with the old value and the new one too.
notifyItemChanged(currentItem);
currentItem = newPosition;
notifyItemChanged(currentItem);

You have to bind your current item number to the model which you use to fill your RecyclerView. In your case I guess it is WorkoutExcersiseHolder class, which should have something like isCurrent() method, which will help to onBindViewHolder decide to expand it or not. Update your model list with your logic and then use RecyclerView.Adapter method notifyItemChanged. For your note, also don't use position parameter to reference current ViewHolder, but instead try to use holder.getAdapterPosition()

pass view holder object to the expand method and then expand throw this object in the expand method

Related

How to disable recyclerview scroll to top (1st item) on notifyDataSetChanged

I have fixed number of items in recycler view, but I update data every 10 seconds and calling notifyDataSetChanged();
When I scrolled to Nth item (EX:20th item) and on notifyDataSetChanged() called recycler view getting refreshed and auto-scroll back to 1st item. How can I stop scrolling and update the recycler view? Thanks in advance.
You can create a view type and holder for your first item then set setIsRecyclable
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof HeaderViewHolder ) {
((HeaderViewHolder ) holder).setIsRecyclable(false);
((HeaderViewHolder ) holder).bind(mData.get(position));
}
}
You should find which items changed and use adapter.notifyItemChanged(itemChangedPosition) instead of notifyDatasetChanged()
Using onSaveInstanceState you will save the state or current position of your recyclerview.Just like this.
Create variable in your activity
Parcelable recylerViewList; //this will store the position
then create this method to fetch recyclerview state
void storeInstance() {
AttachmentsActivity.recylerViewList=mRecyclerView?.layoutManager?.onSaveInstanceState()
}// call this method before notifyDataSetChanged()
Create method to restore the state
void restore() //call this method after notifyDataSetChanged()
{
mRecyclerView?.layoutManager?.onRestoreInstanceState(recylerViewList)
}
Make sure you call this methods in activity or through activity object.

RecyclerView scroll makes findViewHolderForAdapterPosition return null

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)

How to reset recyclerView position item views to original state after refreshing adapter

I have a RecyclerView with rows that have views that when clicked will be disabled for that row position.
The problem is after I update the adapter like this:
adapterData.clear();
adapterData.addAll(refreshedAdapterData);
notifyDataSetChanged();
After refreshing the data, the disabled views at the previous recycler position still remain disabled even though the data is refreshed.
How can I reset the views to the original state after refreshing adapter data.
Use below code.
adapterData.clear();
adapterData.addAll(refreshedAdapterData);
adapter.notifyDataSetChanged();
OR
recyclerView.invalidate();
When you call notifyDataSetChanged(), the onBindViewHolder() method of every view is called. So you could add something like this in the onBindViewHolder() of your Adapter method:
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
if (refreshedAdapterData.get(position).isInDefaultState()) {
//set Default View Values
} else {
//the code you already have
}
}
I have resolved this by putting a conditional statement inside onBindViewHolder method instructing all positions to reset the disabled views if data meets the required conditions for a refreshed data.
#Christoph Mayr, thanks for your comments. It helped point me in the right direction.
I cleared the data then notify change but the selected checkbox doesn't reset but just moves up one position. Let say I selected item #1 , move out of the RecyclerView, came back and it will auto select item #0.
So, I created new adapter again at onResume(), i worked for me but i don't know if it's the right way to handle this situation.
#Override
public void onResume() {
super.onResume();
if(selectedItems != null && selectedItems.size() > 0){
selectedItems.clear(); // if no selected items before then no need to reset anything
if(adapter != null && recyclerView != null){
// to remove the checked box
adapter = null;
adapter = new MyAdapter(items, new MyAdapter.MyAdapterListener() {
#Override
public void onSelected(int pos) {
selectedItems.add(items.get(pos));
}
#Override
public void onUnSelected(int pos) {
selectedItems.remove(items.get(pos));
}
});
recyclerView.setAdapter(adapter);
}
}
}

onBindViewHolder() is never called on view at position even though RecyclerView.findViewHolderForAdapterPosition() returns null at that position

I have a list with 13 items (although items may be added or removed), positions 0-12. When the fragment containing the RecyclerView is first shown, only positions 0 through 7 are visible to the user (position 7 being only half visible). In my adapter I Log every time a view holder is binded/bound (idk if grammar applies here) and record its position.
Adapter
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
From my Log I see that positions 0-7 are bound:
I have a selectAll() method that gets each ViewHolder by adapter position. If the returned holder is NOT null I use the returned holder to update the view to show it's selected. If the returned holder IS null I call selectOnBind() a method that flags the view at that position update to show it's selected when it's binded rather than in real time since it's not currently shown:
public void selectAll() {
for (int i = 0; i < numberOfItemsInList; i++) {
MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
mRecyclerView.findViewHolderForAdapterPosition(i);
Log.d(TAG, "holder at position " + i + " is " + holder);
if (holder != null) {
select(holder);
} else {
selectOnBind(i);
}
}
}
In this method I Log the holder along with its position:
So up to this point everything seems normal. We have positions 0-7 showing, and according to the Log these are the positions bound. When I hit selectAll() without changing the visible views (scrolling) I see that positions 0-7 are defined and 8-12 are null. So far so good.
Here's where it gets interesting. If after calling selectAll() I scroll further down the list positions 8 and 9 do not show they are selected.
When checking the Log I see that it's because they are never bound even though they were reported to be null:
Even more confusing is that this does not happen every time. If I first launch the app and test this it may work. But it seems to happen without fail afterwards. I'm guessing it has something to do with the views being recycled, but even so wouldn't they have to be bound?
EDIT (6-29-16)
After an AndroidStudio update I cannot seem to reproduce the bug. It works as I expected it to, binding the null views. If this problem should resurface, I will return to this post.
This is happening because:
The views are not added to the recyclerview (getChildAt will not work and will return null for that position)
They are cached also (onBind will not be called)
Calling recyclerView.setItemViewCacheSize(0) will fix this "problem".
Because the default value is 2 (private static final int DEFAULT_CACHE_SIZE = 2; in RecyclerView.Recycler), you'll always get 2 views that will not call onBind but that aren't added to the recycler
In your case views for positions 8 and 9 are not being recycled, they are being detached from the window and will be attached again. And for these detached view onBindViewHolder is not called, only onViewAttachedToWindow is called. If you override these function in your adapter, you can see what I am talking.
#Override
public void onViewRecycled(ViewHolder vh){
Log.wtf(TAG,"onViewRecycled "+vh);
}
#Override
public void onViewDetachedFromWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
}
Now in order to solve your problem you need to keep track of the views which were supposed to recycled but get detached and then do your section process on
#Override
public void onViewAttachedToWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
}
The answers by Pedro Oliveira and Zartha are great for understanding the problem, but I don't see any solutions I'm happy with.
I believe you have 2 good options depending on what you're doing:
Option 1
If you want onBindViewHolder() to get called for an off-screen view regardless if it's cached/detached or not, then you can do:
RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );
if ( view_holder != null )
{
//manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
notifyItemChanged( some_position );
The idea is that if the view is cached/detached, then notifyItemChanged() will tell the adapter that view is invalid, which will result in onBindViewHolder() getting called.
Option 2
If you only want to execute a partial change (and not everything inside onBindViewHolder()), then inside of onBindViewHolder( ViewHolder view_holder, int position ), you need to store the position in the view_holder, and execute the change you want in onViewAttachedToWindow( ViewHolder view_holder ).
I recommend option 1 for simplicity unless your onBindViewHolder() is doing something intensive like messing with Bitmaps.
When you have large number of items in the list you have passed to recyclerview adapter you will not encounter the issue of onBindViewHolder() not executing while scrolling.
But if the list has less items(I have checked on list size 5) you may encounter this issue.
Better solution is to check list size.
Please find sample code below.
private void setupAdapter(){
if (list.size() <= 10){
recycler.setItemViewCacheSize(0);
}
recycler.setAdapter(adapter);
recycler.setLayoutManager(linearLayoutManager);
}
I think playing with view is not a good idea in recyclerview. The approach I always use to follow to just introduce a flag to the model using for RecyclerView. Let assume your model is like -
class MyModel{
String name;
int age;
}
If you are tracking the view is selected or not then introduce one boolean to the model. Now it will look like -
class MyModel{
String name;
int age;
boolean isSelected;
}
Now your check box will be selected/un-selected on the basis of the new flag isSelected (in onBindViewHolder() ). On every selection on view will change the value of corresponding model selected value to true, and on unselected change it to false. In your case just run a loop to change all model's isSelected value to true and then call notifyDataSetChanged().
For Example, let assume your list is
ArrayList<MyModel> recyclerList;
private void selectAll(){
for(MyModel myModel:recyclerList)
myModel.isSelected = true;
notifyDataSetChanged();
}
My suggestion, while using recyclerView or ListView to less try to play with views.
So in your case -
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.clickableView.setTag(position);
holder.selectableView.setTag(position);
holder.checkedView.setChecked(recyclerList.get(position).isSelected);
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
#Override
public void onClick(View view){
int position = (int)view.getTag();
recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int position = (int)buttonView.getTag();
recyclerList.get(position).isSelected = isChecked;
}
Hope it will help you, Please let me know if you need any further explanation :)
So I think you question is answered below by #Pedro Oliveira. The main sense of RecycleView, that he using special algorithms for caching ViewHolder in any time. So next onBindViewHolder(...) may not work, for ex. if view is static, or something else.
And about your question you think to use RecycleView for dynamic changed Views. DON'T DO IT! Because RecycleView invalidates views and has caching system, so you will have a lot of problems.
Use LinkedListView for this task!

Issue refreshing a view of a row in recyclerview

I want to update a view of the recyclerview when a notifyItemChanged has been called. The thing is that I don't want to refresh the entire row but only the view of the row. (to avoid the blinking effect of the row)
There is a method called notifyItemChanged(int, payload obj).
Can I use that to achieve that? If so how to do it?
Finally I found how to update only the specific view of a row in RecyclerView.
(1) Override onBindViewHolder(Recycler.ViewHolder VH, int position, List payloads) in the adapter
(2) Inside that onBindViewHolder method,
if(payloads != null && !payloads.isEmpty() && (payloads.get(0) instanceof customObject)){
// update the specific view
}else{
// I have already overridden the other onBindViewHolder(ViewHolder, int)
// The method with 3 arguments is being called before the method with 2 args.
// so calling super will call that method with 2 arguments.
super.onBindViewHolder(holder,position,payloads);
}
(3) So to notify data change and update the view, need to call the notifyItemChanged(int position, Object payload). Need to pass the customObject(model which holds the data) as the payload object.
adapter.notifyItemChanged(i, obj);
Note : Need to disable the RecyclerView's ItemAnimator(By default it is enabled). Otherwise payload object will be empty.
recyclerView.setItemAnimator(null);
or
((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
UPDATE: I used this method recently without disabling the ItemAnimator. So no need to disable ItemAnimator. - (recyclerview-v7:25.4.0)
for further reference
You can use this to get the view of updated item:
View v = recyclerView.getLayoutManager().findViewByPosition(position);
if (v != null){
//update your view
}
Let's say you have an ArrayList of items, and it is fed to the RecyclerView adapter. Let it be something like this.
List<String> items = new ArrayList<>();
RecyclerAdapter adapter = new RecyclerAdapter(items);
recyclerView.setAdapter(adapter);
Now as I can see you want to insert items into the RecyclerView and call notifyItemChanged(int position) method to add only the single item.
For this let's say you have changed the value of item in position 0. That's the first item in the list.
Now the proper usage of the method notifyItemChanged(int position) will be
adapter.notifyItemChanged(0);
This means, the adapter will update the recyclerview with the new value in 0 position.

Categories

Resources