ViewHolder Layout Updates don't apply from onBindViewHolder() - android

The setup I've got a RecyclerView with a custom adapter and custom ViewHolders.
I understand the onCreateViewHolder() method is called when the RecyclerView is running out of ViewHolders to recycle and needs a new one. So I'm just inflating a layout in there and passing it to a new ViewHolder.
Furthermore, onBindViewHolder() is responsible for filling the ViewHolder with data as soon as a new ViewHolder has been created or recycled by the RecyclerView. So what I'm doing in there is calling my method holder.setNode() to pass a data object to the ViewHolder.
The behavior I'm seeing When the activity first launches, all entries are correct. When I'm adding new entries or deleting existing ones, however, things start to get a bit funny.
the title TextView is always set correctly
the background color of the main layout changes seemingly at will, I'm assuming because the RecyclerView is reusing old ones
as does the custom view I have implemented, even though I'm invalidating it and passing it new values which change its appearance noticeably
So I'm wondering: Why aren't those values changed in onBindViewHolder() as soon as views get reused? Or if I'm wrong, what's the real reason for the random switching of layouts?
TaskListAdapter
class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
private ArrayList<NodeHandler.DbNode> dbNodeList = new ArrayList<>();
...
#Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.small_task_view, parent, false);
return new TaskViewHolder(v);
}
#Override
public void onBindViewHolder(TaskViewHolder holder, int position) {
final NodeHandler.DbNode dbNode = dbNodeList.get(position);
holder.setNode(dbNode);
holder.wrapper.findViewById(R.id.card_details).setVisibility(View.GONE);
}
...
public static class TaskViewHolder extends RecyclerView.ViewHolder implements ItemTouchHelperViewHolder {
private FrameLayout wrapper;
private TextView title;
private NodeHandler.DbNode dbNode;
public TaskViewHolder(View view) {
...
}
public void setTitle(String str) {
title.setText(str);
}
public void setMarkers(#IntRange(from = 1, to = Node.MAX_URGENCY) int urgency, #IntRange(from = 1, to = Node.MAX_IMPORTANCE) int importance) {
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
((QuadrantView) wrapper.findViewById(R.id.quadrant_view)).setDimensions(importance, urgency);
// setDimensions will invalidate the view
}
public void setNode(NodeHandler.DbNode dbNodeObject) {
this.dbNode = dbNodeObject;
setTitle(dbNode.toString());
setMarkers(dbNode.getUrgency(), dbNode.getImportance());
setTips();
}
}
}
Let me know if anything else could matter here. I'd be happy to update the question accordingly.

Values are indeed changed in onBindViewHolder as soon as views get reused.
The real reason for the seemingly random switching of layouts is that onBindViewHolder is currently implemented in a way that assumes that the ViewHolder was freshly created and is being bound for its first time. onBindViewHolder should instead be implemented in a way that assumes that the ViewHolder being bound is being reused so it should either:
reset all the values of the ViewHolder to default values first before setting them to other values or
make sure that everything is set inside onBindViewHolder, so one cannot tell that it was ever previously bound to something else.
Random background color changes:
You are right for suspecting that the random background color problem is caused by the RecyclerView reusing ViewHolders.
The reason why this is happening is because of the following code:
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
It only sets the background if the ViewHolder is not an appointment. So if a ViewHolder that is being reused was previously not for an appointment, but is currently for one that is now an appointment, it's background color will be inappropriate.
to fix this, do any of the following:
set the background color of the ViewHolder to some default color before the if statement is executed (as per solution 1 mentioned above):
wrapper.setBackgroundColor(/* default background color */);
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
add an else block to the if statement to set the background color of the ViewHolder to the appropriate color (as per solution 2 mentioned above)
if(!dbNode.isAppointment()) {
wrapper.setBackgroundColor(ContextCompat.getColor(wrapper.getContext(), R.color.lightGray));
}
else
{
wrapper.setBackgroundColor(/* appointment background color */);
}
override the RecyclerView.Adapter's getItemViewType to return different view types based on dbNode.isAppointment(), and create different ViewHolder subclasses for displaying each of them
p.s. I don't know what the problem could be regarding the custom views...sorry

