I have Recycler View with a lot of items in. What I want to do is to change the text in TextView inside item that was clicked. I did it in that way:
wordList.set(position, newWord);
MyProgressActivityAdapter newAdapter = new MyProgressActivityAdapter(wordList, this);
newAdapter.notifyItemChanged(position);
recyclerView.setAdapter(newAdapter);
And everything works fine except of the fact that the screen goes to the top every time I click item. What can I do to avoid that?
You should use the payload version of notifyItemChanged, here is a simple example for you to get the hang of it:
adapter.notifyItemChanged(position, "updateText");
And then in your RecyclerAdapter override the payload version of onBindViewHolder:
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position, #NonNull List payloads) {
if (payloads.isEmpty()) onBindViewHolder(holder, position);
else if ("updateText".equals(payloads.get(0))) {
if (holder instanceof YourViewHolder) {
((YourViewHolder) holder).textView.setText(dataProvider.get(position).getNewText());
}
}
}
Note that this approach prevents RecyclerView from creating a new ViewHolder and then binding your data, so you should just call the notifyItemChanged without resetting the adapter and so.
notifyItemChanged(position) should work if you handle it correctly. Try to handle this inside onBindViewHolder like below:
override fun onBindViewHolder(holder: RecyclerHolder, position: Int) {
holder.itemView.text_view.text = items[position]
holder.itemView.button.setOnClickListener {
items[position] = "New Text"
notifyItemChanged(position)
}
}
Related
I am trying to make a text change when a button located along with the text (layoutPasswd) in recycler view and to change it back if the button is again pressed.Like a password hiding button. The values to the adapter is from a static class object as arraylist. The problem occurring now is that the value for all the items (only for layoutPasswd) in recycler view is same.
public void onBindViewHolder(#NonNull final viewHolder holder, int position) {
holder.layoutUName.setText(users.get(position).getUserName());
pos = position;
holder.layoutPasswd.setText("********");
holder.btnViewChanger.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (holder.view1) {
holder.layoutPasswd.setText(users.get(pos).getPasswd());
holder.btnViewChanger.setText("hide");
holder.view1 = false;
} else {
holder.layoutPasswd.setText("********");;
holder.btnViewChanger.setText("Show");
holder.view1 = true;
}
}
});
You cannot rely on the ViewHolders or Views in a RecyclerView to hold any state, because they are recycled. Every time a view scrolls onto the screen, first it calls your onBindViewHolder function to update the contents of that ViewHolder to match the data.
Any configuration you set on the views or the ViewHolder instance in onBindViewHolder cannot be relied on to stay the same if the view scrolls off the screen, because the original ViewHolder might be recycled to be used for some other data, and when it scrolls back on screen, you might be looking at some other view that has been recycled from other data that just scrolled off the screen.
So if your views have configuration that you want to "stick", you have to back it up when you change it, and restore it in onBindViewHolder. The way you accomplish this will depend on how you are managing the data that you pass to the adapter.
If you can modify your User class, you can add a Boolean to it that stores whether it should show the password. Then in your onBindViewHolder, you restore the state based on this Boolean. And you also update this Boolean when the state changes.
I also updated the way the click listener works to simplify it for toggling. I removed the pos = position line, because almost certainly that is not something you should be doing.
public void onBindViewHolder(#NonNull final viewHolder holder, int position) {
final User user = users.get(position)
holder.layoutUName.setText(user.getUserName());
holder.layoutPasswd.setText(user.isShowPassword() ? user.getPasswd() : "********");
holder.btnViewChanger.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
user.setShowPassword(!user.isShowPassword());
holder.layoutPasswd.setText(user.isShowPassword() ? user.getPasswd() : "********");
holder.btnViewChanger.setText(user.isShowPassword() ? "hide" : "show");
}
});
// ...
}
If you cannot modify the User class, this is more complicated. Then the adapter should have its own ArrayList<Boolean> to store the state by position index, but you need to keep this list at least as long as the data that is bound, and reset everything to false if the whole list of data is refreshed.
This question already has answers here:
How to create RecyclerView with multiple view types
(23 answers)
Closed 3 years ago.
I want to display in same recycle-view which is some post have image and some post does not have images.
I can retrieve all the post with image and non-image,
but i want to change the size of the post when the user only post text(no image).
I expect the output like twitter feed..some post with image and without image have their own size.
Simple way to achieve this scenario is, All you have to do is create a view with both image and text, in recycler adapter check if image data is available make visibility of image visible else Image visibility gone.
Second Approach for this to make multiple view for RecyclerView.
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Log.d(TAG, "onBindViewHolder called");
ContentItem item = mContentItems.get(position);
if(item.getName()!=null){
holder.textName.setVisibility(View.Visible);
holder.textName.setText(item.getName());
}else{
holder.textName.setVisibility(View.GONE);
}
if(item.getPreviewImageDefault()!=null){
holder.imageIcon.setVisibility(View.Visible)
Picasso.with(mContext).load("file://" + item.getPreviewImageDefault()).into(holder.imageIcon);
}else{
holder.imageIcon.setVisibility(View.GONE)
}
}
Another possible solution is create 2 xml layouts and use ViewType in your RecyclerView.
look this How to create RecyclerView with multiple view type?
If you want to hide the image when it is ic_launcher you could do that (suppposing that data.getImage() returns the id of the drawable as integer):
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
if(mItems!=null){
AdapterData data = mItems.get(i);
viewHolder.text.setText(data.getText());
viewHolder.image.setImageResource(data.getImage());
if(TextUtils.isEmpty(data.getText())){
viewHolder.text.setVisibility(View.GONE);
}else{
viewHolder.text.setVisibility(View.VISIBLE);
}
if(data.getImage()==R.drawable.ic_launcher){
viewHolder.image.setVisibility(View.GONE);
}else{
viewHolder.image.setVisibility(View.VISIBLE);
}
}
}
One possible solution, like some people have already said, is to hide/show the ImageView.
You could do that in the ViewHolder that you use for your RecyclerView.
class OptionalImageViewHolder extends RecyclerView.ViewHolder {
private ImageView image;
private TextView text;
// any other views you have
public OptionalImageViewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.yourImageViewIdHere);
text = itemView.findViewById(R.id.yourTextViewIdHere);
// same for any other views you have
}
public void bindView(Tweet tweet) {
// This is where the magic happens
// Note: I make the assumption that you have a class called "Tweet"
// that has a field for "text", a field for "image" (that can be
// null if there's no image), and any other necessary fields.
text.setText(tweet.getTweetText());
if (tweet.hasImage() /* function that returns whether or not there is an image */) {
image.setVisibility(View.VISIBLE);
image.setImageBitmap(tweet.getImage()); // or however you are setting the image
} else {
// else just make the image invisible
image.setVisibility(View.GONE);
}
}
}
Hopefully this gives you an idea.
RecyclerView supports different viewTypes (layouts) which is the proper way in such scenario. E.g.,
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun getViewTypes(position:Int) =
if (mydata[position].hasImage) return R.layout.mylayout_with_image
else R.layout.mylayout_no_image;
override fun onCreateViewHolder(viewType:Int, parent:ViewGroup) : MyViewHolder =
// here viewType = layout id
MyViewHolder(layoutInflater.inflate(viewType, parent))
override fun onBindViewHolder(viewHolder:MyViewHolder, position:Int) {
// guaranteed viewHolder.itemView is the view you want for that position
}
}
Ok so it is a bit complicated, I have a custom RecyclerView Adapter and in the OnBindViewHolder method I would like to remove the current item from the recyclerview depending on some different variables but when I remove the item from the ArrayList and call notifyDataSetChanged(); I get :
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling.
How can I remove the item from the RecyclerView in the onBindViewHolder ? , I do not want to do it before setting the adapter because each item in the recyclerview has a sublist and I want to remove the item if the sublist is empty. Thanks for any ideas and sorry for bad english.
Please follow the method used below as shown in this answer.
private List<DetectedIssue> issues = new ArrayList<DetectedIssue>();
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
issues.remove(position);
notifyItemRemoved(position);
//this line below gives you the animation and also updates the
//list items after the deleted item
notifyItemRangeChanged(position, getItemCount());
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
#Override
public int getItemCount() {
return issues.size();
}
PS: As can be seen from the documentation it is bad practice to use notifyDataSetChanged() if only an item is added, moved or removed as unnecessary processes are run.
This event does not specify what about the data set has changed, forcing any observers to assume that all existing items and structure may no longer be valid. LayoutManagers will be forced to fully rebind and relayout all visible views.
If you want the underlying arraylist in some activity be changed then you will have to use an interface to communicate with the activity to change the arraylist.
I know its a bit late (4 years, heh) but I'm in the same solution. As the error states it is actually impossible to call methods such as adapter.notifyItemRemoved(position); or notifyItemRangeChanged(position, getItemCount()); and even the more intensive notifyDataSetChanged(); from within the onBindViewHolder method unless it is within an onClickListener whereby it will be called after the layout is built and not during the build.
A simple workaround for me was to call datalist.remove(position); so if the layout is rebuilt the view is omitted, then simply call holder.itemView.setVisibility(View.GONE);. It's not as clean, but it gets the job done for views you wish to hide.
In my onBindViewHolder of my RecyclerView.Adapter<SearchAdapter.ViewHolder> when user clicks on cardview a button becomes visible. But when I'm scrolling recyclerview some other items buttons are shown as visible too. Why is this happening?
this is my code:
#Override
public void onBindViewHolder(final ViewHolder viewHolder, final int position) {
viewHolder.card.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (viewHolder.content_layout.getVisibility() == View.VISIBLE) {
viewHolder.content_layout.setVisibility(View.GONE);
viewHolder.address.setMaxLines(2);
viewHolder.attribute.setMaxLines(2);
} else {
viewHolder.content_layout.setVisibility(View.VISIBLE);
viewHolder.address.setMaxLines(8);
viewHolder.attribute.setMaxLines(8);
}
}
});
...
}
Once you start scrolling down the list your views get recycled. This means a previously inflated ViewHolder (some that gets created in onCreateViewHolder) is reused.
So what you have to do is to remember the clicked positions (e.g. via a SparseBooleanArray) and check in onBindViewHolder whether the view should be visible (previously clicked) or not.
You can find a basic usage example of the SparseBooleanArray in this StackOverflow post
The 'other' visible items buttons are the ones using the same viewholder that was modified in the callback. So because viewholders (and views) are recycled :
They should only store information that can be retrieved each time the viewholder is bound to a position.
Anything that may be changed in the views state should be refreshed in onBindViewHolder()
In your case you should store the 'is selected' somewhere else and reset the visibility and maxlines in onBindViewHolder() (not only in the callback)
Good idea is to make a class object with all data you need for one item in recycler view, also add there one boolean isItemWasClicked and inside onBindViewHolder() check this boolean and make buttons visible or not.
For example:
public class OneItemOfList{
int priceToDisplay;
String name;
String date;
boolean wasClicked;
}
public class YourAdapter extends RecyclerView.Adapter<OneItemOfList.ViewHolder> {
ArrayList<OneItemOfList> items;
...
#Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
viewHolder.view.setText(items.get(position).name);
if (items.get(position).wasClicked)
viewHolder.button.setVisible(View.VISIBLE);
else
viewHolder.button.setVisible(View.GONE);
viewHolder.view2.setOnClickListener(...
OnClick(...){
items.get(position).wasClicked = !items.get(position).wasClicked;
});
}
...
}
create an array for example Boolean array, and when each position clicked, set true in same position of array. and in onBindViewHolder check if that array[position] is true set that item visible if.
I am trying to implement a drag and drop in a ListView in android(Ice Cream Sandwich). So when the dragged object reaches the edge of the ListView, I am scrolling the ListView in the relevant direction. The problem is that when we scroll, sometimes the adapter creates new Views as necessary and these 'new' Views did not receive the ACTION_DRAG_STARTED event earlier and hence do not receive the DragEvent updates. Is there any way I can send the events to these views as well?
An easiest way to implement drag and drop in listview is you use this great library.
https://github.com/commonsguy/cwac-touchlist
it's worth trying.
Looking at the source for View, I see:
static final int DRAG_CAN_ACCEPT = 0x00000001;
int mPrivateFlags2;
boolean canAcceptDrag() {
return (mPrivateFlags2 & DRAG_CAN_ACCEPT) != 0;
}
mPrivateFlags2 is package-private and not exposed by the SDK. However, you should be able to change it in a subclass by doing:
try {
Field mPrivateFlags2 = this.getClass().getField("mPrivateFlags2");
int currentValue = mPrivateFlags2.getInt(this);
mPrivateFlags2.setInt(this, currentValue | 0x00000001);
} catch (Exception e) {
}
I have the same problem. I did not solved this recycling problem, but I found a possible workaround still using the Drag & Drop framework. The idea is to change of perspective: instead of using a OnDragListener on each View in the list, it can be used on the ListView directly.
Then the idea is to find on top of which item the finger is while doing the Drag & Drop, and to write the related display code in the ListAdapter of the ListView. The trick is then to find on top of which item view we are, and where the drop is done.
In order to do that, I set as an id to each view created by the adapter its ListView position - with View.setId(), so I can find it later using a combination of ListView.pointToPosition() and ListView.findViewById().
As a drag listener example (which is, I remind you, applied on the ListView), it can be something like that:
// Initalize your ListView
private ListView _myListView = new ListView(getContext());
// Start drag when long click on a ListView item
_myListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(null, shadowBuilder, _myListView.getItemAtPosition(position), 0);
return true;
}
});
// Set the adapter and drag listener
_myListView.setOnDragListener(new MyListViewDragListener());
_myListView.setAdapter(new MyViewAdapter(getActivity()));
// Classes used above
private class MyViewAdapter extends ArrayAdapter<Object> {
public MyViewAdapter (Context context, List<TimedElement> objects) {
super(context, 0, objects);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View myView = convertView;
if (myView == null) {
// Instanciate your view
}
// Associates view and position in ListAdapter, needed for drag and drop
myView.setId(position);
return myView;
}
}
private class MyListViewDragListener implements View.OnDragListener {
#Override
public boolean onDrag(View v, DragEvent event) {
final int action = event.getAction();
switch(action) {
case DragEvent.ACTION_DRAG_STARTED:
return true;
case DragEvent.ACTION_DRAG_DROP:
// We drag the item on top of the one which is at itemPosition
int itemPosition = _myListView.pointToPosition((int)event.getX(), (int)event.getY());
// We can even get the view at itemPosition thanks to get/setid
View itemView = _myListView.findViewById(itemPosition );
/* If you try the same thing in ACTION_DRAG_LOCATION, itemView
* is sometimes null; if you need this view, just return if null.
* As the same event is then fired later, only process the event
* when itemView is not null.
* It can be more problematic in ACTION_DRAG_DROP but for now
* I never had itemView null in this event. */
// Handle the drop as you like
return true;
}
}
}
Now if you need to have a visual feedback when doing a drag and drop, there are several strategies. You can for instance have 2 instance variables in your activity named:
private boolean ongoingDrag = false; // To know if we are in a drag&drop state
private int dragPosition = 0; // You put the itemPosition variable here
When doing the drag and drop in MyListViewDragListener you modify these variables, and you use their state in MyViewAdapter. Of course do not forget to update the UI (in the event thread of course, use a Handler) with something like _myListView.getAdapter()).notifyDataSetChanged() or maybe _myListView.invalidate() method.
The problem is because listView.getPositionForView(view) returns -1 if the view is not visible when it is called. So relying on that will fail when you scroll the list. So, instead of setting a view.setOnLongClickListener() you can set a listView.setOnItemLongClickListener() on the list item which calls startDrag() on the item. onItemLongClick() gives you the position which you can pass to in the myLocalState parameter of startDrag(). Then you recover that in onDrag() using event.getLocalState() and casting it to an Integer. Like this...
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
#Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
position -= listView.getHeaderViewsCount();
DragShadowBuilder dragShadow = new View.DragShadowBuilder(view);
view.startDrag(null, dragShadow, position, 0);
return true;
}
});
Then in your OnDragListener...
#Override
public boolean onDrag(View eventView, DragEvent event) {
Integer dragViewPos = ((Integer) event.getLocalState());
int eventViewPos = listView.getPositionForView(eventView) - listView.getHeaderViewsCount();
...
}