Basically, I'm trying to programatically find out how many items are going to fit in a RecyclerView (and be visible to user, of course) in order to determine how many of them to fetch from cache.
I'm using a LinearLayoutManager.
Also, I'm aware of the LinearLayoutManager method findLastVisibleItemPosition, but obviously it's useless in this case since we're talking on before-initialization time, not after (so it returns -1).
Tried reading the docs or thinking on a creative but efficient idea, but I got nothing on my mind.
Any ideas?
This sounds actually pretty interesting, but only works if your height (vertical scroll) or width (horizontal scroll) is fixed, meaning no wrap_content.
No sample code and nothing tested here:
Create an Adapter with a setter for getCount that gets returned in case your data-source is null/empty
in getCount return at least 1 if your data is empty/null
make sure onBindViewHolder() can handle empty/non-existent data
add a OnChildAttachStateChangeListener to your RecyclerView, everytime the listener gets called, use the view to view.post(new Runnable() {...increase adapters getCount...adapter.notifyItemInserted()} (that runnable is necessary to avoid crash+burn)
OnChildAttachStateChangeListener gets called again >>> compare getCount and findLastVisibleItemPosition. If getCount > findLastVisibleItemPosition + 1 remove that listener. The number of fixed-size views fitting into ListView is findLastVisibleItemPosition + 1
Get your data and set it into you adapter, call notifyDataSetChanged
make sure getCount returns the data-source length from now on.
you could hide the listview behind a loadingscreen, or you can set the child views to invisible in onBindViewHolder
EDIT:
Create an Adapter which returns a ridiculous high count when no data is set and make sure it handles missing data correctly in onBindViewHolder
Extend LinearLayoutManager and Override onLayoutChildren() after the super call if getItemCount() > getChildCount() getChildCount() is the number of Views that would be visible in your RecyclerView
MainActivity.class
public class MainActivity extends AppCompatActivity {
private PreCountingAdapter mAdapter;
private RecyclerView mRecyclerView;
private PreCountLinearLayoutManager mPreCountLayoutManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
mPreCountLayoutManager = new PreCountLinearLayoutManager(this,
LinearLayoutManager.VERTICAL, false);
mPreCountLayoutManager.setListener(new PreCountLinearLayoutManager.OnPreCountedListener() {
#Override
public void onPreCounted(int count) {
mPreCountLayoutManager.setListener(null);
loadData(count);
}
});
mRecyclerView.setLayoutManager(mPreCountLayoutManager);
mAdapter = new PreCountingAdapter();
mRecyclerView.setAdapter(mAdapter);
}
private void loadData(final int visibleItemCount) {
// load data here, probably asynchronously,
// for simplicity just an String Array with size visibleItemCount
final List<String> data = new ArrayList<>();
for (int i = 0; i < visibleItemCount; i++) {
data.add(String.format("child number #%d", i));
}
mRecyclerView.post(new Runnable() {
#Override
public void run() {
mAdapter.swapData(data);
}
});
}
#Override
protected void onDestroy() {
mPreCountLayoutManager.setListener(null);
super.onDestroy();
}
}
PreCountLinearLayoutManager.class
public class PreCountLinearLayoutManager extends LinearLayoutManager {
private OnPreCountedListener mListener;
public interface OnPreCountedListener {
void onPreCounted(int count);
}
public PreCountLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
if (getItemCount() > getChildCount()) {
if (mListener != null) {
mListener.onPreCounted(getChildCount());
}
}
}
public void setListener(OnPreCountedListener listener) {
mListener = listener;
}
}
PreCountingAdapter.class
public class PreCountingAdapter extends RecyclerView.Adapter<PreCountingAdapter.ViewHolder> {
private List<String> mData;
public void swapData(List<String> data) {
mData = data;
notifyDataSetChanged();
}
public class ViewHolder extends RecyclerView.ViewHolder {
View mItemView;
TextView mTextView;
public ViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.text_view);
mItemView = itemView;
}
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
return new ViewHolder(inflater.inflate(R.layout.recycler_child, parent, false));
}
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (mData == null) {
// we are in precounting stage
holder.mItemView.setVisibility(View.INVISIBLE);
} else {
String item = mData.get(position);
holder.mItemView.setVisibility(View.VISIBLE);
holder.mTextView.setText(item);
}
}
#Override
public int getItemCount() {
return mData == null ? Integer.MAX_VALUE : mData.size();
}
}
Related
In the activity when user click on add client button I want to add new view to the screen which contains a spinner with list of client names retrieved from api and a button that will do some action on click.
So I thought I would use a recycleview and adapter for this but I think I'm wrong
in the activity I have the adapter
private ClientAdapter clientAdapter;
When I retrieve clients name from API I set the adapter as
clientRecyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
clientAdapter= new clientAdapter(clientList , this , this);
clientRecyclerView.setAdapter(podAdapter);
At this point I don't want the recycle view to render anything until user click on add new client button then I want to display one item that has spinner with client names and a button.
Then if he clicks again on add client button I want to show another spinner and button and so on.
However now I'm having 3 clients so recycleview render 3 view items which make sense.
But what the trick that I should do to achieve my goal?
Here's my adapter
public class ClintsAdapter extends RecyclerView.Adapter<ClintsAdapter.ViewHolder> {
private List<Clients> clientsList;
private EventListener listener;
public ClintsAdapter(List<Clients> clientsList, EventListener listener , Context context) {
this.clientsList = clientsList;
this.EventListener = listener;
}
#NonNull
#Override // To inflate view
public ClintsAdapter.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listitem_client, parent, false);
ViewHolder viewHolder = new ViewHolder(view, listener);
return viewHolder;
}
#Override
public void onBindViewHolder(#NonNull ClintsAdapter.ViewHolder holder, int position) {
ClintsAdapter = new ArrayAdapter<Client>(context, R.layout.spinner_text_view, clientsList);
ClintsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
holder.clientSpinner.setAdapter(ClintsAdapter);
holder.clientSpinner.setTitle("Choose client");
}
#Override
public int getItemCount() {
if (clientsList == null)
return 0;
return clientsList.size();
}
public interface PODListener {
void onClick(int position);
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private SearchableSpinner clientSpinner , collectMethodSpinner;
EventListener listener;
public ViewHolder(View itemView, final EventListener listener) {
super(itemView);
this.listener = podListener;
clientSpinner = itemView.findViewById(R.id.spinner_client);
btnComment = itemView.findViewById(R.id.btn_comment);
btnComment.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if ( listener != null ) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION){
listener.onClick(position);
}
}
}
});
}
#Override
public void onClick(View v) {
}
}
}
and here's my list item
From comments:
The problem is I'm passing list of clients to the adapter (size of 3) then the adapter render 3 items. I don't want this behavior I want to have 0 item if user click on add I will render one item and so on
You are using a single ArrayList<Client> for two different purposes:
The list of clients to choose from in the spinner
The number of spinners to display in the RecyclerView.
These are two separate things, so you need two separate lists.
You can do that with just adding integer value for your ClientsAdapter. Set its default value as 0 and create a method for changing it's value. When you want to add new item (new Spinner and Button) use that method and notify your adapter.
Add a new field called count for your ClientsAdapter.
private int count;
Inside constructor assign its value to 0. So on start its value will be 0 and RecyclerView will show nothing.
public ClintsAdapter(List<Clients> clientsList, EventListener listener , Context context){
this.clientsList = clientsList;
this.EventListener = listener;
count = 0;
}
Change getItemCount method's return value. According to your code getItemCount returns size of your List. That List is for Spinner and has no relation with this method. Instead of returning your List's size return count.
#Override
public int getItemCount() {
return count;
}
Create a method for changing count's value. count starts with 0 (assigned it 0 inside constructor) and when you click Button (add new Spinner and Button) this method will change its value.
public void addItem(int count) {
this.count = count;
}
Whenever you click Button simply call addItem method and pass new count value and notify your clientAdapter.
addClient.setOnClickListener(v -> {
int count = clientRecyclerView.getChildCount();
clientAdapter.addItem(count+1);
clientAdapter.notifyItemInserted(count);
});
NOTE: I don't get it why you're setting podAdapter for RecyclerView.
clientRecyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
clientAdapter= new clientAdapter(clientList , this , this);
clientRecyclerView.setAdapter(podAdapter);
You're creating clientAdapter reference for your ClientsAdapter but while setting adapter for RecyclerView, you're using different reference (podAdapter).
Full code for ClientsAdapter:
public class ClintsAdapter extends RecyclerView.Adapter<ClintsAdapter.ViewHolder> {
private List<Clients> clientsList;
private EventListener listener;
private int count;
public ClintsAdapter(List<Clients> clientsList, EventListener listener , Context context) {
this.clientsList = clientsList;
this.EventListener = listener;
count = 0;
}
#NonNull
#Override // To inflate view
public ClintsAdapter.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.listitem_client, parent, false);
ViewHolder viewHolder = new ViewHolder(view, listener);
return viewHolder;
}
#Override
public void onBindViewHolder(#NonNull ClintsAdapter.ViewHolder holder, int position) {
ClintsAdapter = new ArrayAdapter<Client>(context, R.layout.spinner_text_view, clientsList);
ClintsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
holder.clientSpinner.setAdapter(ClintsAdapter);
holder.clientSpinner.setTitle("Choose client");
}
public void addItem(int count) {
this.count = count;
}
#Override
public int getItemCount() {
return count;
}
public interface PODListener {
void onClick(int position);
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private SearchableSpinner clientSpinner , collectMethodSpinner;
EventListener listener;
public ViewHolder(View itemView, final EventListener listener) {
super(itemView);
this.listener = podListener;
clientSpinner = itemView.findViewById(R.id.spinner_client);
btnComment = itemView.findViewById(R.id.btn_comment);
btnComment.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if ( listener != null ) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION){
listener.onClick(position);
}
}
}
});
}
#Override
public void onClick(View v) {
}
}
}
I have a RecyclerView that I fill with data using an AsyncTask. When I clear the List with clear() and mAdapter.notifyDataSetChanged() and then try to fill it again with the AsyncTask, I get this exception:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter position
Replacing notifyItemInserted() with notifyDataSetChanged() inside my AsyncTask solves the problem, but I don't think that's a good solution, and I'd like to understand why the first method doesn't work.
My AsyncTask doInBackground() method:
#Override
protected Void doInBackground(Void... voids) {
for (int i = 0; i < 100; i++) {
mDataContainer.addItem(i);
publishProgress(i);
}
return null;
}
and my AsyncTask onProgressUpdate() method:
#Override
protected void onProgressUpdate(Integer... values) {
mAdapter.notifyItemInserted(values[0]);
super.onProgressUpdate(values);
}
I hope someone can help me with this. Thanks in advance.
Edit:
Here is the adapter:
private class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private List<Item> mItems;
private int selectedPos = RecyclerView.NO_POSITION;
private class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private Item mItem;
private TextView mNameTextView;
private TextView mMembersTextView;
MyViewHolder(View view) {
super(view);
itemView.setOnClickListener(this);
mNameTextView = itemView.findViewById(R.id.item_name);
mMembersTextView = itemView.findViewById(R.id.item_members);
}
#Override
public void onClick(View view) {
notifyItemChanged(selectedPos);
selectedPos = getLayoutPosition();
notifyItemChanged(selectedPos);
mOnItemSelectedListener.onItemSelected(mItem);
}
}
MyAdapter(List<Item> items) {
mItems = items;
}
#NonNull
#Override
public MyViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(getActivity())
.inflate(R.layout.list_item, parent, false);
return new MyViewHolder(view);
}
#Override
public void onBindViewHolder(MyViewHolder myViewHolder, int position) {
Item item = mItems.get(position);
myViewHolder.mItem = item;
myView.mNameTextView.setText(item.getName());
myView.mMembersTextView.setText(String.format(Locale.US,"%d/50", item.getMembers()));
if (!mIsInit) {
// select item that was selected before orientation change
if (selectedPos != RecyclerView.NO_POSITION) {
Item selectedItem = mItems.get(selectedPos);
mOnItemSelectedListener.onItemSelected(selectedItem);
// else select item 0 as default on landscape mode
} else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){
selectedPos = 0;
mOnItemSelectedListener.onItemSelected(MyViewHolder.mItem);
}
mIsInit = true;
}
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
myViewHolder.itemView.setSelected(selectedPos == position);
}
}
#Override
public int getItemCount() {
return mItems.size();
}
}
You should only be adding items to the adapter (and notifying) from the UI thread. Changing the adapter's backing data from the background and then notifying on the UI thread later via onProgressUpdate is quite likely to result in race conditions. In this case, mDataContainer.addItem(i); should be moved to onProgressUpdate before you notify the adapter.
It's hard to confirm if that's the only issue without seeing the definition of mDataContainer, but fixing the synchronization here would definitely be the first step toward fixing this.
I think it is better if you add all value in background and notify adapter after all data inserted"
#Override
protected Void doInBackground(Void... voids) {
for (int i = 0; i < 100; i++) {
mDataContainer.addItem(i);
}
return null;
}
#Override
protected void onPostExecute(Long result) {
mAdapter.notifyItemRangeInserted(0, 100);
}
I also suggest you should not use AsyncTask because it is deprecated on Android 11
I am trying to load images from my phone into my recyclerview.
So far, I have created a recyclerview.adapter, and a gridlayoutmanager, and attached them to my recyclerview. I can successfully retrieve full paths of the images and add them to the adapter using an async task.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gallery);
settingUp();
run();
}
public void settingUp(){
//setting up recycler view
recyclerView = findViewById(R.id.imageGallery);
recyclerView.setHasFixedSize(true);
//setting up recyclerview layout manager
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getApplicationContext(),2);
recyclerView.setLayoutManager(layoutManager);
//setting up recyclerview adapter
adapter = getInstance(getApplicationContext());
recyclerView.setAdapter(adapter);
// instance of load task
loadTask = new LoadTask();
}
private void run() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE);
}
else
{
loadTask.execute();
}
}
/**
* Loads the images into the cursor using a background thread
*/
private class LoadTask extends AsyncTask<Void,Void, Cursor>
{
// images are loaded into the cursor here, in the background.
#Override
protected Cursor doInBackground(Void... voids) {
loadImagesIntoCursor();
return imageCursor;
}
// this method runs after doInBackground finishes
#Override
protected void onPostExecute(Cursor cursor) {
transferImagesToAdapter(cursor);
}
}
//*********************************************************************************************************
/**
* Loads all the Images from external storage into the cursor.
*/
private void loadImagesIntoCursor()
{
imageCursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media.DATA}, null, null,
MediaStore.Images.Media.DATE_ADDED + " DESC");
}
private void transferImagesToAdapter(Cursor imageCursor)
{
String path;
if(imageCursor != null)
{
imageCursor.moveToFirst();
while (!imageCursor.isAfterLast()){
path = imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media.DATA));
adapter.add(path);
imageCursor.moveToNext();
}
imageCursor.close();
}
}
/** MY ADAPTER CLASS **/
class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ImageViewHolder>{
private ArrayList<String> images;
//private static ImageAdapter singleton = null;
Context context;
public ImageAdapter(Context context, ArrayList<String> paths)
{
images = paths;
this.context = context;
}
public ImageAdapter(Context context)
{
this.context = context;
}
//--------- OVERRIDE METHODS ---------------------------------------------
#NonNull
#Override
public ImageViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_layout,parent);
return new ImageViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull ImageViewHolder holder, int position) {
holder.image.setScaleType(ImageView.ScaleType.CENTER_CROP);
// load image into image holder view using picasso library
Picasso.with(context).load(images.get(position)).into(holder.image);
}
#Override
public int getItemCount() {
return images.size();
}
//------------------------------------------------------------------------
public void add(String path){
this.images.add(path);
}
//------------------------------------------------------------------------
public class ImageViewHolder extends RecyclerView.ViewHolder{
protected ImageView image;
public ImageViewHolder(View view){
super(view);
this.image = view.findViewById(R.id.img);
}
}
}
However, after adding the imagepaths I have no clue as to what trigger the onCreateViewHolder and onBindViewHolder methods in the adapter which will display my images in the recyclerview.
Any help please!
Two thing you need to do to update RecyclerView list item.
Update your data list inside your adapter
Notify dataset change.
private void transferImagesToAdapter(Cursor imageCursor)
{
String path;
if(imageCursor != null)
{
imageCursor.moveToFirst();
while (!imageCursor.isAfterLast()){
path = imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media.DATA));
adapter.add(path); // If it update data list in your adapter then fine.
adapter.notifyDataSetChanged(); // It will do all the callbacks and update your screen.
imageCursor.moveToNext();
}
imageCursor.close();
}
}
Recycler View and the Recycler View Adapter are two different things. Simply adding data to your Adapter will do nothing but fatten up the Adapter object.
The Recycler View has to be told that the Adapter's data state has changed and that the Recycler View should pull the updated data from the Adapter.
There are 2 ways to do it:
Calling the method notifyDataSetChanged method after putting in all the data changes in the Adapter's data source.
Using the DiffUtil
The difference between the above mentioned techniques is that notifyDataSetChanged totally resets the data to the current data source's data, all of it whereas DiffUtil changes only the data that has been updated and doesn't bother with the ones that haven't been changed.
So, if your data source has around a 1000 objects and you change only one if them, it's better to use DiffUtils because notifyDataSetChanged will recreate all the views even the ones that didn't change, which affects performance.
Also, the problem with #vikas jha's answer is that if there are many images, there will be substantial performance bottlenecks because for even a single image addition, the entire data source will be reloaded and the fact that it's inside a loop aggravates the problem. So move the adapter.notifyDataSetChanged(); call outside the loop. I'm assuming that you have a List of images as your data source, where the add method simply adds an image to the List.
It's best to have an overloaded add method for your adapter which accepts a list of images, append them to the end of the existing list of images in your adapter (assuming that you have previously loaded images) and then calling notifyDataSetChanged.
your activity class
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
run();
setContentView(R.layout.activity_gallery);
settingUp();
}
public void settingUp(){
//setting up recycler view
recyclerView = findViewById(R.id.imageGallery);
//setting up recyclerview layout manager
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getApplicationContext(),2);
recyclerView.setLayoutManager(layoutManager);
// instance of load task
loadTask = new LoadTask();
}
private void run() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE);
}
}
/**
* Loads the images into the cursor using a background thread
*/
private class LoadTask extends AsyncTask<Void,Void, List<String>
{
// images are loaded into the cursor here, in the background.
#Override
protected List<String> doInBackground(Void... voids) {
return getAllImages();
}
// this method runs after doInBackground finishes
#Override
protected void onPostExecute(List<String imagesList>) {
ImageAdapter imageAdapter = new ImageAdapter(getActivity, magesList);
recyclerview.setAdapter(imageAdapter);
}
}
//*********************************************************************************************************
/**
* Loads all the Images from external storage into the cursor.
*/
private void getAllImages()
{
List<String> imagesList = new ArrayList<>();
Cursor imageCursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media.DATA}, null, null,
MediaStore.Images.Media.DATE_ADDED + " DESC");
if(imageCursor != null)
{
imageCursor.moveToFirst();
while (!imageCursor.isAfterLast()){
String path = imageCursor.getString(imageCursor.getColumnIndex(MediaStore.Images.Media.DATA));
imagesList.add(path);
imageCursor.moveToNext();
}
imageCursor.close();
}
return imagesList;
}
Your adapter
/** MY ADAPTER CLASS **/
class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ImageViewHolder>{
private List<String> images;
Context context;
public ImageAdapter(Context context, List<String> paths)
{
this.images = paths;
this.context = context;
}
//--------- OVERRIDE METHODS ---------------------------------------------
#NonNull
#Override
public ImageViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_layout,parent);
return new ImageViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull ImageViewHolder holder, int position) {
holder.image.setScaleType(ImageView.ScaleType.CENTER_CROP);
// load image into image holder view using picasso library
Picasso.with(context).load(images.get(position)).into(holder.image);
}
#Override
public int getItemCount() {
return images.size();
}
public class ImageViewHolder extends RecyclerView.ViewHolder{
protected ImageView image;
public ImageViewHolder(View view){
super(view);
this.image = view.findViewById(R.id.img);
}
}
I'm setting up a RecyclerView that uses a ListAdapter to calculate animations on changes. The RecyclerView is receiving data through a ViewModel that fetches a list via Firebase. The items shown are of the Mensa kind. A Mensa item can change its visibility, its occupancy, or the distance displayed.
I want to implement two buttons that favorite/hide items, therefore changing their position in the list. Two buttons in every item allow the user to favorite or hide an item. This will move an item to the top / to the bottom of a list, in accordance with the sorting strategy, which places favorites first, defaults second, and hiddens last.
However, when I click on a button, the list will rearrange, but the item clicked won't rebind. Buttons retain their old state (and OnClickListeners), and only scrolling the list will call the onBind method. Is my problem with the DiffUtil.Callback? I really don't know what is wrong with my code.
I am already providing a new list in the submitList method of the adapter (this suggestion from another stackoverflow question enabled animations in my case), but the clicked item still won't redraw.
in MensaListActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mensa_list);
viewModel = ViewModelProviders.of(this).get(MensaListModel.class);
final RecyclerView recyclerView =findViewById(R.id.mensa_list_recyclerview);
final MensaListAdapter adapter = new MensaListAdapter(this, new MensaListAdapter.ItemButtonsListener() {
#Override
public void visibilityButtonClicked(Mensa mensa, VisibilityPreference newVisibility) {
viewModel.visibilityChanged(mensa, newVisibility);
}
});
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(false);
recyclerView.setLayoutManager(new GridLayoutManager(this, 1, RecyclerView.VERTICAL, false));
viewModel.getMensaData().observe(this, new Observer<LinkedList<Mensa>>() {
#Override
public void onChanged(LinkedList<Mensa> mensas) {
adapter.submitList(new LinkedList<>(mensas));
}
});
in MensaListModel.java
public LiveData<LinkedList<Mensa>> getMensaData() {
return mensaData;
}
// ...
public void visibilityChanged(Mensa changedItem, VisibilityPreference newVisibility) {
LinkedList<Mensa> newData = getMensaData().getValue();
int index = newData.indexOf(changedItem);
newData.remove(index);
newData.add(changedItem);
sortMensaData(newData);
// sortMensaData calls postValue method
MensaListAdapter.java
public class MensaListAdapter extends ListAdapter<Mensa, MensaListAdapter.MensaViewHolder> {
private final ItemButtonsListener listener;
private final Context context;
class MensaViewHolder extends RecyclerView.ViewHolder {
TextView nameLabel;
TextView addressLabel;
TextView restaurantTypeLabel;
TextView occupancyLabel;
TextView distanceLabel;
ImageButton favoriteButton;
ImageButton hideButton;
public MensaViewHolder(#NonNull View itemView) {
super(itemView);
// a bunch of assignments
}
public void bindData(final Mensa newMensa) {
nameLabel.setText(newMensa.getName());
addressLabel.setText(newMensa.getAddress());
restaurantTypeLabel.setText(newMensa.getType().toString());
String occText = "Occupancy: " + newMensa.getOccupancy().toInt();
occupancyLabel.setText(occText);
if (newMensa.getDistance() != -1) {
distanceLabel.setVisibility(View.VISIBLE);
distanceLabel.setText(Double.toString(newMensa.getDistance()));
} else {
distanceLabel.setVisibility(View.INVISIBLE);
}
switch(newMensa.getVisibility()){
case FAVORITE:
favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_active, null));
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.visibilityButtonClicked(newMensa, VisibilityPreference.DEFAULT);
}
}); break;
case DEFAULT:
favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_inactive, null));
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.visibilityButtonClicked(newMensa, VisibilityPreference.FAVORITE);
}
}); break;
case HIDDEN:
favoriteButton.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.favorite_inactive, null));
favoriteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
listener.visibilityButtonClicked(newMensa, VisibilityPreference.FAVORITE);
}
}); break;
// removed hidebutton assignments, as they're identical to the favoritebutton assignment
}
}
}
public MensaListAdapter(Context context, ItemButtonsListener listener) {
super(DIFF_CALLBACK);
this.context = context;
this.listener = listener;
}
private static final DiffUtil.ItemCallback<Mensa> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Mensa>() {
#Override
public boolean areItemsTheSame(#NonNull Mensa oldItem, #NonNull Mensa newItem) {
return oldItem.equals(newItem);
}
#Override
public boolean areContentsTheSame(#NonNull Mensa oldItem, #NonNull Mensa newItem) {
return oldItem.getDistance() == newItem.getDistance()
&& oldItem.getOccupancy().equals(newItem.getOccupancy())
&& oldItem.getVisibility().equals(newItem.getVisibility());
}
};
#Override
public int getItemViewType(int position) {
return R.layout.mensa_list_item;
}
#NonNull
#Override
public MensaViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
return new MensaViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull MensaViewHolder holder, int position) {
holder.bindData(getItem(position));
}
public interface ItemButtonsListener{
void visibilityButtonClicked(Mensa mensa, VisibilityPreference newVisibility);
}
}
Mensa.java
public class Mensa {
private String uID;
private String name;
private String address;
private Occupancy occupancy;
private RestaurantType type;
private VisibilityPreference visibility;
private double latitude;
private double longitude;
// distance is calculated lazily as soon as location coordinates are available, -1 means not calculated.
private double distance = -1;
public Mensa() {
}
// generated by android studio
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Mensa mensa = (Mensa) o;
return uID.equals(mensa.uID);
}
#Override
public int hashCode() {
return Objects.hash(uID);
}
// a bunch of getters and setters
}
The list before clicking the favorite button (heart). Relevant are the heart and eye buttons.
The list after favoriting the "Akademiestraße" item. It has changed positions, but the heart icon has not changed and the OnClickListeners are still the same.
The list after scrolling and returning to the top of the list. The heart is now filled, and OnClickListeners are changed.
It seems to me that your data is updating but the RecyclerView is only updating the order and not the item's view. Try calling your adapter's notifyDataSetChanged() after you update an item in your view.
Remember that your views are being recycled, meaning if you have for example a checkbox that was check in position 0. Scrolling will make that checkbox to be reuse in some item thus you may see other item that was checked as well even though you never check it. Always save the state of your view as it will be recycle. You can use your POJO/model to save the state with a boolean field.
Also when working with DiffUtil make sure it is a different instance of list not reusing the old one because it may not update your data.
You may also want to change this adapter.submitList(new LinkedList<>(mensas)); to just this adapter.submitList(mensas);
I have a RecyclerView that I am using. I have used RecyclerView before but never had this problem.
When I scroll up and down some of the items disappear, and some of the items that disappear appear again in the bottom.
Code:
ViewHolder:
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView txt;
public ViewHolder(View view) {
super(view);
txt = (TextView) view.findViewById(R.id.txt);
}
}
Adapter:
public class MyAdapter extends RecyclerView.Adapter<ViewHolder> {
private final Activity activity;
private final ArrayList<HashMap<String, String>> mItems;
public MyAdapter (Activity activity, ArrayList<HashMap<String, String>> mItems) {
this.activity = activity;
this.mItems= mItems;
}
#Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
return new ViewHolder(LayoutInflater.from(activity).inflate(R.layout.items, viewGroup, false));
}
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
HashMap<String, String> item = mItems.get(position);
String info = item.get("info ");
if (info!= null) {
viewHolder.txt.setText(info);
} else {
viewHolder.txt.setVisibility(View.GONE);
}
}
#Override
public int getItemCount() {
return (null != mItems? mItems.size() : 0);
}
}
onBindViewHolder reuses Views so let's say the first time onBindViewHolder() is called, info is null. This will cause that row to have visibility of View.GONE.
When onBindViewHolder is called again to bind a new row, the view for that row is still View.GONE - nothing is reset between rows being bound.
Therefore your if statement should reset the state completely:
if (info!= null) {
viewHolder.txt.setText(info);
viewHolder.txt.setVisibility(View.VISIBLE);
} else {
viewHolder.txt.setVisibility(View.GONE);
}
This will ensure that each row's visibility is set correctly.