Related

how to change specific items in recycler view?

I want to remove textview from specific items in recycler view. I wrote the below code to do that
#Override
public void onBindViewHolder(#NonNull final Myholder holder, final int possion) {
final String n = names.get(possion);
if(possion==3){
holder.textView.setVisibility(View.GONE);}}
but this changes all items and i want to make gone only position number 3 item view.
this is my view holder
public static class Myholder extends RecyclerView.ViewHolder {
TextView textView;
public Myholder(#NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textfolder);
}
}
what should i do?
Always remember that RecyclerView recycles view holders. That is, the same ViewHolder instance will be re-used for different views. This means that it is almost always a bad idea to have an if statement that modifies a view withouth having a corresponding else.
So, try this instead:
if (possion == 3) {
holder.textView.setVisibility(View.GONE);
} else {
holder.textView.setVisibility(View.VISIBLE);
}
Note also that just checking the position argument is not necessarily a good idea. If you use notifyItemInserted() or notifyItemRemoved(), this can lead to problems.
It would be better to set something about the item at that position to indicate that its text should not be shown.

How to make RecyclerView stops recycling defined positions?

My problem is: I have a video streaming happening on one of the views inside the RecyclerView.
When the user scrolls, the view gets recycled and other cameras starts their own streaming on that recycled viewholder. This is bad for user interface since the streaming process takes some seconds to start.
How can I say to the RecyclerView: "Hey Recycler, please, do not recycle that exact position x and give that position ALWAYS the same viewholder you gave it the first time, instead of random one"?
Please someone help me =(
In your getItemViewType(int position) method of adapter, assign unique values for each video, so it will always return same ViewHolder for same video as you wish.
return unique positive number as type for each video type (here i used the adapter position as unique key)
return negative numbers for any non-video items. (nothing special here, just to avoid conflicts with video items, we use negative numbers for non-video items)
I hope you get the idea. cheers :)
#Override
public int getItemViewType(int position) {
// Just as an example, return 0 or 2 depending on position
// Note that unlike in ListView adapters, types don't have to be contiguous
if(dataList.get(position).isVideo()){
return position;
}else{
return -1;//indicates general type, if you have more types other than video, you can use -1,-2,-3 and so on.
}
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case -1: View view1 = LayoutInflater.from(parent.getContext())
.inflate(R.layout.general_item, parent, false);
return new GeneralViewHolder(view1);
default:View view2 = LayoutInflater.from(parent.getContext())
.inflate(R.layout.video_item, parent, false);
return new VideoViewHolder(view2);
}
}
Perform viewHolder.setIsRecyclable(false) on the ViewHolder you want not to be recycled.
From docs of ViewHolder#setIsRecyclable(boolean):
Informs the recycler whether this item can be recycled. Views which are not recyclable will not be reused for other items until setIsRecyclable() is later set to true.
This will cause only one ViewHolder to be created.
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
...
#Override
public void onViewAttachedToWindow(final RecyclerView.ViewHolder holder) {
if (holder instanceof VideoViewHolder) {
holder.setIsRecyclable(false);
}
super.onViewAttachedToWindow(holder);
}
#Override
public void onViewDetachedFromWindow(final RecyclerView.ViewHolder holder) {
if (holder instanceof VideoViewHolder){
holder.setIsRecyclable(true);
}
super.onViewDetachedFromWindow(holder);
}
...
}
RecyclerView uses one view multiple times, when it contains the list which is not displaying on the screen at a time(means a list contain large amount of items which is not displaying on screen at same time you need to scroll up and down). When user scroll the list the offscreen items are reused to display the remaining list items which is called recycling.
To Stop recycling the items call this method in your onBindViewHolder method:
viewHolder.setIsRecyclable(false);
This statement stop the recycling the views.
To Start recycling the items call this method in your onBindViewHolder method:
viewHolder.setIsRecyclable(true);
I hope this will solve your problem.
Thanks
Your problem comes from the viewholder itself. Viewholders keep reference to views, while the adapter don't. The adapter keeps the data collection only. So, add a field to the viewholder to keep a reference of the data element you used to populate the view in the viewholder. In other words:
public class SomeViewHolder extends RecyclerView.ViewHolder{
private View view;
private Data data;
public SomeViewHolder(View itemView) {
super(itemView);
view = itemView;
}
public void bindData(Data data){
view.setData(data);
this.data = data;
}
public void setData(Data data){
this.data = data;
}
public Data getData(){
return data;
}
public View getView(){
return view;
}
}
Now, the viewholder know which element of the adapter is using. Therefore, when overriding the binding method in the adapter, you can check if the holder has already bonded with some data, and, if the data contains video, you can avoid the binding and forcefully set an already loaded view.
#Override
public void onBindViewHolder(SomeViewHolder holder, int position) {
//videoViewData is a data field you have to put into the adapter.
//videoView is a view field you have to put into the adapter.
if(adapterData.get(position).equals(videoViewData)){
holder.setView(videoView);
holder.setData(adapterData.get(position));
}else{
holder.bindData(adapterData.get(position));
if(adapterData.get(position).isVideo()){
videoViewData = adapterData.get(position);
videoView = holder.getView();
}
}
}
Finally, you'll have to override the onViewRecycled method in the adapter, so, when a view containing a video gets recycled, you can get the view and put it somewhere else.
public void onViewRecycled(SomeViewHolder holder){
if(holder.getData().isVideo()){
videoViewData = holder.getData().
videoView = holder.getView();
videoView.pauseVideo();
}
}
keep in mind, this can cause some serious leaks if you don't manage the stored view. Also, you have to define methods for telling when your data is video, and a properly defined equals method.
Best way to handle item not to recycle in recyclerview this answer will resolve your problem.
Not to recycle item
Try using this for that particular position:
holder.setIsRecyclable(false);
Hope this may help.
If You are using query, you can use
query.limit(//no of items you want to show in your RecyclerView)
give it a try.
or Plese post your QueryCode

Accessing a list item's view and data at the same time

I am working on a messaging application and would like to set the chat bubble as coming from the left or the right depending on who the owner of the message is.
public class Message {
public String messageText;
public boolean mine;
// ...Constructor
}
Inside MessageAdapter I have:
#Override
public MessageHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(this.itemResource, parent, false);
return new MessageHolder(this.context, view);
}
#Override
public void onBindViewHolder(MessageHolder holder, int position) {
Message message = this.messages.get(position);
holder.bindMessage(message);
}
I have been following tutorials to migrate from ListView to RecyclerView and am now unclear as to how to access both an item's data and its view at the same time without getting NullPointerException. The code I want to use looks like this:
Drawable background = getResources().getDrawable(message.mine ? R.drawable.bubble_right : R.drawable.bubble_left);
background.setColorFilter(getResources().getColor(R.color.ColorPrimary), PorterDuff.Mode.SRC_IN);
view.findViewById(R.id.message_text).setBackgroundDrawable(background);
However, if I put it in onCreateViewHolder I can't figure out how to access the individual message to check ownership. If I put it in onBindViewHolder I have message but can't get its view. How do I solve this?
If you are moving from ListView to RecyclerView, then it's very easy to understand.
getView() (of ListView) == onCreateViewHolder() + onBindViewHolder();
If you remember we need to check view given by getView is null or not, and if it's null then we need to take care of instancing otherwise set the data to corresponding views.
RecyclerView are making our life easy, the onCreateView creates new instance of the view of each row(minimum required) and onBindViewHolder takes care of binding data to each view element.
I think now you are clear why and how to handle these two methods of RecyclerView.
As per your code, the line view.findViewById(R.id.message_text) should be moved to your ViewHolder class.
And the following lines will go in onBindViewHolder
Drawable background = getResources().getDrawable(message.mine ? R.drawable.bubble_right : R.drawable.bubble_left);
background.setColorFilter(getResources().getColor(R.color.ColorPrimary), PorterDuff.Mode.SRC_IN);
holder.your_view.setBackgroundDrawable(background);
Let me know if you want to know further clarifications.

