getChildAt(pos) returns null on RecyclerView - android

I am using RecyclerView where i have number of rows, each row has progress bar which updates.First four rows are visible.When row count is greater than 4 and i call getChildAt(pos) on my recyclerview to get the View,it is returning null. Kindly help.

As recyclerView.getChildAt(position) was giving me null in the above scenario, I finally solved using the lines below. It can be useful to people who run into similar scenarios.
RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)
recyclerView.findViewHolderForAdapterPosition(position);
if (null != holder) {
holder.itemView.findViewById(R.id.xyz).setVisibility(View.VISIBLE);
}

When there is more than one page of data in your recycler view and you're using getChildAt() method, it will throw a null pointer exception. This is because when the position is greater than the number of visible items on the display, getChildAt() won't work as it can only get to the view that is displayed on the screen.
Therefore, use recyclerView.layoutManager?.findViewByPosition(position) to get the view at the given position.

You can set tags for every item in your adapter:
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder,position);
holder.yourView.setTag(yourTag);
}
Then you can use the following code in your activity:
RecyclerView recyclerView=(RecyclerView)(findViewById(R.id.recycler));
View yourView=(View)(recyclerView.findViewWithTag(YourTag));

Related

Purpose of RecyclerView.NO_POSITION and smoothScrollToPosition()?

I’m some kind of new working with RecyclerView and I have noticed lately on one of the tutorials that they use RecyclerView.NO_POSITION with smoothScrollToPosition().
Here's the example:
private RecyclerView mRecyclerView;
private int mPosition = RecyclerView.NO_POSITION;
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (mPosition == RecyclerView.NO_POSITION) mPosition = 0;
mRecyclerView.smoothScrollToPosition(mPosition);
}
But I don’t know exactly what NO_POSITION or this smoothScrollToPosition() actually do? Also I tried to search official doc, other essays or checking other guys questions here. Unfortunately, none helped me.
Can anyone explain what their purpose/why do we need to use them?
NO_POSITION is a constant whose value is -1, it basically means that when don't you find the position of the model in the underlying dataset the return value of this method will be NO_POSITION.
smoothScrollToPostion(value) tells your view to go to that particular position in the recyclerView.
NO_POSITION is an int with -1 value;
smoothScrollToPosition() will scroll the RecyclerView to the requested position.
According to the API docs:
RecyclerView.NO_POSITION is a constant value -1.
And in case of getBindingAdapterPosition() method NO_POSITION is used for when item has been removed from the adapter, RecyclerView and/or Adapter.notifyDataSetChanged() has been called after the last layout pass or the ViewHolder has already been recycled.
smoothScrollToPosition starts a smooth scroll to an adapter position. If the adapter position is RecyclerView.NO_POSITION, then it means something went wrong, and it might even throw an exception in your code, so be careful!

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!

How to show and hide view in recycler view in Android?

