I made a list that's loaded with Contact friends and the user can select them by tapping on them. If a person is selected, the listitem's backgorund changes colour, if deselected, the bg colouring goes away.
Problem is, when I call my method on an OnClickListener, it's fine.
When I however call it in a loop to colour already selected friends (e.g. when revisiting the list), it doesn't do the colouring.
The loop that goes through the elements to call colorize if needed:
for (int i = 0; i < adapter.getCount();i++){
ContactFriend cf = (ContactFriend) adapter.getItem(i);
View v = getViewByPosition(i,listView);
colorizeFriendBg(v, cf);
adapter.notifyDataSetChanged();
}
note I do the exact same in the listener and it works fine there.
And the colorizer:
private void colorizeFriendBg(View v, ContactFriend friend){
if(friend.isSelected()){
v.setBackgroundColor(0x993399ff);
}else{
v.setBackgroundColor(0x00000000);
}
v.invalidate();
}
This issue is quite strange and I have no idea what to do in order to make it right. The whole bunch is called from onActivityCreated, if that matters.
Edit:
I debugged it of course and the code runs and should change the colour, not running isn't the issue.
Edit again:
here's the listener implementation:
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ContactFriend fr = (ContactFriend) adapter.getItem(position);
addToSelected(fr);
//TODO: make it switch some BG colour when clicked. use getViewByPosition.
View v = getViewByPosition(position,listView);
colorizeFriendBg(v,fr);
adapter.notifyDataSetChanged();
}
});
what type of item View are you getting from the Adapter?
that View could / should implement colorize() and color itself;
for example: v.colorize(contact.isSelected()) to switch colors.
or with Android Data-Binding XML (where the viewModel is an instance of Contact):
<data class="com.acme.databinding.ContactViewHolderBinding">
<variable name="viewModel" type="com.acme.model.Contact"/>
</data>
...
android:backgroundColor="#{viewModel.isSelected ? R.color.MAGENTA : R.color.BLACK}"
class Contact just would require a getter and a setter for property isSelected.
one actually can also bind event handlers, which would be an alternate approach.
You need to call invalidate() on your view to make the color changes visible.
invalidate() forces a redraw with the new colors.
Related
I'm using a Recycler View to show all the images from the galley or the external storage of a device in a Grid Layout Manager. And I'm using a Radio Button to show if the image is selected or not.
PROBLEM
Whenever I select or deselect a Radio Button from the visible Views in the Recycler View some other Views which are outside the Visible Screen got selected or deselected.
It is like I'm pressing on the same View of the Recycler View, but the images are different.
PROBLEM
well that's because of the recycler view concept of reusing the views instead of creating new views every time you scroll.
you see if you have 100 items you want to show in a recycler view and only 20 of them could appear to the user, recycler view creates only 20 view holder to represent the 20 items, whenever the user scroll recycler view will still have 20 view holder only but will just switch the data stored in this view holders rather than create new view holders.
now to handle selection of your items there's two ways to do this.
the naive way
hold selection in a boolean array inside the recycle view adapter.
whenever the user scrolls, the adapter calls onBindViewHolder to update the visible viewholder with the proper data.
so when onBindViewHolder gets called just set the radio button selection according the boolean array using the position sent in the method call
at the end of your usage to the recycler view you can create a getter method in the adapter to get the selection array list of boolean and pass the data based on it
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_ClassType> data;
ArrayList<Boolean> dataSelected ;
public PhotosGalleryAdapter(ArrayList<Your_Data_ClassType> data) {
this.data = data;
dataSelected = new ArrayList<>(data.size()) ;
}
...
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
RadioButton radioButton = holder.getRadioButton()
radioButton.setChecked(dataSelected.get(position));
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
dataSelected.set(holder.getAbsoluteAdapterPosition() , isChecked) ;
}
});
...
}
}
the other way is to use a selection tracker and it should be the correct way to handle selections in a recycler view.
the problem with this way is it needs a lot of editing to the code and creating new classes to include as parameters in the selection tracker, but in the end you'll find it worth the time you spent on it.
in order to start with this way you need to do the following :
firstly, decide what should be a key (String-Long-Parcelable) so the tracker should use to differentiate between your data , the safest way is either String or Parcelable as I once tried Long and ended up with lots and lots of problems (in your case I will assume it's the photo's uri which will be of type string)
secondly, you need to create two new classes, one that extends ItemDetailsLookup, and the other extends ItemKeyProvider, and should use the key as their generic type (the type that is put between <> )
your two classes should look like this (that you might copy them straight forward)
the one that extends ItemKeyProvider :
public class GalleryItemKeyProvider extends ItemKeyProvider<String>{
PhotosGalleryAdapter adapter ;
/**
* Creates a new provider with the given scope.
*
* #param scope Scope can't be changed at runtime.
*/
public GalleryItemKeyProvider(int scope,PhotosGalleryAdapter m_adapter) {
super(scope);
this.adapter = m_adapter;
}
#Nullable
#Override
public String getKey(int position) {
return adapter.getKey(position);
}
#Override
public int getPosition(#NonNull String key) {
return adapter.getPosition(key);
}
}
the one that extends ItemDetailsLookup :
public class GalleryDetailsLookup extends ItemDetailsLookup<String> {
private final RecyclerView recView ;
public GalleryDetailsLookup(RecyclerView m_recView){
this.recView = m_recView;
}
#Nullable
#Override
public ItemDetails<String> getItemDetails(#NonNull MotionEvent e) {
View view = recView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recView.getChildViewHolder(view);
if (holder instanceof PhotosGalleryViewHolder) {
return ((PhotosGalleryViewHolder) holder).getItemDetails();
}
}
return null;
}
}
thirdly, you should include this new two methods in your adapter to be used by the above classes
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
...
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
...
}
forthly (if there's an english word called forthly), you should initialize the tracker with all the above classes that were created before and he will handle the rest, the tracker takes as parameters
a unique selection tracker id (if that will be the only selection tracker you will use then name it anything)
the ItemKeyProvider that we created
the DetailsLookup that we created
a String-Long-Parcelable Storage to store the keys that were selected in (in our case it will be a String Storage)
a Selection predicate, it's responsible to handle the way of selection you want to do, you want it to be able to (select only one item-multiple selection with no limits- based on a weird algorithm like even only or odd only), in my case I will use a default multiple selection one but if you want to alter it with another selection algorithm you should create a new class that extends SelectionPredicates and implement your way of selection, you could also just check the other default ones might be what you're looking for.
anyway, that's how the initialization should look (you should put this code wherever you initialize your recycler view at whether it's in fragment or activity method):
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
...
}
I didn't find a way to let me initialize the adapter with data and then create the tracker inorder to make the viewholders know about their selection or not, so in this case I firstly created the tracker and then made the adapter know about it's data using a setter and notifyDataSetChanged
what I mean by that is after creating the tracker instantly set the tracker and data to the adapter, so the initRecycleView should look like this
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
photosAdapter.setTracker(tracker);
photosAdapter.setData(data);
photosAdapter.notifyDataSetChanged();
...
}
Last but no least, you should handle how the view holders should know if they were selected or not, so you should let the adapter know about the tracker and its data by creating a setter method in it, that's how the adapter should look like in the end :
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_Class> data;
private SelectionTracker<String> tracker;
public PhotosGalleryAdapter() {
data = new ArrayList<>();
}
public ArrayList<Your_Data_Class> getData() {
return data;
}
public void setData(ArrayList<Your_Data_Class> m_data) {
this.data = m_data;
}
#Override
public ScheduleViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
...
}
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
boolean isSelected = tracker.isSelected(data.get(i).getUri());
RadioButton radioButton = holder.getRadioButton;
radioButton.setChecked(isSelected);
}
#Override
public int getItemCount() {
return data.size();
}
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
public void setTracker(SelectionTracker<String> m_tracker) {
this.tracker = m_tracker;
}
}
(as you may notice if you initialized the adapter with its data through the constructor, when he asks the tracker if there were an item selected or not, it will result in a NullPointerException as at the moment of initializing the adapter you still didn't initialize the tracker)
that way you could keep track of your selection the way google suggests in their documentation (which I honestly don't know why the made it very complicate like that).
if you want to know all the selected item in the end of your application/fragment use, you should call tracker.getSelection() which will return a Selection List for you to iterate on
There's a tiny problem/feature with the tracker that it won't start selecting the first item until you use a long press on it, that happens only in the first item you select, if you do want this feature (start selecting mode by long press) then leave it as it is
incase you don't want it you can make the tracker select a ghost key (any unique string key that means nothing to your data) at the beginning which should later enable the selection mode with a simple click on any photo
tracker.select("");
this also the way to make a default/old selection at the beginning, you could make a for loop and call tracker.select(Key) if you do want the tracker to start with few items being selected
N.B : incase you use the Ghost Key method you should watchout that the selection array that will get returned when you call tracker.getSelection() will also contain this Ghost Key.
at the end if you do have the curiosity of reading about selection tracker in the documentation follow this link
or maybe if you know how to read kotlin follow this two links
implementing-selection-in-recyclerview
a guide to recyclerview selection
I was stuck in the selection problem for days before I figure how to do all that so I hope you find your way through it.
Omar Shawky has covered the solutions.
With my answer I will stress on the reason why someone may face this sort of an issues with recycler views and how to avoid this common issue in the future (avoiding pitfalls).
Reason:
This issue happens because RecyclerView recycles views. So a RecyclerView item's view once inflated can get reused to show another off screen (to be scrolled to) item. This helps reduces re-inflation of views which otherwise can be taxing.
So if the radio button of an item's view is selected, and the same view gets reused to show some other item, then that new item can also have a selected radio button.
Solution:
The simplest solution for such issues is to have an if else logic in your ViewHolder to provide logic for both selected and de-selected cases. We also do not rely on information from radio button itself for initial setup (we do not use radioButton.isSelected() at the time of setup)
e.g code to write inside your ViewHolder class:
private boolean isRadioButtonChecked = false; // ViewHolder class level variable. Default value is unchecked
// Now while binding in your ViewHolder class:
// Setup Radio button (assuming there is just one radio button for a recyclerView item).
// Handle both selected and de-selected cases like below (code can be simplified but elaborating for understanding):
if (isRadioButtonChecked) {
radioButton.setChecked(true);
} else {
radioButton.setChecked(false);
}
radioButton.setOnCheckedChangeListener(
(radioButton, isChecked) -> isRadioButtonChecked = isChecked);
Do not do any of the following while setting up:
private boolean isRadioButtonChecked = false; // class variable
//while binding do not only handle select case. We should handle both cases.
if (isRadioButtonChecked) { // --> Pitfall
radioButton.setChecked(true);
}
radioButton.setOnCheckedChangeListener((radioButton, isChecked) -> isRadioButtonChecked = isChecked);
OR
// During initial setup do not use radio button itself to get information.
if (radioButton.isChecked()) { // --> Pitfall
radioButton.setChecked();
}
I've been working on an online shop type of application, and I've hit a bump: I've been tasked to add a favorites system, but I can't figure out how to enable pressing a button that's part of the RecyclerView item to add it to favorites.
(In this case, the heart, which is supposed to turn to a full heart when clicked)
Add a boolean value for favourite in your list . Initially , keep it false .
You need to have two drawables , one for selected state and another for unselected state .
In your onBindViewHolder , set the drawable on runtime on the basis of above condition .
if(list.isfav)
{
holder.ivHeart.setImageDrawable(ContextCompat.getDrawable(context,(R.drawable.selected));
}else{
holder.ivHeart.setImageDrawable(ContextCompat.getDrawable(context,(R.drawable.unselected));
}
Put onClick on this ivHeart eg:
holder.ivHeart.setOnClickListener(v -> {
if(list.isfav) {
list[adapterPosition].isfav = false;
}else{
list[adapterPosition].isfav = true;
}
notifyItemChanged(adapterPosition);
});
Dont forget to notify the item while changing item .
In your RecyclerView adapter's onBindViewHolder() method, add click listener to your view and change the drawable programmatically.
The code will be something like this
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
holder.your_like_imageview.setOnClickListener{
holder.your_like_imageview.setImageDrawable(ContextCompat.getDrawable(context,R.drawable.something_else));
};
}
I'm assuming you're using an ImageView for the heart. What you can do is set a click listener on that ImageView and process that click.
An ideal way to do this would be to use an interface to handle click events that you pass to the Adapter.
However you could do something like this in the onBindViewHolder method:
imageView.setOnClickListener {
// depending your logic change the tint for the icon or the drawable
onClick(data[position], addToWishlist)
notifyDataSetChanged()
}
The onClick method will receive the particular item and a flag to add or remove it from
the wishlist:
fun onClick(data: Data, addToWishlist: Boolean) {
// you can perform the addition/ deletion from the wishlist here
}
I am trying to make a ListView where you can have no more than 4 items clicked at once.
These items have to be adjacent to each other.
When I click my first item, I want to see if the previous OR the following item is clicked already.
Note: When I click an item, I change the background color. So if I want to tell if an item is clicked or not, I just want to check the background color.
public void OnItemClick(AdapterView<?> parent, View, view, int position, long id){
View currentItem = view;
currentItem.setBackgroundResource(R.drawable.li_gradient);
// How do I get the view in front of and behind currentItem
// to check their current background color? (Assuming they exist)
}
You should do this inside your adapter class, the model item need to have a background resource property.
Just cast the adapter, get the previous or next item, set the new background and notify the listView
public void OnItemClick(AdapterView<?> parent, View, view, int position, long id){
YourAdapterClass adapter = (YourAdapterClass) parent.getAdapter();
// TODO check if is a valid position
YourItem item = (YourItem) adapter.getItem(position-1);
item.setBackgroundResource(R.drawable.li_gradient);
adapter.notifyDataSetChanged();
}
Keep in mind that if the activity is recreated (because of configuration change for example) or if the list view is redrawn (for example when it is notified of a change) it will not automatically know about the changes made to the view in the onClick method and the background changes could be lost.
I would make a custom AdapterView with methods like itemSelected(int i), clearAllSelected(), getSelectedItems(), etc and make the adapterview the single point of responsibility for tracking and maintaining those changes and for rendering the background.
I'm now implementing an application that contains an Activity includes ListView, when the user selects any item in the list view, the background & text color of this view are changed, So, i placed the code of this changes in the onItemClick(AdapterView<?> arg0, View arg1,int position, long arg3) method ..but,this is not the problem.
The problem is, when i open the activity, i need to make an initial selection "before the user selects any thing"..so i made listView.setSelection(index);, but, unfortunately, this code doesn't invoke the onItemClick(AdapterView<?> arg0, View arg1,int position, long arg3)..So, the view doesn't changed "background and text color".
Any solution for that..?!!!
Thanks,
try onItemSelected for selection
selecting a iem in list view does not fires onItemClick
Create a method in yout Adapter to set the selection (or do it in the constructor):
public myAdapter (COntext context, int initialSelectedPos){
setSelectedPos(initialSelectedPos);
}
public void setSelectedPos(int pos){
mSelectedPos = pos;
}
Then check in your getView is given pos is the same than mSelectedPos.
#Override
public View getView(..., int pos){
/*convertView stuff*/
if (pos == mSelectedPos){
//Put the background as it is selected
}else{
//...
}
return view;
}
In your OnItemClick method from your OnItemClickListener call the setSelectedPos method of your Adapter.
You will solve the issue you commented and also when your selected view is no longer visible on screen and comes back to screen, will still be marked as selected ( I am pretty sure it was appearing with the original background).
after setting selection
listView.setSelection(index);
and then after call the
listView.getAdapter().notifyDatasetChanged();
i think this will solve your problem
I implemented the android listview with the ListActivity. Here I have the problem that when i click on the list item no action is performed when the flash color is also not coming that is the orange color. So do you have any idea about this kindly answer to my question.
#Override
protected void onListItemClick(ListView l, View v, int position, long id)
{
super.onListItemClick(l, v, position, id);
Toast.makeText(getApplicationContext(), "msg msg", Toast.LENGTH_SHORT)
.show();
}
I put this code also into the Main ListActivity.
The first thing what you have to note here is, whenever there are Clickable elements like Buttons or ImageButtons present in your ListView element, they take the control of click events. And so your ListView won't get the chance to accept the click event.
What you simply have to do is, set the focusable attribute to false for the Button or ImageButton you have in your ListView. But still they will work without any problem and also your ListView's onListItemClick will also work.
Try this,
<Button android:id="#+id/textsize_increaser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#+id/back_button"
android:focusable="false"
android:text=" A + "/>
Here I have added this android:focusable="false" and it works fine. try it.
Have you set the choice mode of ListView to SINGLE :
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
And if you have any clickable imageview or textview or button in the list item, then make them not focusable (in your Adapter class):
yourButton.setFocusable(false);
yourButton.setFocusableInTouchMode(false);
Are you using custom Adapter? and inflating layout with button or any view that eats away the list list view focus as child, then it won't work obviously. make sure to set
android:focusable="false"
to such view in xml file. hope this works for you.
Set this in your listactivity java file
listview1.setFocusable(false);
Actually there is a parameter meant for that to prevent children views from getting focus, just add the following in the parent layout:
android:descendantFocusability="blocksDescendants"
As the documentation explains:
The ViewGroup will block its descendants from receiving focus.
Eclipse suggested me to add textIsSelectable="true" to my TextViews in the layout xml which was used for list view.
Well, if you want to click the items in the list then you should not add those tags.
make sure that you are
Not using Scroll View with List View
Not using Scroll View in your row item layout for List View
If Scroll View is present at any of above place remove it
refer to this post for a solution:
Click is not working on the Listitem Listview android
View v = parent.getChildAt(position);
parent.requestChildFocus(v,view);
v.setBackground(res.getDrawable(R.drawable."Some drawable for clicked row"));
int count = parent.getChildCount();
for(int i=0; i<count; i++)
{
if(i!=position)
{
v = parent.getChildAt(i);
v.setBackground(res.getDrawable(R.drawable."some drawable for not clicked row));
}
}
listview.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View v, int pos,
long id) {
Toast.makeText(v.getContext(), exm.get(pos).getDefinition(),
Toast.LENGTH_SHORT).show();
}
});
listItemButton.setFocusable(false);
listItemButton.setFocusableInTouchMode(false);
Set the above in your adapter. It's not working in XML