I have a recyclerview showing items like this:
Selecting items calculates a total price from the number you see in the circle of each item, adding them up while you select more items. The upper right corner shows how mach money is left.
Now I want to grey out items if the money left is smaller of the price for an item, which can be seen in the first item "Enlightenment". I have this achieved by setting the alpha inside the OnBindViewHolder to 0.5F instead of 1.0f.
My problem is, that this works only for items currently not seen, aka outside of the screen. In the example of my screenshot I scrolled down first until Enlightenment was out of sight and then scrolled it back in to view achieving what you can see. But I want to visible items to also reflect if they are buyable or not the moment I select another item. For instance in the screenshot, everything which is currently shown but the two already selected items should be greyed out, as there is only 20 left which is not enough the items you see.
How can I achieve this for the items visible?
public class CivicsListAdapter extends ListAdapter<Card, CivicsViewHolder> {
private SelectionTracker<String> tracker;
private CivicViewModel mCivicViewModel;
private LinearLayoutManager mLayout;
public CivicsListAdapter(@NonNull DiffUtil.ItemCallback<Card> diffCallback, LinearLayoutManager layout) {
super(diffCallback);
this.mLayout = layout;
}
@NonNull
@Override
public CivicsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return CivicsViewHolder.create(parent);
}
@Override
public void onBindViewHolder(@NonNull CivicsViewHolder holder, int position) {
Card current = getItem(position);
String name = current.getName();
int price = current.getCurrentPrice();
Resources res = holder.itemView.getResources();
boolean isSelected = tracker.isSelected(name);
holder.bindName(name, getItemBackgroundColor(current, res));
holder.bindPrice(current.getCurrentPrice());
holder.bindBonus(current.getBonus());
holder.bindBonusCard(current.getBonusCard());
holder.bindIsActive(isSelected);
holder.itemView.setOnClickListener(v -> {
// clicked on single card in list
tracker.select(name);
Toast.makeText(v.getContext(), name
+ " clicked. \nYou can select more advances if you have the treasure.",Toast.LENGTH_SHORT).show();
});
if (!isSelected && price == 0) {
tracker.select(name);
} else {
// can we buy the card?
if (!isSelected && mCivicViewModel.getRemaining().getValue() < price) {
holder.mCardView.setBackgroundResource(R.color.dark_grey);
holder.mCardView.setAlpha(0.5F);
} else {
holder.mCardView.setBackgroundResource(R.drawable.item_background);
holder.mCardView.setAlpha(1F);
}
}
if (current.getBonus() > 0) {
holder.mFamilyBox.setVisibility(View.VISIBLE);
}
else {
holder.mFamilyBox.setVisibility(View.INVISIBLE);
}
}
static class CivicsDiff extends DiffUtil.ItemCallback<Card> {
@Override
public boolean areItemsTheSame(@NonNull Card oldItem, @NonNull Card newItem) {
Log.v("DIFF", "inside are ItemsTheSame");
return oldItem.getIsBuyable() == newItem.getIsBuyable();
}
@Override
public boolean areContentsTheSame(@NonNull Card oldItem, @NonNull Card newItem) {
Log.v("DIFF", "inside are ContentTheSame");
return oldItem.getIsBuyable() == oldItem.getIsBuyable();
}
}
public void setSelectionTracker(SelectionTracker<String> tracker) {
this.tracker = tracker;
}
public void setCivicViewModel(CivicViewModel model) {this.mCivicViewModel = model;}
public static Drawable getItemBackgroundColor(Card card, Resources res) {
int backgroundColor = 0;
if (card.getGroup2() == null) {
switch (card.getGroup1()) {
case ORANGE:
backgroundColor = R.color.crafts;
break;
case YELLOW:
backgroundColor = R.color.religion;
break;
case RED:
backgroundColor = R.color.civic;
break;
case GREEN:
backgroundColor = R.color.science;
break;
case BLUE:
backgroundColor = R.color.arts;
break;
default:
backgroundColor = R.color.purple_700;
break;
}
} else {
switch (card.getName()) {
case "Engineering":
backgroundColor = R.drawable.engineering_background;
break;
case "Mathematics":
backgroundColor = R.drawable.mathematics_background;
break;
case "Mysticism":
backgroundColor = R.drawable.mysticism_background;
break;
case "Written Record":
backgroundColor = R.drawable.written_record_background;
break;
case "Theocracy":
backgroundColor = R.drawable.theocracy_background;
break;
case "Literacy":
backgroundColor = R.drawable.literacy_background;
break;
case "Wonder of the World":
backgroundColor = R.drawable.wonders_of_the_world_background;
break;
case "Philosophy":
backgroundColor = R.drawable.philosophy_background;
break;
}
}
return ResourcesCompat.getDrawable(res,backgroundColor, null);
}
}
class CivicsViewHolder extends RecyclerView.ViewHolder {
private final TextView nameItemView;
private final TextView priceItemView;
private final TextView bonusCardItemView;
private final TextView bonusItemView;
public final View mCardView;
public final LinearLayout mFamilyBox;
private CivicsViewHolder(View itemView) {
super(itemView);
nameItemView = itemView.findViewById(R.id.name);
priceItemView = itemView.findViewById(R.id.price);
bonusCardItemView = itemView.findViewById(R.id.familyname);
bonusItemView = itemView.findViewById(R.id.familybonus);
mCardView = itemView.findViewById(R.id.card);
mFamilyBox = itemView.findViewById(R.id.familylayout);
}
public void bindName(String name, Drawable drawable) {
nameItemView.setText(name);
nameItemView.setBackground(drawable);
}
public void bindPrice(int price) {
priceItemView.setText(String.valueOf(price));
}
public void bindBonusCard(String cardName) {bonusCardItemView.setText(cardName);};
public void bindBonus(int bonus) {bonusItemView.setText(String.valueOf(bonus));}
public void bindIsActive(boolean isActive) {mCardView.setActivated(isActive);}
static CivicsViewHolder create(ViewGroup parent) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_row, parent, false);
return new CivicsViewHolder(view);
}
public ItemDetailsLookup.ItemDetails<String> getItemDetails() {
return new MyItemDetails(getBindingAdapterPosition(), nameItemView.getText().toString());
}
}
I think my problem comes from the way the data changes. I have a tracker defined and multiselect items in the recycler. This in turn changes the data itself which should result in items getting grayed out when they get to expensive for the remaining treasure. Now I tried doing the notifyDataChanged inside the trackers observer methods, but that lets to end endless recursion until an exception gets called from what I guess the recursion never ends, as every call of notify from inside the tracker observers onItemStateChanged seems to call the method itself again.
So I am a bit clueless WHERE I should call the notifyDataChanged method without causing this endless recursion.
No matter where I try to add the line mAdapter.notfiyDataChanged; to, it always results in endless log lines like this until the app crashes the moment I select the first item of the recyclerview.
1970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.519 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.519 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onItemStateChanged : Mysticism
2022-06-04 11:36:24.519 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.519 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.519 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onSelectionChanged
2022-06-04 11:36:24.520 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: treasure remaining : 150
2022-06-04 11:36:24.520 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onSelectionRefresh
2022-06-04 11:36:24.520 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.520 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.520 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onItemStateChanged : Mysticism
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onSelectionChanged
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: treasure remaining : 150
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onSelectionRefresh
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.521 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onItemStateChanged : Mysticism
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onSelectionChanged
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: treasure remaining : 150
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onSelectionRefresh
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider :Mysticism
2022-06-04 11:36:24.522 11970-11970/org.tesira.mturba.civichelper V/MODEL: inside getPosition von MyItemKeyProvider Position :1
2022-06-04 11:36:24.523 11970-11970/org.tesira.mturba.civichelper V/OBSERVER: inside onItemStateChanged : Mysticism
The Log.v I added shows a bit where the programs run around in circles. When I comment the notify line out, everything starts working again besides putting the shading right on the visible items. This last try is from putting the notify line into the observer of the remaining treasure (as its LiveData, right corner of screenshot above).
The notify works ONCE when I put it into the observer of the treasure and enter a different value for treasure (left upper corner of screenshot).
tracker = new SelectionTracker.Builder<>(
"my-selection-id",
mRecyclerView,
myItemKeyProvider,
new MyItemDetailsLookup(mRecyclerView),
StorageStrategy.createStringStorage())
.withSelectionPredicate(new MySelectionPredicate<>(this, mCivicViewModel))
.build();
mAdapter.setSelectionTracker(tracker);
mAdapter.setCivicViewModel(mCivicViewModel);
tracker.addObserver(new SelectionTracker.SelectionObserver<String>() {
@Override
public void onItemStateChanged(@NonNull String key, boolean selected) {
super.onItemStateChanged(key, selected);
// item selection changed, we need to redo total selected cost
Log.v("OBSERVER", "inside onItemStateChanged : " + key);
mCivicViewModel.calculateTotal(tracker.getSelection());
}
@Override
public void onSelectionRefresh() {
super.onSelectionRefresh();
Log.v("OBSERVER", "inside onSelectionRefresh");
}
@Override
public void onSelectionChanged() {
super.onSelectionChanged();
Log.v("OBSERVER", "inside onSelectionChanged");
}
@Override
public void onSelectionRestored() {
super.onSelectionRestored();
Log.v("OBSERVER", "inside onSelectionRestored");
}
});
this is from my ViewModel the method to redo the remaing treasure which is called from the tracker everytime a selection changes.
/**
* Calculates the sum of all currently selected advances during the buy process and
* updates remaining treasure.
* @param selection Currently selected cards from the View.
*/
public void calculateTotal(Selection<String> selection) {
int newTotal = 0;
for (String name : selection) {
Card adv = getAdvanceByName(name);
newTotal += adv.getCurrentPrice();
}
this.remaining.setValue(treasure.getValue() - newTotal);
}