I have a RecyclerView that contains expandable items. clicking on an item expands it. The problem is it also expand some other cards, unexpectedly. I checked everything and I couldn't find why is this happening, but I did manage to find out that the clicked item always somehow has the same id as the other expanded item. The error occurs only when the list is big enough, so I think it has something to do with the RecyclerViews functionality. Also using notifyDataSetChanged() works, but it eliminates the animations, and I want the layout to be animated...
this question looks to discuss the same problem I'm facing... but yet I don't know how to solve it.
I couldn't understand why is this happening or how to fix this... below are some images and code to help you understand better, and maybe see if the problem is in the code...
this is the RecyclerView:
An expanded card item looks like this:
Here's my Adapters class:
public class ActiveGoalsAdapter extends RecyclerView.Adapter<ActiveGoalsAdapter.ActiveGoalsViewHolder> {
private Context context;
private Cursor cursor;
private ArrayList<Goal> activeGoals;
private static boolean[] openedFromParent = new boolean[]{false, true}, editing = new boolean[]{false};
public ActiveGoalsAdapter(Context context, ArrayList<Goal> activeGoals, Cursor cursor) {
this.context = context;
this.activeGoals = activeGoals;
this.cursor = cursor;
}
public class ActiveGoalsViewHolder extends RecyclerView.ViewHolder {
public LinearLayout shrunkContainer, subGoalsTitleContainer;
public RelativeLayout expandedContainer, subGoalsRecyclerViewContainer, btnDelete, btnCancel, btnSave;
public ConstraintLayout editPanel;
public CustomProgressBar shrunkProgressBar, expandedProgressBar;
public ImageButton btnExpandShrink, btnEdit, btnBackToParent;
public TextView title, description;
public RecyclerView subGoalsRecyclerView;
public ExtendedEditText nameET, descriptionET;
public ActiveGoalsViewHolder(#NonNull View itemView) {
super(itemView);
shrunkContainer = itemView.findViewById(R.id.shrunk_active_goal_container);
expandedContainer = itemView.findViewById(R.id.expanded_active_goal_container);
editPanel = itemView.findViewById(R.id.edit_panel);
btnExpandShrink = itemView.findViewById(R.id.active_goal_expand_shrink_btn);
btnEdit = itemView.findViewById(R.id.active_goal_edit_btn);
btnBackToParent = itemView.findViewById(R.id.active_goal_back_to_parent_btn);
shrunkProgressBar = itemView.findViewById(R.id.shrunk_active_goal_progress_bar);
shrunkProgressBar.enableDefaultGradient(true);
title = itemView.findViewById(R.id.expanded_active_goal_title);
expandedProgressBar = itemView.findViewById(R.id.expanded_active_goal_progress_bar);
expandedProgressBar.enableDefaultGradient(true);
description = itemView.findViewById(R.id.expanded_active_goal_description);
subGoalsTitleContainer = itemView.findViewById(R.id.expanded_active_goal_sub_goals_title_container);
subGoalsRecyclerViewContainer = itemView.findViewById(R.id.expanded_active_goal_sub_goals_container);
subGoalsRecyclerView = itemView.findViewById(R.id.expanded_active_goal_sub_goals_recyclerview);
nameET = itemView.findViewById(R.id.expanded_active_goal_edit_name_edit_text);
descriptionET = itemView.findViewById(R.id.expanded_active_goal_edit_description_edit_text);
btnDelete = itemView.findViewById(R.id.edit_delete_button);
btnCancel = itemView.findViewById(R.id.edit_cancel_button);
btnSave = itemView.findViewById(R.id.edit_save_button);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (expandedContainer.getVisibility() == View.VISIBLE) {
shrink();
} else {
expand();
}
}
});
}
private void expand(){
TransitionManager.beginDelayedTransition((ViewGroup) itemView.getRootView(), new AutoTransition());
expandedContainer.setVisibility(View.VISIBLE);
shrunkProgressBar.setVisibility(View.INVISIBLE);
}
private void shrink(){
TransitionManager.beginDelayedTransition((ViewGroup) itemView.getRootView(), new AutoTransition());
expandedContainer.setVisibility(View.GONE);
shrunkProgressBar.setVisibility(View.VISIBLE);
}
}
#NonNull
#Override
public ActiveGoalsViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.active_goal_card, parent, false);
return new ActiveGoalsViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull ActiveGoalsViewHolder holder, int position) {
if (activeGoals.get(position) == null) {
return;
}
GoalDBHelper db = new GoalDBHelper(context);
Goal currentGoal = activeGoals.get(position);
Cursor subGoalsCursor = db.getSubGoalsCursorOf(currentGoal);
ArrayList<Goal> subGoalsArrayList = db.getSubGoalsArrayListOf(currentGoal);
String name = currentGoal.getName(),
description = currentGoal.getDescription(),
parent = currentGoal.getParentGoal();
int timeCounted = currentGoal.getTimeCounted(),
timeEstimated = currentGoal.getTimeEstimated();
for (Goal subGoal : activeGoals) {
if (subGoal.getParentGoal().equals(name)) {
subGoalsArrayList.add(subGoal);
}
}
holder.shrunkProgressBar.setText(name);
holder.shrunkProgressBar.setProgress((timeCounted * 100 / timeEstimated));
holder.shrunkProgressBar.setRadius(300.0f);
holder.expandedProgressBar.setText("");
holder.expandedProgressBar.setProgress((timeCounted * 100 / timeEstimated));
holder.expandedProgressBar.setRadius(300.0f);
holder.title.setText(name);
holder.description.setText(description);
if (subGoalsArrayList.size() <= 0) {
holder.subGoalsTitleContainer.setVisibility(View.GONE);
holder.subGoalsRecyclerViewContainer.setVisibility(View.GONE);
} else {
holder.subGoalsTitleContainer.setVisibility(View.VISIBLE);
holder.subGoalsRecyclerViewContainer.setVisibility(View.VISIBLE);
initSubGoalsAdapter(holder.subGoalsRecyclerView, subGoalsArrayList, subGoalsCursor);
}
if (openedFromParent[0]) {
holder.btnBackToParent.setVisibility(View.VISIBLE);
} else {
holder.btnBackToParent.setVisibility(View.GONE);
}
}
public void initSubGoalsAdapter(RecyclerView subGoalsRecyclerView, ArrayList<Goal> subGoals, Cursor subGoalsCursor) {
GoalsAdapter adapter = new GoalsAdapter(context, subGoals, subGoalsCursor);
final CarouselLayoutManager layoutManager = new CarouselLayoutManager(CarouselLayoutManager.VERTICAL, false);
layoutManager.setPostLayoutListener((CarouselLayoutManager.PostLayoutListener) new CarouselZoomPostLayoutListener());
subGoalsRecyclerView.setLayoutManager(layoutManager);
subGoalsRecyclerView.setHasFixedSize(true);
subGoalsRecyclerView.setAdapter(adapter);
}
#Override
public int getItemCount() {
return activeGoals.size();
}
public void swapCursor(Cursor newCursor) {
if (cursor != null) {
cursor.close();
}
cursor = newCursor;
if (newCursor != null) {
notifyDataSetChanged();
}
}
}
Where is the problem? and how should I fix it?
Help would be highly appreciated
The problem is that RecyclerView reuses ViewHolders during scrolling. For example on position 10 it can uses ViewHolder from position 2 (let's imagine this item was expanded) and if you don't bind expanded / collapsed state for ViewHolder on position 10 it will have expanded state. So to solve the problem you have to track ViewHolder state and update ViewHolder every onBindViewHolder method calling.
Here is a good answer related to selection in RecyclerView and you will have almost the same logic for expanded / collapsed states.
https://stackoverflow.com/a/28838834/9169701
I'm not familiar with the utilities you're using for animation. But, you can do something like this to track and update the visibility of your views:
private ArrayList<MyData> dataList;
private ArrayList<boolean> itemStates; // Create a list to store the item states
public MyAdapter(ArrayList<MyData> myData){
dataList = myData;
itemStates = new ArrayList<>();
// Build the default state values for each position
for(MyData data: dataList){
itemStates.add(false);
}
}
#Override
public void onBindViewHolder(MyHolder holder, int position){
// Whatever you need to do on each item position ...
final boolean visible = itemStates.get(position);
// Set the visibility of whichever view you want
if(visible){
holder.myView.setVisibility(View.VISIBLE);
}else{
holder.myView.setVisibility(View.GONE);
}
// Change the visibility after clicked
holder.itemView.setOnClickListener(new View.OnClickListener(){
// Use the ViewHolder's getAdapterPosition()
// to retrieve a reliable position inside the click callback
int pos = holder.getAdapterPosition();
if(visible){
// Play the hide view animation for this position ...
}else{
// Play the show view animation for this position ...
}
// Set the new item state
itemStates.set(pos, !visible);
// Refresh the Adapter after a delay to give your animation time to play
// (I've used 500 milliseconds here)
new Handler().postDelayed(new Runnable(){
#Override
public void run(){
notifyDataSetChanged();
}
}, 500);
});
}
You can refer to my code for the solution, maybe it'll help.
final boolean isExpanded = position == currentPosition;
holder.childLayout.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
holder.itemView.setActivated(isExpanded);
Animation slideDown = AnimationUtils.loadAnimation(context, R.anim.slide_down_animation);
holder.childLayout.startAnimation(slideDown);
if (isExpanded)
currentPosition = position;
holder.parentLayout.setOnClickListener(v -> {
currentPosition = isExpanded ? -1 : position;
notifyItemChanged(currentPosition);
notifyItemChanged(position);
});
Hope this solves your problem.
Edit:
currentPosition is a variable which is assigned to -1 and it stores the current position of the item in the recyclerview.
position is the variable of the BindViewHolder
setActivated() is a method defined for view. You can check it here.
childLayout is the layout of the view that is shown after the expansion.
parentLayout is the layout on which you click to expand.
Related
I am designing a relatively complex UI, i have searched stackoverflow and haven't found similar design. There could be many approaches to this, but i would like to ask expert opionions on how to achieve this and i would like to share my approach and make sure i am doing it the right way. My approach is that i have created a recycleview with header and inside header recycleview i am using an expandable recycleview library developed by h6ah4i (taken from github). Please let me know if there's a better approach to this.
The following image preview is a live mockup of final result i would like to get. It's not the actual screen. My question is what is the best way to achieve this, should i use expandable recycleview or expandable listview in recycleview header. I appreciate any answer as approaches or libraries it doesn't have to be similar to my code. Any suggestions are welcomed. I hope this post will also help other people like me in search of similar solution.
RecycleView Adapter
public class RecycleAdapterPlantSearch extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private List<Plants> plantsList;
private Context context;
private OnItemClickListener onItemClickListener;
public RecycleAdapterPlantSearch(Context context, List<Plants> plantsList, OnItemClickListener onClickListener) {
this.context = context;
this.plantsList = plantsList;
onItemClickListener = onClickListener;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_ITEM) {
// Here Inflating your recyclerview item layout
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_plant_search_plant_item, parent, false);
return new ItemViewHolder(itemView, onItemClickListener);
} else if (viewType == TYPE_HEADER) {
// Here Inflating your header view
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_plant_search_header, parent, false);
return new HeaderViewHolder(itemView, onItemClickListener);
} else return null;
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
/*
position 0 is for header
*/
if (holder instanceof HeaderViewHolder) {
// setheadersdata_flag = true;
HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder;
// You have to set your header items values with the help of model class and you can modify as per your needs
// Setup expandable feature and RecyclerView
RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);
SimpleDemoExpandableItemAdapter.OnListItemClickMessageListener clickListener = message -> {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
};
List<BadgesVM> badgesVMList = null;
badgesVMList = new ArrayList() {{
add(new BadgesVM("447", "Bienenfreundlich", "bienenfreundlich", false));
add(new BadgesVM("320,322", "Vogelfreundlich", "vogelfreundlich", false));
add(new BadgesVM("321", "Insektenfreundlich", "insektenfreundlich", false));
add(new BadgesVM("445", "Ökologisch wertvoll", "oekologisch", false));
add(new BadgesVM("531", "Schmetterlings freundlich", "schmetterlings", false));
add(new BadgesVM("530", "Heimische Pflanze'", "heimische Pflanze'", false));
}};
// Create wrapped adapter: MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
RecyclerView.Adapter adapter;
adapter = new SimpleDemoExpandableItemAdapter(context, expMgr,badgesVMList, clickListener);
adapter = expMgr.createWrappedAdapter(adapter);
//adapter = new DemoHeaderFooterAdapter(adapter, null);
headerViewHolder.recyclerViewExpandable.setAdapter(adapter);
headerViewHolder.recyclerViewExpandable.setLayoutManager(new LinearLayoutManager(context));
// NOTE: need to disable change animations to ripple effect work properly
((SimpleItemAnimator) headerViewHolder.recyclerViewExpandable.getItemAnimator()).setSupportsChangeAnimations(false);
expMgr.attachRecyclerView(headerViewHolder.recyclerViewExpandable);
} else if (holder instanceof ItemViewHolder) {
final ItemViewHolder itemViewHolder = (ItemViewHolder) holder;
itemViewHolder.plantDescText.setText(plantsList.get(position - 1).getDescription());
RequestOptions options = new RequestOptions()
.centerCrop()
.placeholder(R.drawable.background_small);
String imageUrl = APP_URL.BASE_ROUTE_INTERN + plantsList.get(position - 1).getImages().get(0).getSrcAttr();
Glide.with(context).load(imageUrl).apply(options).into(itemViewHolder.plantImg);
}
}
#Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
}
return TYPE_ITEM;
}
#Override
public long getItemId(int position) {
return position;
}
// getItemCount increasing the position to 1. This will be the row of header
#Override
public int getItemCount() {
return plantsList.size() + 1;
}
public interface OnItemClickListener {
void OnItemClickListener(View view, int position);
void RecycleViewExtraDetails(ChipGroup chipGroup);
void nestedRecycleViewsSpecialOdd(RecyclerView nestedRecycleView);
}
private class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private TextView searchNameTxt, searchFamilyTxt, plantGroupTxt, plantFamilySearchTxt, ecologyFilterTxt,
frostSearchTxt;
private ChipGroup chipGroup;
private Button filterSearchBtn;
private CardView ecologyCv;
private CardView detailSearchCv;
private RecyclerView recyclerViewExpandable;
public HeaderViewHolder(View headerView, OnItemClickListener onItemClickListener) {
super(headerView);
searchNameTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_search);
searchFamilyTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_search);
ecologyCv = headerView.findViewById(R.id.cardView_plant_search_header_ecology);
detailSearchCv = headerView.findViewById(R.id.cardView_plant_search_header_detail_search);
plantGroupTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_group);
plantFamilySearchTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_family);
ecologyFilterTxt = headerView.findViewById(R.id.textView_plant_search_header_ecology_filter);
frostSearchTxt = headerView.findViewById(R.id.textView_plant_search_header_frost_filter);
chipGroup = headerView.findViewById(R.id.chip_group_plant_search_header);
filterSearchBtn = headerView.findViewById(R.id.button_plant_search_header_filter_search);
recyclerViewExpandable = headerView.findViewById(R.id.expandable_list_view_plant_search);
searchNameTxt.setOnClickListener(this);
searchFamilyTxt.setOnClickListener(this);
ecologyCv.setOnClickListener(this);
detailSearchCv.setOnClickListener(this);
plantGroupTxt.setOnClickListener(this);
plantFamilySearchTxt.setOnClickListener(this);
ecologyFilterTxt.setOnClickListener(this);
filterSearchBtn.setOnClickListener(this);
frostSearchTxt.setOnClickListener(this);
}
#Override
public void onClick(View view) {
onItemClickListener.OnItemClickListener(view, getAdapterPosition());
onItemClickListener.RecycleViewExtraDetails(chipGroup);
}
}
public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private Button readMoreBtn;
private TextView plantDescText;
private ImageView plantImg;
public ItemViewHolder(View itemView, OnItemClickListener onItemClickListener) {
super(itemView);
plantDescText = itemView.findViewById(R.id.textView_plant_search_plants_item_description_text);
readMoreBtn = itemView.findViewById(R.id.button_plant_search_plant_item_read_more);
plantImg = itemView.findViewById(R.id.imageView_plant_search_plants_item_plant_image);
readMoreBtn.setOnClickListener(this);
}
#Override
public void onClick(View view) {
onItemClickListener.OnItemClickListener(view, getAdapterPosition() - 1);
}
}
}
nested header recycleview
public class SimpleDemoExpandableItemAdapter extends AbstractExpandableItemAdapter<SimpleDemoExpandableItemAdapter.MyGroupViewHolder,
SimpleDemoExpandableItemAdapter.MyChildViewHolder> implements View.OnClickListener {
RecyclerViewExpandableItemManager mExpandableItemManager;
List<MyBaseItem> mItems;
OnListItemClickMessageListener mOnItemClickListener;
List<BadgesVM> badgesVMList;
Context context;
static class MyBaseItem {
public final int id;
public final String text;
public MyBaseItem(int id, String text) {
this.id = id;
this.text = text;
}
}
static abstract class MyBaseViewHolder extends AbstractExpandableItemViewHolder {
TextView textView;
Slider frostSlider;
RecyclerView detailRecycleView;
public MyBaseViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(android.R.id.text1);
frostSlider = itemView.findViewById(R.id.slider_plant_search_expandable);
detailRecycleView = itemView.findViewById(R.id.recycle_view_plant_search_detail_search);
}
}
static class MyGroupViewHolder extends MyBaseViewHolder {
public MyGroupViewHolder(View itemView) {
super(itemView);
}
}
static class MyChildViewHolder extends MyBaseViewHolder {
public MyChildViewHolder(View itemView) {
super(itemView);
}
}
public SimpleDemoExpandableItemAdapter(Context context, RecyclerViewExpandableItemManager expMgr, List<BadgesVM> badgesVMList, OnListItemClickMessageListener clickListener) {
mExpandableItemManager = expMgr;
mOnItemClickListener = clickListener;
this.badgesVMList = badgesVMList;
this.context = context;
setHasStableIds(true); // this is required for expandable feature.
mItems = new ArrayList<>();
mItems.add(new MyBaseItem(0, "Filter nach ökologischen Kriterien"));
mItems.add(new MyBaseItem(1, "Frosthärte"));
mItems.add(new MyBaseItem(2, "Detailsuche"));
}
#Override
public int getGroupCount() {
return mItems.size();
}
#Override
public int getChildCount(int groupPosition) {
int childCount = 0;
int groupId = mItems.get(groupPosition).id;
if (groupId == 0) {
childCount = badgesVMList.size();
} else if (groupId == 1) {
childCount = 1; //contains only one item
} else if (groupId == 2) {
childCount = 1; //contains only one item
}
return childCount;
}
#Override
public long getGroupId(int groupPosition) {
// This method need to return unique value within all group items.
return mItems.get(groupPosition).id;
}
#Override
public long getChildId(int groupPosition, int childPosition) {
// This method need to return unique value within the group.
int groupId = mItems.get(groupPosition).id;
int childId = 0;
if (groupId == 0) {
badgesVMList.get(childPosition).getId();
} else if (groupId == 1) {
childId = 0;
} else if (groupId == 2) {
childId = 0;
}
return childId;
}
#Override
#NonNull
public MyGroupViewHolder onCreateGroupViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_group_item_for_expandable_minimal, parent, false);
MyGroupViewHolder vh = new MyGroupViewHolder(v);
vh.itemView.setOnClickListener(this);
return vh;
}
#Override
#NonNull
public MyChildViewHolder onCreateChildViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_child_item_for_expandable_minimal, parent, false);
MyChildViewHolder vh = new MyChildViewHolder(v);
vh.itemView.setOnClickListener(this);
return vh;
}
#Override
public void onBindGroupViewHolder(#NonNull MyGroupViewHolder holder, int groupPosition, int viewType) {
MyBaseItem group = mItems.get(groupPosition);
holder.textView.setText(group.text);
}
#Override
public void onBindChildViewHolder(#NonNull MyChildViewHolder holder, int groupPosition, int childPosition, int viewType) {
int groupId = mItems.get(groupPosition).id;
if (groupId == 0) {
BadgesVM badgesVM = badgesVMList.get(childPosition);
holder.textView.setVisibility(View.VISIBLE);
holder.frostSlider.setVisibility(View.GONE);
holder.detailRecycleView.setVisibility(View.GONE);
holder.textView.setText(badgesVM.getName());
} else if (groupId == 1) {
holder.textView.setVisibility(View.GONE);
holder.frostSlider.setVisibility(View.VISIBLE);
holder.detailRecycleView.setVisibility(View.GONE);
} else if (groupId == 2) {
holder.textView.setVisibility(View.GONE);
holder.frostSlider.setVisibility(View.GONE);
holder.detailRecycleView.setVisibility(View.VISIBLE);
// Setup expandable feature and RecyclerView
RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);
DetailSearchExpandableItemAdapter.OnListItemClickMessageListener clickListener = message -> {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
};
List<BadgesVM> badgesVMList = null;
badgesVMList = new ArrayList() {{
add(new BadgesVM("447", "Bienenfreundlich", "bienenfreundlich", false));
add(new BadgesVM("320,322", "Vogelfreundlich", "vogelfreundlich", false));
add(new BadgesVM("321", "Insektenfreundlich", "insektenfreundlich", false));
add(new BadgesVM("445", "Ökologisch wertvoll", "oekologisch", false));
add(new BadgesVM("531", "Schmetterlings freundlich", "schmetterlings", false));
add(new BadgesVM("530", "Heimische Pflanze'", "heimische Pflanze'", false));
}};
// Create wrapped adapter: MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
RecyclerView.Adapter adapter2;
adapter2 = new DetailSearchExpandableItemAdapter(context, expMgr, badgesVMList, clickListener);
adapter2 = expMgr.createWrappedAdapter(adapter2);
//adapter = new DemoHeaderFooterAdapter(adapter, null);
holder.detailRecycleView.setAdapter(adapter2);
holder.detailRecycleView.setLayoutManager(new LinearLayoutManager(context));
// NOTE: need to disable change animations to ripple effect work properly
((SimpleItemAnimator) holder.detailRecycleView.getItemAnimator()).setSupportsChangeAnimations(false);
expMgr.attachRecyclerView(holder.detailRecycleView);
}
}
#Override
public boolean onCheckCanExpandOrCollapseGroup(#NonNull MyGroupViewHolder holder, int groupPosition, int x, int y, boolean expand) {
// handles click event manually (to show Snackbar message)
return false;
}
#Override
public void onClick(View v) {
RecyclerView rv = RecyclerViewAdapterUtils.getParentRecyclerView(v);
RecyclerView.ViewHolder vh = rv.findContainingViewHolder(v);
int rootPosition = vh.getAdapterPosition();
if (rootPosition == RecyclerView.NO_POSITION) {
return;
}
// need to determine adapter local flat position like this:
RecyclerView.Adapter rootAdapter = rv.getAdapter();
int localFlatPosition = WrapperAdapterUtils.unwrapPosition(rootAdapter, this, rootPosition);
long expandablePosition = mExpandableItemManager.getExpandablePosition(localFlatPosition);
int groupPosition = RecyclerViewExpandableItemManager.getPackedPositionGroup(expandablePosition);
int childPosition = RecyclerViewExpandableItemManager.getPackedPositionChild(expandablePosition);
String message;
if (childPosition == RecyclerView.NO_POSITION) {
// Clicked item is a group!
// toggle expand/collapse
if (mExpandableItemManager.isGroupExpanded(groupPosition)) {
mExpandableItemManager.collapseGroup(groupPosition);
message = "COLLAPSE: Group " + groupPosition;
} else {
mExpandableItemManager.expandGroup(groupPosition);
message = "EXPAND: Group " + groupPosition;
}
} else {
// Clicked item is a child!
message = "CLICKED: Child " + groupPosition + "-" + childPosition;
}
mOnItemClickListener.onItemClicked(message);
}
public interface OnListItemClickMessageListener {
void onItemClicked(String message);
}
}
You were right to search a library that does most of the work for you, but I don't like the library you picked. It does not seem very flexible and concise. I suggest to take a look at Groupie, its API is pretty clean. Also check Reddit for some discussion on libraries.
If you want to write it yourself I think you can solve it without nested Adapter's. Just create an 'expandable group' item type. Then in getItemCount() you count all items and their nested items (when expanded). Take a look at the Groupie source code.
Some additional feedback on your code:
I would explicitly add the header to the list of items you give to your adapter. So instead of a List<Plants>, you rather provide a List<Item> and have a HeaderItem and PlantsItem. This way you have a clear separation between your domain models (Plants) and view models (the items) in your adapter.
Your onBindViewHolder() method does way too much. Let your ViewHolder subclasses take care of that. Create an abstract ViewHolder with an abstract bindTo(Item item) method. Then in your HeaderViewHolder subclass it and do the actual work (after an instanceof check).
Have a look at view binding, it can make your code more concise. (So does Kotlin.)
You can use ConcatAdapter to have multiple adapters with ViewHolders that hold different type of layouts even with the ones that contain RecyclerViews, i used in my last project and it works fine, you can check it out here, dashboard module uses multiple adapters to have different type of layouts.
You can also use the approach they used in Google iosched app to have one adapter with multiple layouts in better way where you move logic from adapter to ViewHolders and their wrapper class ViewBinders. ViewBinder is responsible of
calling onViewHolder, onCreateViewHolder and bind data type to a ViewBinder and ViewBinder to a layout. There is an article about how to use it in medium, i will post the link if i can find it. You can also check out this sample i created for animations but used ViewBinders in a simple form to create different type of layouts.
Below is the type of data and layout i wish to show in GridLayout and in which order
val data = mutableListOf<Any>().apply {
// Add Vector Drawables
add(HeaderModel("Animated Vector Drawable"))
add(AVDModel(R.drawable.avd_likes))
add(AVDModel(R.drawable.avd_settings))
add(HeaderModel("Seekable Vector Drawable"))
add(SeekableVDModel(R.drawable.avd_compass_rotation))
add(SeekableVDModel(R.drawable.avd_views))
add(SeekableVDModel(R.drawable.avd_hourglass))
add(HeaderModel("Clocks"))
add(AVDModel(R.drawable.avd_clock_alarm))
add(AVDModel(R.drawable.avd_clock_clock))
add(AVDModel(R.drawable.avd_clock_stopwatch))
}
These are correspond type of data i want to use in my RecyclerView, it's the types and binding to ViewHolder and layout in these classes.
private fun createViewBinders(): HashMap<ItemClazz, MappableItemBinder> {
val avdViewBinder = AVDViewBinder()
val seekableVDViewBinder = SeekableVDViewBinder()
val headViewBinder = HeaderViewBinder()
return HashMap<ItemClazz, MappableItemBinder>()
.apply {
put(
avdViewBinder.modelClazz,
avdViewBinder as MappableItemBinder
)
put(
seekableVDViewBinder.modelClazz,
seekableVDViewBinder as MappableItemBinder
)
put(
headViewBinder.modelClazz,
headViewBinder as MappableItemBinder
)
}
}
And set the data List to adapter and let adapter call corresponding layout that is bound to data
val dataList:List<Any> = getVectorDrawableItemList()
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
val adapter = MultipleViewBinderListAdapter(
createViewBinders(),
RecyclerView.Adapter.StateRestorationPolicy.ALLOW
).apply {
submitList(dataList)
}
For the expandable list, iosched app good way of doing it, there is video about how to animate expandable items in RecyclerVİew here. You can set state in ViewHolder and even use MotionLayout for animating from collapsed to expandable state. All can be done without any third party library and very clean way.
Inside my RecyclerView Adapter class I have 2 view types to display the results of my query:
#Query("SELECT l.log_id, l.junction_id ,l.date, l.workout_id, l.total_weight_lifted,
l.reps, l.set_number FROM log_entries_table
AS l LEFT JOIN exercise_workout_junction_table AS ej
ON ej.exercise_workout_id = l.junction_id WHERE ej.exercise_id = :exerciseID
ORDER BY substr(l.date, -4) DESC, substr(l.date, -7) DESC, (l.date) DESC")
LiveData<List<Log_Entries>> getAllExerciseHistoryLogs(int exerciseID);
The first view type is used to display all logEntries in which the date is unique:
The second view type is to display the rest of the logEntries which share the same date as the above:
My current code works fine, however every time I scroll down and the recyclerView updates, all the log-Entries with 'unique' dates (which should use the first viewType) get changed to display the second view type.
How can I stop my recyclerView view type from changing?
Before scroll -> After Scroll
RecyclerView Adapter
public class ExerciseHistoryAdapter2 extends RecyclerView.Adapter {
private OnItemClickListener listener;
private List<Log_Entries> allLogEntries = new ArrayList<>();
private List<String> uniqueDates = new ArrayList<>();
String logEntryDate;
public void setExercises(List<Log_Entries> allLogEntries) {
this.allLogEntries = allLogEntries;
notifyDataSetChanged();
}
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view;
if (viewType == 0) {
view = layoutInflater.inflate(R.layout.exercise_history_item, parent, false);
return new ViewHolderOne(view);
}
view = layoutInflater.inflate(R.layout.exercise_history_item_two, parent, false);
return new ViewHolderTwo(view);
}
#Override
public long getItemId(int position) {
return allLogEntries.get(position).getLog_id();
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
logEntryDate = allLogEntries.get(position).getDate();
if (uniqueDates.contains(logEntryDate)) {
// bindViewHolder2
ViewHolderTwo viewHolderTwo = (ViewHolderTwo) holder;
viewHolderTwo.textViewWeight.setText(String.valueOf(allLogEntries.get(position).getTotal_weight_lifted()));
viewHolderTwo.textViewReps.setText(String.valueOf(allLogEntries.get(position).getReps()));
} else {
uniqueDates.add(logEntryDate);
//bind viewholder1
ViewHolderOne viewHolderOne = (ViewHolderOne) holder;
viewHolderOne.textViewDate.setText(allLogEntries.get(position).getDate());
viewHolderOne.textViewWeight.setText(String.valueOf(allLogEntries.get(position).getTotal_weight_lifted()));
viewHolderOne.textViewReps.setText(String.valueOf(allLogEntries.get(position).getReps()));
}
}
#Override
public int getItemCount() {
return allLogEntries.size();
}
#Override
public int getItemViewType(int position) {
logEntryDate = allLogEntries.get(position).getDate();
if (uniqueDates.contains(logEntryDate)) {
return 1;
}
return 0;
}
class ViewHolderOne extends RecyclerView.ViewHolder {
private TextView textViewDate;
private TextView textViewWeight;
private TextView textViewReps;
public ViewHolderOne(#NonNull View itemView) {
super(itemView);
textViewDate = itemView.findViewById(R.id.textView_dateH);
textViewWeight = itemView.findViewById(R.id.textView_weightH);
textViewReps = itemView.findViewById(R.id.textView_repss);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION) {
listener.onItemClick(allLogEntries.get(position));
}
}
});
}
}
class ViewHolderTwo extends RecyclerView.ViewHolder {
private TextView textViewWeight;
private TextView textViewReps;
public ViewHolderTwo(#NonNull View itemView) {
super(itemView);
textViewWeight = itemView.findViewById(R.id.textView_weightH2);
textViewReps = itemView.findViewById(R.id.textView_repss2);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (listener != null && position != RecyclerView.NO_POSITION) {
listener.onItemClick(allLogEntries.get(position));
}
}
});
}
}
public interface OnItemClickListener {
void onItemClick(Log_Entries log_entries);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener;
}
}
Your getItemViewType and onBindViewHolder has some issues.
#Override
public int getItemViewType(int position) {
// type 0 = with date header
// type 1 = without date header
// if list is sorted chronologically
if (position == 0) {
return 0
}
String currentDate = allLogEntries.get(position).getDate();
String previousDate = allLogEntries.get(position - 1).getDate();
if (currentDate.equals(previousDate)) {
return 1
} else {
return 0
}
}
The first item will always have the header since the list is sorted chronologically. For the rest of the items, you need to check whether the date for the current item is the same as the previous item. Based on that condition you return the type.
You do not need to manage a list of unique dates. You have shared mutable states between the functions that are being called multiple times and are not synced. Just delete these and the references of them from onBindViewHolder
private List<String> uniqueDates = new ArrayList<>();
String logEntryDate;
I think to set holder.setIsRecyclable(false); would solve the issue, because the recycler view will then no longer recycle the items... But this is not a good solution for long lists.
EDIT:
I reviewd your code in onBindViewHolder()...
I think the problem comes with uniqueDates.add(logEntryDate); and that the onBindViewHolder method is called multiple times.
This is how the recycler view proceeds:
the first item in list will be unique because uniqueDates is empty. Therefore it will be added to the list.
the other items will be added correctly, as you see in your first screenshot
when you scroll down, the onBindViewHolder method will be executed for every item again
because the uniqueDates list already contains the first date, as it was added in step one, this item will now recognized as not-unique one
the wrong list will be displayed after scrolling as you see in your second screenshot
SOLUTION:
You will have to add a logic which identifies unique dates in another way, which is independet of the onBindViewholder method
OR
you would have to add code, that removes dates on a specific point, so that the list identifies the first item every time as unique and not just the first time.
I am set MaterialTextView inside RelativeLayout and set RelativeLayout size programmatically different size for every device.
Then i have using ViewTreeObserver to set setMaxLines and setEllipsize in MaterialTextView but i am facing some problem show the text in MaterialTextView using RecyclerView.Adapter.
I am using load more RecyclerView i am getting all data then after show text automatically in list and also notify data adapter then show text.
not showing text inside MaterialTextView in list
phone lock on/off then showing data in MaterialTextView
set recyclerview in fragment
recyclerView = view.findViewById(R.id.recyclerView_sub_category);
recyclerView.setHasFixedSize(true);
final LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
adapter code
public class SubCategoryAdapter extends RecyclerView.Adapter {
private final int VIEW_TYPE_LOADING = 0;
private final int VIEW_TYPE_ITEM = 1;
private final int VIEW_TYPE_QUOTES = 2;
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_ITEM) {
other data load
} else if (viewType == VIEW_TYPE_QUOTES) {
View v = LayoutInflater.from(activity).inflate(R.layout.quotes_adapter, parent, false);
return new Quotes(v);
} else if (viewType == VIEW_TYPE_LOADING) {
progressbar load
}
return null;
}
#Override
public void onBindViewHolder(#NonNull final RecyclerView.ViewHolder holder, #SuppressLint("RecyclerView") final int position) {
if (holder.getItemViewType() == VIEW_TYPE_ITEM) {
final ViewHolder viewHolder = (ViewHolder) holder;
// ------------- code -----
} else if (holder.getItemViewType() == VIEW_TYPE_QUOTES) {
final Quotes quotes = (Quotes) holder;
quotes.relativeLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, columnWidth / 2));
Typeface typeface = Typeface.createFromAsset(activity.getAssets(), "text_font/" + subCategoryLists.get(position).getQuote_font());
quotes.textView.setTypeface(typeface);
quotes.textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
quotes.textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int noOfLinesVisible = quotes.textView.getHeight() / quotes.textView.getLineHeight();
quotes.textView.setMaxLines(noOfLinesVisible);
quotes.textView.setEllipsize(TextUtils.TruncateAt.END);
quotes.textView.setText(subCategoryLists.get(position).getStatus_title());
}
});
}
}
#Override
public int getItemCount() {
return subCategoryLists.size() + 1;
}
#Override
public int getItemViewType(int position) {
if (position != subCategoryLists.size()) {
if (subCategoryLists.get(position).getStatus_type().equals("quote")) {
return VIEW_TYPE_QUOTES;
} else {
return VIEW_TYPE_ITEM;
}
} else {
return VIEW_TYPE_LOADING;
}
}
public class Quotes extends RecyclerView.ViewHolder {
private RelativeLayout relativeLayout;
private MaterialTextView textView;
public Quotes(#NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.textView_quotes_adapter);
relativeLayout = itemView.findViewById(R.id.rel_quotes_adapter);
}
}
}
I am not completely sure about the problem that you are having there. However, I think I found some problems in your code.
The first problem that I see in setting up your RecyclerView is setting the layout size fixed by the following.
recyclerView.setHasFixedSize(true);
Looks like you are changing the item layout size dynamically. Hence you need to remove the line above while setting up your RecyclerView.
The second problem that I see is, there is no textView_category in the ViewHolder for Quote. Hence the following should throw an error.
quotes.textView_category.setText(subCategoryLists.get(position).getCategory_name());
One problem I can see is that you are calling
quotes.textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
in the first line of your listener callback no matter what. In a RecyclerView the first few times this is called the layout might still be unmeasured but you're cancelling the listener callback after the first callback. So this line fires, and if the view is still unmeasured then the next few lines are going to fail and your code will get no more opportunities to fill the view. Try checking the measuredWidth or measuredHeight of your view for something greater than 0 before cancelling future listener callbacks.
I have using this. works perfectly
textView.setText(subCategoryLists.get(position).getStatus_title());
textView.post(new Runnable() {
#Override
public void run() {
ViewGroup.LayoutParams params = textView.getLayoutParams();
if (params == null) {
params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
final int widthSpec = View.MeasureSpec.makeMeasureSpec(textView.getWidth(), View.MeasureSpec.UNSPECIFIED);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(textView.getHeight(), View.MeasureSpec.UNSPECIFIED);
textView.measure(widthSpec, heightSpec);
textView.setMaxLines(heightSpec / textView.getLineHeight());
textView.setEllipsize(TextUtils.TruncateAt.END);
}
});
I have a problem using folding cell library from the RAMOTION
I have implemented everything but I am facing a problem with the list view
I have a list of planets and when user tap on let's say Jupiter the view gets unfolded and more information is visible to the user and when user tap on the same view which is seeing then the view gets folded
Problem
if the user scrolls down the list and then scroll back up and come back up to Jupiter the view remains unfolded and it is happening to all the view.
I appreciate if anyone helps me out
folding state
here
unfolding state
here
AdapterClass
#SuppressWarnings({"WeakerAccess", "unused"})
public class SolarSystemFoldingCellListAdapter extends ArrayAdapter<SolarSystemItemFoldingCell> {
private HashSet<Integer> unfoldedIndexes = new HashSet<>();
private View.OnClickListener defaultRequestBtnClickListener;
private int incomingPosition ;
public SolarSystemFoldingCellListAdapter(Context context, List<SolarSystemItemFoldingCell> objects) {
super(context, 0, objects);
}
#NonNull
#Override
public View getView(int position, View convertView, #NonNull ViewGroup parent) {
// get item for selected view
SolarSystemItemFoldingCell solarSystemItemFoldingCell = getItem(position);
// if cell is exists - reuse it, if not - create the new one from resource
FoldingCell cell = (FoldingCell) convertView;
final ViewHolder viewHolder;
if (cell == null) {
viewHolder = new ViewHolder();
LayoutInflater vi = LayoutInflater.from(getContext());
cell = (FoldingCell) vi.inflate(R.layout.solar_system_folding_cell, parent, false);
// binding view parts to view holder
viewHolder.foldingCell = cell.findViewById(R.id.folding_cell);
viewHolder.relativeLayoutFolded = cell.findViewById(R.id.relativeLayoutFolded);
viewHolder.linearLayoutFolded = cell.findViewById(R.id.linearLayoutFolded);
viewHolder.planetOrStarNameFolded = cell.findViewById(R.id.planetOrStarNameFolded);
viewHolder.mass = cell.findViewById(R.id.mass);
viewHolder.actualMass = cell.findViewById(R.id.actualMass);
viewHolder.distance = cell.findViewById(R.id.distance);
viewHolder.actualDistance = cell.findViewById(R.id.actualDistance);
viewHolder.diameter = cell.findViewById(R.id.diameter);
viewHolder.actualDiameter = cell.findViewById(R.id.actualDiameter);
viewHolder.speed = cell.findViewById(R.id.speed);
viewHolder.actualSpeed = cell.findViewById(R.id.actualSpeed);
viewHolder.moreInfoButton = cell.findViewById(R.id.button);
viewHolder.frameLayoutUnfolded = cell.findViewById(R.id.frameLayoutUnfolded);
viewHolder.planetOrStarNameUnfolded = cell.findViewById(R.id.planetOrStarNameUnfolded);
cell.setTag(viewHolder);
} else {
// for existing cell set valid valid state(without animation)
if (unfoldedIndexes.contains(position)) {
cell.unfold(true);
} else {
cell.fold(true);
}
viewHolder = (ViewHolder) cell.getTag();
}
if (null == solarSystemItemFoldingCell)
return cell;
// bind data from selected element to view through view holder
viewHolder.planetOrStarNameFolded.setText(solarSystemItemFoldingCell.getPlantOrStarNameFolded());
viewHolder.actualMass.setText(solarSystemItemFoldingCell.getActualMass());
viewHolder.actualDistance.setText(solarSystemItemFoldingCell.getActualDistance());
viewHolder.actualDiameter.setText(solarSystemItemFoldingCell.getActualDiameter());
viewHolder.actualSpeed.setText(solarSystemItemFoldingCell.getActualSpeed());
viewHolder.planetOrStarNameUnfolded.setText(String.valueOf(solarSystemItemFoldingCell.getPlanetOrStarNameUnfolded()));
//setting Fonts
viewHolder.planetOrStarNameFolded.setTypeface(App.getAppInstance().getArvoBold());
viewHolder.mass.setTypeface(App.getAppInstance().getArvoBold());
viewHolder.distance.setTypeface(App.getAppInstance().getArvoBold());
viewHolder.diameter.setTypeface(App.getAppInstance().getArvoBold());
viewHolder.speed.setTypeface(App.getAppInstance().getArvoBold());
viewHolder.actualMass.setTypeface(App.getAppInstance().getArvoRegular());
viewHolder.actualDistance.setTypeface(App.getAppInstance().getArvoRegular());
viewHolder.actualDiameter.setTypeface(App.getAppInstance().getArvoRegular());
viewHolder.actualSpeed.setTypeface(App.getAppInstance().getArvoRegular());
viewHolder.moreInfoButton.setTypeface(App.getAppInstance().getArvoRegular());
// set custom btn handler for list item from that item
if (solarSystemItemFoldingCell.getRequestBtnClickListener() != null) {
viewHolder.moreInfoButton.setOnClickListener(solarSystemItemFoldingCell.getRequestBtnClickListener());
} else {
// (optionally) add "default" handler if no handler found in item
viewHolder.moreInfoButton.setOnClickListener(defaultRequestBtnClickListener);
}
viewHolder.relativeLayoutFolded.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(getContext(), "Something gets clicked", Toast.LENGTH_SHORT).show();
viewHolder.foldingCell.fold(false);
registerFold(incomingPosition);
}
});
return cell;
}
// simple methods for register cell state changes
public void registerToggle(int position) {
if (unfoldedIndexes.contains(position)) {
registerFold(position);
incomingPosition = position;
}else
registerUnfold(position);
}
public void registerFold(int position) {
unfoldedIndexes.remove(position);
}
public void registerUnfold(int position) {
unfoldedIndexes.add(position);
}
public View.OnClickListener getDefaultRequestBtnClickListener() {
return defaultRequestBtnClickListener;
}
public void setDefaultRequestBtnClickListener(View.OnClickListener defaultRequestBtnClickListener) {
this.defaultRequestBtnClickListener = defaultRequestBtnClickListener;
}
// View lookup cache
private static class ViewHolder {
RelativeLayout relativeLayoutFolded ;
LinearLayout linearLayoutFolded ;
TextView planetOrStarNameFolded;
TextView mass;
TextView actualMass;
TextView distance;
TextView actualDistance;
TextView diameter;
TextView actualDiameter;
TextView speed;
TextView actualSpeed ;
Button moreInfoButton ;
FrameLayout frameLayoutUnfolded ;
TextView planetOrStarNameUnfolded;
FoldingCell foldingCell ;
}
}
SolarSystemClass
private void listViewIntegration (){
arrayList = addingDataIntoList();
solarSystemFoldingCellListAdapter = new SolarSystemFoldingCellListAdapter(SolarSystem.this , arrayList);
listView.setAdapter(solarSystemFoldingCellListAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int duration = 500; //miliseconds
int offset = 0; //fromListTop
listView.smoothScrollToPositionFromTop(position,offset,duration);
// toggle clicked cell state
((FoldingCell) view).toggle(false);
// register in adapter that state for selected cell is toggled
solarSystemFoldingCellListAdapter.registerToggle(position);
// listView.smoothScrollToPosition(position);
}
});
}
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);