Android - Custom dynamically-generated compound View recreated in RecyclerView, causing poor performance

I'm using a custom CompoundView which extends LinearLayout to display items of a RecyclerView. Each item displays an article which contains multiple paragraphs and images. The CompoundView adds TextView or ImageView dynamically based on the data attached by CompoundView.setData(List<DataPiece> pieces), the number of which is unknown before data is attached. Each DataPiece object tells CompoundView whether it's a piece of text or an image. And here is the code for CompoundView.setData(List<DataPiece> pieces):
public void setData(List<DataPiece> pieces) {
removeAllViews();
for (DataPiece dataPiece : pieces) {
switch (dataPiece.getType()) {
case IMAGE:
ImageView imageView = new ImageView(getContext());
...
addView(imageView);
break;
case TEXT:
TextView textView = new TextView(getContext());
...
addView(textView);
break;
}
}
}
In the RecyclerView.Adapter.onBindViewHolder(), the data is attached to CompoundView by calling MyViewHolder.compoundView.setData(...). And it works fine when the RecyclerView is created.
However, for a CompoundView item with multiple ImageViews and TextViews, when I scroll away from it and then scroll back, the scroll becomes heavily unsmooth.
I guess it's because removeAllViews() in setData() is called, and the CompoundView creation for-loop is executed again by the recycler. But I don't know how to avoid this.
And I also wonder why the scroll is always smooth when using TextView(with Images) in a RecyclerView even it's recycled too.
Thanks in advance!
There are multiple considerations that could go into deciding what the best approach might be.
First, do you have an idea about the maximum number of items in the recycler's list? If it is just a handful, maybe you could ditch the RecyclerView approach and just add your CompoundView into a container hosted by a ScrollView.
Secondly - is the layout of each item fairly complicated (a.k.a. are there many TextViews, ImageViews etc. in it)? If yes, maybe you could take an approach that would resemble an ExpandableListView - show a summary as each list item and expand to the full layout of the item on click.
Thirdly - if none of the above is acceptable and you still want to go the current approach - don't construct/add your view in the binding method. Do it in the onCreateViewHolder, when the system expects you to construct your view (I don't know for sure but by the time you're called on onBindViewHolder your view might have been already added to the hierarchy and any hierarchical change to it has a ripple effect on its containers - but don't take my word for it, I don't actually know the view is already added, it is just an assumption). You will have to assign each item a different type, so that in onCreateViewHolder you could match the view type with the supporting data (for the addition of the corresponding number of child views); create the view from scratch each time - this way you don't need to call on removeAllViews. Something like(I left out parts of the adapter that are not relevant to the case):
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
ArrayList<DataPiecesList> mItems;
public RecyclerViewAdapter(ArrayList<DataPiecesList> items) {
mItems = items;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
CompoundView compoundView = new CompoundView();
List<DataPiece> dataPieces = mItems.get(viewType);
for (int i = 0; i < dataPieces.size(); i++)
{
// construct TextView or ImageView or whatever
compoundView.add(child);
}
MyViewHolder view = new MyViewHolder(compoundView);
return view;
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
CompoundView compoundView = viewHolder.itemView;
DataPiece dataPiece = mItems.get(i);
for (int j = 0; j < compoundView.getChildCount(); j++)
{
compoundView.getChildAt(j) <- dataPiece.get(j);
}
}
#Override
public int getItemViewType(int position) {
return position;
}
#Override
public int getItemCount() {
return mItems.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
...
public MyViewHolder(View itemView) {
super(itemView);
}
}
}
RecyclerView is supposed to reuse the views. It will be slow if you throw away the already created TextView / ImageView objects and create new ones every time.
It sounds like you need a RecyclerView with multiple view types. The idea is to create multiple view holders - some of them with ImageView, the others with TextView. You'll have to override the getItemViewType(int position) method of your adapter - it should return different values for the IMAGE items and the TEXT items. The onCreateViewHolder(ViewGroup parent, int viewType) receives a viewType parameter so you know which type of ViewHolder to create there. In the onBindViewHolder(VH holder, int position) you could assume that the holder passed to you is the correct type (i.e. the type with TextView for TEXT items and the type with ImageView for IMAGE items), so there is no need to remove its child views and create them again.
There is nice article about RecyclerView's Adapters with multiple view types here.