In my app I am using recycler view.I want to show and hide view on particular condition.But when I scroll recycler views I am not getting expected behaviour.When I Visible a view it gets visible for other rows as well randomly.
What I understand is when it recycles it reuses view and when previous view when it gets recycled it finds the visibility of that view.How can I hide view on particular condition? here is my adapter code
#Override
public void onBindViewHolder(UrduRhymesViewHolder holder, int position) {
RhymesModel current = mUrduRhymesList.get(position);
AppUtility.setCustomFont(mContext, holder.tvUrduRhymesName, Constants.HANDLEE_REGULAR);
holder.tvUrduRhymesName.setText(current.getRhymeName());
holder.ivUrduRhymesLogo.setImageUrl(current.getThumbnailUrl(), mImageRequest);
int status = AppUtility.getFavouriteStatus(mContext, current.getRhymeName(), new UrduRhymesDb(mContext));
if (status == 0)
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star_unactive);
else
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star);
ProgressbarDetails progressbarDetails = ProgressbarDetails.getProgressDetail(current.getRhymeName());
if (progressbarDetails == null) {
progressbarDetails = new ProgressbarDetails();
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
} else {
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
}
ProgressbarDetails.addUpdateProgressDetail(current.getRhymeName(), progressbarDetails);
if (progressbarDetails != null && progressbarDetails.isDownloading) {
Log.e("test","downloading foe position "+position );
holder.downloadButtonLayout.setBackgroundResource(R.mipmap.btn_download);
holder.pbRhymeDownload.setVisibility(View.VISIBLE);
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
} else {
Log.e("test","should not be visible for position "+position);
holder.pbRhymeDownload.setVisibility(View.GONE);
}
}
Here progressbarDetails.isDownloading (having value true) is the criteria when I want to show my view but is else clause it is not hiding my view
Edit: Here ProgressbarDetails (Singleton )is a class keeping reference of every row of adapter's progress bar.
No direct way of hiding and unhiding recylerview childitems.
Solution:
Let us assume that the recyclerview adapter is ArrayList
Now make another arraylist (temp_list)
Scenarios:
Hide: iterate through your adapter items and remove the ones that you want to hide. Put each of these into temp_list. After iteration is over, call notifyDataSetChanged()
Show: iterate through your temp_list items and remove the ones that you want to show. Put each of these into adapter. After iteration is over, call notifyDataSetChanged()
You should add a flag in your viewHolder that indicates if this view should be displayed or not . and check this flag every time in the onBindViewHolder.
because the recyclerView reuses the same views you should make a decision depending on something special for every view in you viewHolder.
do u mean when your data has been changed?
and your layout want to change ?
adapter.notifyDataSetChanged();

Null pointer exception when trying to programmatically perform click on non visible Recycler view item

I have a recycler view and I want to perform click on one of its items.
Here is my code:
mRecyclerView.findViewHolderForAdapterPosition(2).itemView.performClick();
It works fine when the item is visible, but when it is not visible i'm getting a null pointer exception
also i tried scroll to position before perform click but i got same result
Any idea on how to solve this issue?
I have solved my problem with this code
mRecyclerView.getLayoutManager().scrollToPosition(17);
search_list.postDelayed(new Runnable() {
#Override
public void run() {
mRecyclerView.findViewHolderForAdapterPosition(17).itemView.performClick();
}
}, 50);
There is a slight delay for the viewholder to be created. Thus if the item is clicked before viewholder is created an NPE would occur
Unfortunately for you, this is working as intended. When a child View is scrolled out of the boundaries of a RecyclerView, the child View is often reused to display another item for another position in the list, hence you will get a null View for the position that is no longer displayed.
What you can do is implement a getItem() on the RecyclerView.Adapter to retrieve the item for that position. Not sure if that satisfies your requirements though.
It is recommended to use a listener to wait for the drawing to complete,
Then perform the operation you want.
recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// At this point the layout is complete
recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});

getChildCount() and getChildAt() return zero for RecyclerView

I am trying to get a count of RecyclerView children. I've tried this:
myRView.getChildCount();
And this:
LinearLayoutManager lm = ((LinearLayoutManager)myRViews.getLayoutManager());
lm.getChildCount();
I need to get a child View at certain position so that's why I need this. If I call getChildAt(index) I never get any child View.
Both of these methods return 0. How else to get a child View from RecyclerView? My items appear correctly and everything is working fine. I am calling these methods after I create my adapter and set it to RecyclerView. Thank you for your help.
I assume you are calling those methods before layout is calculated.
When you set the adapter, the layout will happen in the next view tree traversal. You can consider adding a ViewTreeObserver.
You could use your adapter getItemCount() function.
myRView.getAdapter.getItemCount();
The function is called when the view is not ready that's why it returns zero, use post() method will help.
myRView.post(new Runnable() {
#Override
public void run() {
Log.show("myRView.post " + myRView.getChildCount());
View childView = myRView.getChildAt(0);
}
});
getChildCount() may return 0 if you forget to set LayoutManager to RecyclerView.
myRView.setLayoutManager(mLayoutManager);

Categories

Resources