Could someone please help before this drives me completely insane!
Imagine you have a list view. It has in it 9 items, but there is only space to display 6 without scrolling. If an item is selected the background colour will change to indicate this.
If you select any item from 2 to 8 inclusive all is well in the world.
If you select item 1 it also selects item 9 and vica versa. Also with this selection if you scroll up and down a random number of times, the selection will change. If you continue to scroll up and down, the selection changes back to 1 and 9. The value of the selected item is always the actual item you selected.
This is my code from my adapter :
public class AvailableJobAdapter extends ArrayAdapter<JobDto> {
private Context context;
private ArrayList<JobDto> items;
private LayoutInflater vi;
public AvailableJobAdapter(Context context, ArrayList<JobDto> items) {
super(context, 0, items);
this.context = context;
this.items = items;
vi = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
JobDto jh = getItem(position);
if (convertView == null) {
convertView = vi.inflate(R.layout.inflator_job_list, null);
holder = new ViewHolder();
holder.numberText = (TextView) convertView.findViewById(R.id.txtNumber);
holder.descriptionText = (TextView) convertView.findViewById(R.id.txtDescription);
holder.statusText = (TextView) convertView.findViewById(R.id.txtStatus);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.numberText.setText(jh.getJobNumber());
holder.descriptionText.setText(jh.getDescription());
holder.statusText.setText(jh.getStatus());
return convertView;
}
public static class ViewHolder {
public TextView numberText;
public TextView descriptionText;
public TextView statusText;
}
}
and this is the code from my click listener :
jobs.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Button btnOk = (Button) findViewById(R.id.btnOk);
view.setSelected(true);
int selected = position;
int pos = val.get(selected);
int firstItem = jobs.getFirstVisiblePosition();
int viewIndex = selected - firstItem;
if (pos == 0) {
jobs.getChildAt(viewIndex).setBackgroundColor(getResources().getColor(R.color.selected));
val.set(selected, 1);
selectedCount ++;
} else {
jobs.getChildAt(viewIndex).setBackgroundColor(getResources().getColor(R.color.unselected));
val.set(selected, 0);
selectedCount --;
}
if (selectedCount > 0 ){
btnOk.setEnabled(true);
} else {
btnOk.setEnabled(false);
}
}
});
I have spent hours researching this and trying various suggestions.
Any help will be greatly appreciated.
****EDIT****
After playing with some suggestion I tested it with a HUGE list. This is exactly what the behaviour is :-
If your screen has space for 10 items, if you select item 1 it also highlights 11, 21, 31, 41 etc.
Anything in between these values behaves correctly.
From this guide:
When your ListView is connected to an adapter, the adapter will instantiate rows until the ListView has been fully populated with enough items to fill the full height of the list. At that point, no additional row items are created in memory.
Instead, as the user scroll through the list, items that leave the screen are kept in memory for later use and then every new row that enters the screen reuses an older row kept around in memory. In this way, even for a list of 1000 items, only ~7 item view rows are ever instantiated or held in memory.
That's the root of the problem you are facing. You are changing the background color of the views in your click listener. But once a selected item is scrolled out of the screen, its view will be reused for the new item that is swiping in. As the reused view had its background color changed, the new item will consequently have that same color.
You need to take in account that views are recycled, so they might be "dirty" when you get them in getView(). I recommend you to take a look at the guide from where I got the quotes above, it's a nice and important read.
One possible way to fix that is to add a boolean field to your JobDto class and use it to track if an item is selected or not. Then in getView(), you could update the background color accordingly. You'll also probably need to add the item root view(convertView) to your ViewHolder in order to change its background color.
In setOnItemClickListener() just update setSelected() true of false for clicked position and notifydataset. In getView put a condition
if(jh.isSelelcted())
{
// background is selected color
}else{
// background is non selected color
}
note : handle else condition.
Related
After looking for some answers here, I find myself in a disturbing situation where my Listview is really getting on my nerve.
Here are the questions I looked for :
Maintain ListView Item State
How to save state of CheckBox while scrolling in ListView?
I'm using a custom adapter with a custom row as below.
My Listview is simple as it is displaying a custom row made of three elements :
1) an ImageView displaying contact picture cropped in a circle ;
2) a TextViewdisplaying the contact full name as plain text ;
3) and finally an ImageView that holds the purpouse of a CheckBox.
Please focus on the last element. The ImageView CheckBox-like will have its src changed upon click.
When the user click, the ImageView will switch between a check sign and an unchecked sign according to it's previous status. Possible status are : {checked | unchecked}
So far so good.
But as soon as I scroll the ListView, any aforementioned change will disappear as Android recycle unused view.
Here comes the so-called ViewHolder pattern. Unfortunately, this pattern is failling me on two issues :
First, when scrolling, my organized-in-an-alphabetical-order listview gets disorganized.
e.g. somehow, whitout any reason, the first displayed contact name gets displayed again later on the ListView as I scrolled. That can happen with any row ! So it would seem unused view are being wrongly re-used.
Second, and in accordance to the first issue, the checked status do seem to stay, but not always and if it does stay, it may very well stay on the wrong row ... and that can happen randomly, of course. Therefore ViewHoder is not a viable solution.
Before discouvering the ViewHolder pattern, I have been using a HashMap to store the item position upon click as followed :
ContactsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public final void onItemClick(final AdapterView<?> adapterView, final View view,
final int position, final long id) {
final ImageView check = (ImageView) view.findViewById(R.id.checkImage);
final TextView name = (TextView) view.findViewById(R.id.contactName);
final Boolean isChecked = Boolean.valueOf(checkedContactsList.isChecked(position));
if (isChecked != null && isChecked.booleanValue() == true) {
check.setImageDrawable(getActivity().getResources().getDrawable(R.drawable.unchecked_sign));
checkedContactsList.(position);
} else {
check.setImageDrawable(getActivity().getResources().getDrawable(R.drawable.checked_sign));
checkedContactsList.add(position, true);
}
}
});
I tried adding a different value instead of position.
I tried with ContactsListView.getPositionForView(view)
And I also tried with the View's ID, but still it doesn't work.
I wish I could use ContactsListView.getSelectedItemPosition() but it returns -1 as there is no selection event because I'm handling a touch/click event.
And this is how my Custom Adapter looks like :
public final View getView(final int position,
final View convertView, final ViewGroup parent) {
final LayoutInflater inflater = (LayoutInflater) this.context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View contactRowView = inflater.inflate(R.layout.contact_row, parent, false);
final ImageView contactPic = (ImageView) contactRowView.findViewById(R.id.contactPic);
final TextView contactName = (TextView) contactRowView.findViewById(R.id.contactName);
final ImageView checkImage = (ImageView) contactRowView.findViewById(R.id.checkImage);
// the list is the same as above and therefore contains the exact same entries
if (this.checkedContactsList.isChecked(position))
checkImage.setImageDrawable(this.context.getResources().getDrawable(R.drawable.checked_sign));
contactPic.setImageBitmap(cropePictureInCircle(this.contacts.get(position).getPicture()));
contactName.setText(this.contacts.get(position).getName());
return contactRowView;
}
Is there a good way to keep the checked row checked and the unchecked row unchecked in the given alphabetical order ?
Thanks !
For the list position change I know the solution but for the second problem I am still searching for a solution, anyway first make a viewHolder class;
public class ViewHolder{
//put all of your textviews and image views and
//all views here like this
TextView contactName;
ImageView checkImage;
ImageView contactImage;
}
Then edit your adapter:
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
final View contactRowView = convertView;
ViewHolder holder;
if (contactRowView == null) {
final LayoutInflater inflater = (LayoutInflater)
this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE
);
contactRowView =
inflater.inflate(R.layout.contact_row, parent,
false);
holder = new ViewHolder():
holder.contactPic = (ImageView)
contactRowView.findViewById(R.id.contactPic);
holder.contactName = (TextView)
contactRowView.findViewById(R.id.contactName);
holder.checkImage = (ImageView)
contactRowView.findViewById(R.id.checkImage);
contactRowView.setTag(holder);
} else {
holder = contactRowView.getTag();
// the list is the same as above and therefore contains the exact same entries
if (this.checkedContactsList.isChecked(position))
holder.checkImage.setImageDrawable(this.context.get.
Resources().getDrawable(R.drawable.checked_sign));
holder.contactPic.setImageBitmap(cropePictureInCircle(this.contacts.get(position).getPicture()));
holder.contactName.setText(this.contacts.get(position).getName());
return contactRowView;
}
}
Hope this helps and sorry because writing code from my phone is totally awkward.
I use even and odd rows to set backgrond to my listview rows. In my efficientAdapter I set the row background as follows:
public View getView(int position, View convertView, ViewGroup parent) {
vi = convertView;
if (convertView == null) {
vi = inflater.inflate(R.layout.ecran_multiple_row, null);
holder = new ViewHolder();
holder.txIndex = (TextView) vi.findViewById(R.id.txIndex);
holder.txSTitle = (TextView) vi.findViewById(R.id.txSTitle);
holder.btOnOFF = (ImageView) vi.findViewById(R.id.btOnOFF);
vi.setTag(holder);
} else
holder = (ViewHolder) vi.getTag();
/*
* CHANGE ROW COLOR 0 WHITE 1 GRAY
*/
if ( position % 2 == 0) //0 even 1 odd..
vi.setBackgroundResource(R.drawable.listview_selector_odd);
else
vi.setBackgroundResource(R.drawable.listview_selector_even);
/*
* ONE ITEM IN ARRAY
*/
if (data.toArray().length==1){
holder.btOnOFF.setBackgroundResource(R.drawable.air_radio_button_rouge);
}else {
holder.btOnOFF.setBackgroundResource(R.drawable.air_deezer_check);
}
return vi;
}
and in my MainActivity.Class. I select an item using on itemclicklistener() as shown below:
**lvRMultiple.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
imgview = (ImageView) view.findViewById(R.id.btOnOFF);
//And change its background here
imgview.setBackgroundResource(R.drawable.air_radio_button_rouge);
}
});**
When i clicked on an item btnOff image change successfully but when i scroll down it change to default background. Secondly when i click on one item after the other both becomes the new image but i want only the row clicked by the user to change to new image and the previous image are set to default.
All row view of a ListView created by the getView() method of BaseAdpter class. When ever we scroll the ListView all, new viable row create by getView() using recycle. So getView() called again and again when new row is viable on scroll.
There are two solution of your question:-\
You can save the status of ListView
// Save ListView state
Parcelable state = listView.onSaveInstanceState();
// Set new items
listView.setAdapter(adapter);
// Restore previous state (including selected item index and scroll position)
listView.onRestoreInstanceState(state)
And other solution is create RowView at runtime and add it on a Parent Layout by using addView() method.
LayoutInflater inflater = LayoutInflater.from(context);
// You should use the LinerLayout instead of the listview, and parent Layout should be inside of the ScrollView
parentView = (LinerLayout)this.findViewById(R.id.parentView);
for(int i = 0; i<=numberOfRow;i++){
LinearLayout rowView = (LinerLayout)inflater.inflate(R.layout.rowView);
ImageView rowImageView = (ImageView)rowView.findViewById(R.id.rowImage);
rowImageView.setOnClickListener(new View.onClickListListener(){
#Override
public void onClick(){
rowImageView.setImageBitmap(onClickBitmapImage);
}
});
parentView.addView(rowView);
}
Please check this answer Maintain/Save/Restore scroll position when returning to a ListView
More Reference
http://developer.android.com/reference/android/widget/Adapter.html#getView(int,android.view.View, android.view.ViewGroup)
The item changes back to the default background because the view gets recycled. This is the same problems of checkboxes losing their checked state
Check out this answer too see how to handle it:
CheckBox gets unchecked on scroll in a custom listview
As for your second problem, I believe it's already answered here:
highlighting the selected item in the listview in android
Hope it helps
I facing below issue with item back ground on scrolling.
In my application I have a listview which require multi-selection. Also this is a custom list where selection needs to be represented by change in list item color instead of check-box based approach.
For this: In the OnClick I'm checking if the position is selected or not and then set the background for the item. However this has issue when I scroll the list. Taking an example:
suppose the list has 50 items. And 10 are visible at a time. I select say 5th item [thus changing the background]. And then I scroll the list. After scroll the visible part of the list corresponding to earlier 5th item ,say 15th item in item of the list but 5th index in visible portion,still has background corresponding to selected state. Whereas it should not have been set since I have not selected 15th item yet.
I tried:
a-In the getView method of adapter, if the item is not one of selected items I'm setting one background else different.Tried - setBackgroundColor as well setBackgrounddrawable.
b- In the xml have set the cacheColorHint to transparent
c- Have selector attached to items and the items responding to state [pressed,selected] in onlcick.
However still I'm not able to get rid of unwanted background color for item on scrolling.
Any help. I tried various suggestion mentioned in various post in SO but not succcessful yet.
I tried
thanks
pradeep
this is a normal behavior of ListView adapter in android, its getView() called on every scroll and for every new list item it call getView, if listview item currently not visible on UI then its convertView is equals to null: At a time listview take load of only visible list items, if it showing at a time 10 element out of 50, then listView.getChildCount() will return only 10 not 50.
In your case when you select 5, it reflected selection for 5+10(visible items count) = 15, 25, 35, 45 too.
To solve this problem you should have a flag associate with your each listItem data, for example if you have string array itemData[50] as array, then take an array of boolean isSelected[50] with initial value false for each.
Take a look for getView(), in adapter class:
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
string text = itemData[position]
if (convertView == null) {
rowLayout = (RelativeLayout) LayoutInflater.from(context)
.inflate(R.layout.list_view_item, parent, false);
holder = new ViewHolder();
holder.txtString= (TextView) rowLayout
.findViewById(R.id.txtTitle);
rowLayout.setTag(holder);
} else {
rowLayout = (RelativeLayout) convertView;
holder = (ViewHolder) rowLayout.getTag();
}
if(isSelected[position] == true){
holder.txtString.setText("Selected")
rowLayout.setBackGround(selected)
}else{
holder.txtString.setText("Not Selected")
rowLayout.setBackGround(notSelected)
}
public class ViewHolder {
public TextView txtString;
}
and in your Activity class on listView.setOnItemClickListener():
listView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> arg0, View arg1,
int position, long arg3) {
// TODO Auto-generated method stub
isSelected[position] = true // on selection
RelativeLayout rowLayout = (RelativeLayout) view;
rowLayout.setBackGround(Selected);
// also set here background selected for view by getting layout refference
}
});
I have a GridView in which I want to always show 7 icons, and sometimes an additional icon depending on a request. In the beginning the additional icon is never shown. This is the structure:
0 1 2
3 4 5
6 [7]
All the icons fit into the screen so I don't need/have scroll. Each icon is composed by an image and a text.
For this, I have a CustomAdapter which extends BaseAdapter. I have overriden the getView method in which I set the text and the image for each icon.
public View getView(int position, View convertView, ViewGroup parent) {
View v = null;
if (convertView == null) {
LayoutInflater li = ((Activity) context).getLayoutInflater();
v = li.inflate(R.layout.icon, null);
} else {
v = convertView;
}
TextView tv = (TextView) v.findViewById(R.id.icon_textView);
tv.setText(position);
ImageView iv = (ImageView) v.findViewById(R.id.icon_ImageView);
iv.setImageResource(imageResourcesArray[position]);
if ((position == ADDITIONAL_ICON)) && !showAdditionalIcon) {
v.setVisibility(View.INVISIBLE);
}
return v;
}
The imageResourcesArray[] is an array of integers with the image resources.
The other functions and variables in the CustomAdapter are:
public static final int ADDITIONAL_ICON = 7;
private boolean showAdditionalIcon = false;
public showAdditionalIcon(){
this.showAdditionalIcon = true;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
public hideAdditionalIcon(){
this.showAdditionalIcon = false;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
Later on, I create and set the CustomAdapter to the GridView from a class which extends Activity (say ClassA):
GridView grid = (GridView) findViewById(R.id.main_gridView);
customAdapter = new CustomAdapter(this);
grid.setAdapter(customAdapter);
My problem appears when after some calculations and requests to a server, I have to show the additional icon (number 7). So I call (from ClassA):
customAdapter.showAdditionalIcon();
Now, the additional icon appears, but the first icon disappears... I have tried to use notifyDataSetInvalidated() and notifyDataSetChanged() but both had the same result.
Of course, I could generate a new CustomAdapter with the additional icon allowed, but I would preffer not to do it...
Thanks in advance.
I'm not sure if this counts as an answer for you. Root of the problem seems to be the convertView we are using. I did not dig so deep into Android source, but I think there is no guarantee on how views are reused even it is obvious that all views are already visible and there should be no reuse behind the scenes.
What this means is that the view we linked to position 7 as we visualize this whole scenario is actually reused later at position 0. Since your code does not explicitly reset a view to be visible, the view will be reused with visibility set to INVISIBLE, thus the mystery of the disappearing first item.
Simplest solution should be as #Vinay suggest above, by explicitly setting to View.VISIBLE.
if ((position == ADDITIONAL_ICON))) {
if (!showAdditionalIcon)
v.setVisibility(View.INVISIBLE);
else
v.setVisibility(View.VISIBLE);
}
Hope this helps, but I'm really hoping some Android expert pops by to tell us more about how this whole thing of reusing old views actually works.
An activity has a Button and a ListView.
Initially, only the Button is visible. When the button is pressed, the ListView is displayed.
When displayed, is it possible for me to show one particular item as selected/focussed?
A use case could be that suppose it is a list of language settings and when the list opens, the currently selected language must be shown as highlighted.
If I know the index of the item, how to set it as focused on display?
I post my solution, because google still doesn't know the answer.
getListView().setItemChecked(selectedGroupIndex, true);
In short, ListView::setSelection(int position) is what you need. However, depending on whether the device is in touch mode or not, it may or may not have visual effect (background highlighting). For more details, refer to Android ListView Selection Problem
If you use an Adapter for your ListView add this code to your adapter:
public class MyAdapter extends ArrayAdapter<MyClass> {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflator = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rowView = inflator.inflate(R.layout.my_adapter, null);
} else {
rowView = (View) convertView;
}
//...
// set selected item
LinearLayout ActiveItem = (LinearLayout) rowView;
if (position == selectedItem){
ActiveItem.setBackgroundResource(R.drawable.background_dark_blue);
// for focus on it
int top = (ActiveItem == null) ? 0 : ActiveItem.getTop();
((ListView) parent).setSelectionFromTop(position, top);
}
else{
ActiveItem.setBackgroundResource(R.drawable.border02);
}
}
private int selectedItem;
public void setSelectedItem(int position) {
selectedItem = position;
}
}
In your Activity:
myAdapter.setSelectedItem(1);
I am using an Adapter and didn't want to set custom background colors, but use the android:state_selected in drawable xml. SetSelection didn't work for me, but maybe that's also since I needed SetNotifyDataChanged which shows that the Selected State is not persistent.
I also found that the Selected state for an item in a ListView is not persistent, since SetNotifyDataChanged results in updating the ListView layout which clears them all. Setting the item to Selected in the Adapter's GetView is too soon too.
Eventually I set the Selected state for the view of the selected item after the layout of the listview has been changed, which is when LayoutChange event is being triggered (in Java it's probably attaching a to OnLayoutChangeListener of the ListView).
To make it really easy I store the view of the selected item as Adapter's SelectedItemView.
In the ListView's LayoutChange eventhandler I just set the adapter's SelectedItemView.Selected to true.
Here's the code from my Activity where I set the Adapter for the ListView and also subscribe to LayoutChange (or in Java attach an OnLayoutChangeListener)
ringTonesListView.Adapter = ringTonesListAdapter;
ringTonesListView.LayoutChange += (s, layoutChangeArgs) => {
//At this stage the layout has been updated and the Selected can be set to true for the view of the selected item. This will result in android:state_selected logic to be applied as desired and styling can be completely done per layout in Resources.
ringTonesListAdapter.SelectedItemView.Selected = true;
};
Here's my code for the Adapter:
public class RingTonesListAdapter : BaseAdapter<RingToneItem>
{
List<RingTone> Items { get; set; }
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
// re-use an existing view, if one is available
// otherwise create a new one
if (view == null)
{
view = Context.LayoutInflater.Inflate(Resource.Layout.AlertSoundItem, parent, false);
view.Click += SelectRingTone;
}
RingTone ringTone = this[position];
if (ringTone.Selected)
{
//==> Important
//Store this view since it's the view for the Selected Item
SelectedItemView = view;
//Setting view.Selected to true here doesn't help either, since Selected will be cleared after.
}
return view;
}
private void SelectRingTone(object sender, EventArgs args)
{
View view = (View)sender;
string title = view.FindViewById<TextView>(Resource.Id.ringToneTitle).Text;
RingToneItem ringToneItem = Items.First(rt => rt.Title == title);
if (!ringToneItem.Selected)
{
//The RingTone was not selected and is selected now
//Deselect Old and Select new
foreach (RingToneItem oldItem in Items.Where(rt => rt.Selected))
{
oldItem.Selected = false;
}
// Select New RingTone
ringToneItem.Selected = true;
//Update the ListView.
//This will result in removal of Selected state for all Items when the ListView updates it's layout
NotifyDataSetChanged();
}
//Now play the test sound
NotifierService.TestSound(Context, ringToneItem);
}
public View SelectedItemView { get; set; }
}