Challenges in RecyclerView of Android L

I have been playing around with RecyclerView for a little bit. Is there any easy way to put OnClickListener for items in RecyclerView? I have tried implementing it in ViewHolder. The onClick event never got triggered.
And I have used notifyItemInserted(position) for adding new value into RecyclerView. The UI does not got refreshed automatically. Needed to pull up and down to refresh. But when I invoke notifyDatasetChanged(..), it is ok.
I have applied DefaultItemAnimator to RecyclerView. But, not seeing any animation when new item added.
Thanks advance for any idea.
This is the first Android L component I have tested out and I am stucking there.
Here is my Adapter class:
public class AdapterRecyclerView extends RecyclerView.Adapter<AdapterRecyclerView.MyViewHolder> {
private List<String> arrExperiences;
//Provide a reference to the type of views that you are using - Custom ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder {
public TextView tvExperienceTitle;
public TextView tvExperienceDesc;
public MyViewHolder(RelativeLayout itemView) {
super(itemView);
tvExperienceTitle = (TextView) itemView.findViewById(R.id.tv_experience_title);
tvExperienceDesc = (TextView) itemView.findViewById(R.id.tv_experience_desc);
}
}
//Provide a suitable constructor : depending on the kind of dataset.
public AdapterRecyclerView(List<String> arrExperiences){
this.arrExperiences = arrExperiences;
}
//Create new view : invoke by a Layout Manager
#Override
public AdapterRecyclerView.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RelativeLayout view = (RelativeLayout) LayoutInflater.from(parent.getContext()).inflate(R.layout.view_item_recycler, parent, false);
MyViewHolder myViewHolder = new MyViewHolder(view);
return myViewHolder;
}
#Override
public void onBindViewHolder(AdapterRecyclerView.MyViewHolder viewHolder, int position) {
//get element from your dataset at this position.
//replace the content of the view with this element.
viewHolder.tvExperienceTitle.setText(arrExperiences.get(position));
}
#Override
public int getItemCount() {
return arrExperiences.size();
}
public void addExperience(String experience, int position){
arrExperiences.add(position, experience);
notifyItemInserted(position);
//notifyDataSetChanged();
}
public void removeExperience(){
int index = (int) (Math.random() * arrExperiences.size());
arrExperiences.remove(index);
notifyItemRemoved(index);
//notifyDataSetChanged();
}
}
Simply add this in your Adapter:
#Override
public void onBindViewHolder(AdapterRecyclerView.MyViewHolder viewHolder, int position) {
yourItems.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
//do your stuff
}
});
}
Please see my answer here. You do need an extra class (which may be included as part of the full release) but it will allow you to create OnItemClickListeners the way you are used to for ListViews.
Since you still didn't mark correct any answer, and even if it's an old question, I will try to provide the way I do. I think it is very clean and professional. The functionalities are taken from different blogs (I still have to mention them in the page), merged and methods have been improved for speed and scalability, for all activities that use a RecycleView.
https://github.com/davideas/FlexibleAdapter
At lower class there is SelectableAdapter that provides selection functionalities and it's able to maintain the state after the rotation, you just need to call the onSave/onRestore methods from the activity.
Then the class FlexibleAdapter handles the content with the support of the animation (calling notify only for the position. Note: you still need to set your animation to the RecyclerView when you create it the activity).
Then you need to extend over again this class. Here you add and implements methods as you wish for your own ViewHolder and your Domain/Model class (data holder). Note: I have provided an example which does not compile because you need to change the classes with the ones you have in your project.
I think that, it's the ViewHolder that should keep the listeners of the clicks and that it should be done at the creation and not in the Binding method (that is called at each invalidate from notify...() ).
Also note that this adapter handles the basic clicks: single and long clicks, if you need a double tap you need to use the way Jabob Tabak does in its answer.
I still have to improve it, so keep an eye on it. I also want to add some new functionalities like the Undo.
Here you get a simple Adapter class which can perform onItemClick event on each list row for the recyclerview.

Categories

Resources