Android ListView Refresh Single Row - android

After I have gotten the data for a single row of a ListView, I want to update that single row.
Currently I am using notifyDataSetChanged(); but that makes the View react very slowly. Are there any other solutions?

As Romain Guy explained a while back during the Google I/O session, the most efficient way to only update one view in a list view is something like the following (this one update the whole view data):
ListView list = getListView();
int start = list.getFirstVisiblePosition();
for(int i=start, j=list.getLastVisiblePosition();i<=j;i++)
if(target==list.getItemAtPosition(i)){
View view = list.getChildAt(i-start);
list.getAdapter().getView(i, view, list);
break;
}
Assuming target is one item of the adapter.
This code retrieve the ListView, then browse the currently shown views, compare the target item you are looking for with each displayed view items, and if your target is among those, get the enclosing view and execute the adapter getView on that view to refresh the display.
As a side note invalidate doesn't work like some people expect and will not refresh the view like getView does, notifyDataSetChanged will rebuild the whole list and end up calling getview for every displayed items and invalidateViews will also affect a bunch.
One last thing, one can also get extra performance if he only needs to change a child of a row view and not the whole row like getView does. In that case, the following code can replace list.getAdapter().getView(i, view, list); (example to change a TextView text):
((TextView)view.findViewById(R.id.myid)).setText("some new text");
In code we trust.

One option is to manipulate the ListView directly. First check if the index of the updated row is between getFirstVisiblePosition() and getLastVisiblePosition(), these two give you the first and last positions in the adapter that are visible on the screen. Then you can get the row View with getChildAt(int index) and change it.

This simpler method works well for me, and you only need to know the position index to get ahold of the view:
// mListView is an instance variable
private void updateItemAtPosition(int position) {
int visiblePosition = mListView.getFirstVisiblePosition();
View view = mListView.getChildAt(position - visiblePosition);
mListView.getAdapter().getView(position, view, mListView);
}

The following code worked for me. Note when calling GetChild() you have to offset by the first item in the list since its relative to that.
int iFirst = getFirstVisiblePosition();
int iLast = getLastVisiblePosition();
if ( indexToChange >= numberOfRowsInSection() ) {
Log.i( "MyApp", "Invalid index. Row Count = " + numberOfRowsInSection() );
}
else {
if ( ( index >= iFirst ) && ( index <= iLast ) ) {
// get the view at the position being updated - need to adjust index based on first in the list
View vw = getChildAt( sysvar_index - iFirst );
if ( null != vw ) {
// get the text view for the view
TextView tv = (TextView) vw.findViewById(com.android.myapp.R.id.scrollingListRowTextView );
if ( tv != null ) {
// update the text, invalidation seems to be automatic
tv.setText( "Item = " + myAppGetItem( index ) + ". Index = " + index + ". First = " + iFirst + ". Last = " + iLast );
}
}
}
}

There is another much more efficient thing you can do, if it fits your use-case that is.
If you are changing the state and can somehow call the proper (by knowing the position) mListView.getAdapter().getView() it will the most efficient of all.
I can demonstrate a really easy way to do it, by creating an anonymous inner class in my ListAdapter.getView() class. In this example I have a TextView showing a text "new" and that view is set to GONE when the list item is clicked:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// assign the view we are converting to a local variable
View view = convertView;
Object quotation = getItem(position);
// first check to see if the view is null. if so, we have to inflate it.
if (view == null)
view = mInflater.inflate(R.layout.list_item_quotation, parent, false);
final TextView newTextView = (TextView) view.findViewById(R.id.newTextView);
view.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mCallbacks != null)
mCallbacks.onItemSelected(quotation.id);
if (!quotation.isRead()) {
servicesSingleton.setQuotationStatusReadRequest(quotation.id);
quotation.setStatusRead();
newTextView.setVisibility(View.GONE);
}
}
});
if(quotation.isRead())
newTextView.setVisibility(View.GONE);
else
newTextView.setVisibility(View.VISIBLE);
return view;
}
The framework automatically uses the correct position and you do have to worry about fetching it again before calling getView.

For me below line worked:
Arraylist.set(position,new ModelClass(value));
Adapter.notifyDataSetChanged();

Just for the record, did anyone consider the 'View view' on the override method ?
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//Update the selected item
((TextView)view.findViewById(R.id.cardText2)).setText("done!");
}

Related

Android: Change color of 1 listview entry applies on every 9th

I'm writing a little android application.
I have got a problem with a listview.
When an entry is clicked, the state of the datastructure should be marked as selected or not selected. In the datastructure everything is fine, but the selection should be visualised by changeing the backgroundcolor of the entry.
So far so good, but after a click the correct element change its color, but also every 9th element (entry 0 clicked -> changed color, but also entry 8, entry 16 and so on)in the listview but i have no idea why.
Question: Why does not only the clicked entry changed Color.?
Here my getView code:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View listItem = convertView;
if (listItem == null)
listItem = LayoutInflater.from(mContext).inflate(R.layout.listitem, parent, false);
final User currentUser = userList.get(position);
TextView username = listItem.findViewById(R.id.lv_username);
String state = "disabled";
if(currentUser.getSelected()) state = "enabled";
username.setText(currentUser.getUsername() + " - " + state);
listItem.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
currentUser.setSelected(!currentUser.getSelected());
if (currentUser.getSelected()) {
Log.d("onClick in Adapter", "selected");
v.setBackgroundColor(Color.WHITE);
}
else {
Log.d("onClick in Adapter", "not selected");
v.setBackgroundColor(Color.TRANSPARENT);
}
notifyDataSetChanged();
}
});
return listItem;
}
Thats the way the recyclingview works, since a previously non-visible view is being used, all state is kept, so when reusing it will be already colored (note that if you click it will not become transparent, but clicking twice will cause the data structure is intact).
The solution is to always reapply the state to a view about to become visible.
if (listItem == null)
listItem = LayoutInflater.from(mContext).inflate(R.layout.listitem, parent, false);
listItem.setBackgroundColor(currentUser.getSelected() ? Color.WHITE : Color.TRANSPARENT);
did you try this, inside getview get the position check if it equals 9th, then change background color for that row.
if (position==9){
convertView.setTextColor(Color.RED);
}

firstVisibleItem passed into onScrollListener does not get updated when more items are added to list in listView

I have a list view which gets its contents from the web. on my onscrollListener, I have a check, so that if the firstVisibleItem is less than a thresh hold, I download more data. I also have checks to make sure I don't try to get more items when a previous get is still in progress.
But even after the get data request has completed, new data has been added to the data list, and notifyDataSetChange has been called, my firstVisibleItem is not updated (ie still shows the previus value that triggered the request for more data, even though the list has grown and the same row should now have a bigger number as its position). So in essence, once I hit the threshold for getting more data the first time, it will start a chain of events that will keep downloading more and more data until all rows have been retrieved.
I am using a LinkedList for my data. I need to be able to add items on both ends of my list.
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (moreMessages && !getMessagesInProgress && (firstVisibleItem < 10)) {
getMessages(NUM_MESSAGES_TO_GET, "after");
getMessagesInProgress = true;
}
}
When I successfully get the next chunk of data from the server, I add them it to my linked list using LinkedList.addFirst() in a for loop, call notifyDataSetChanged, and set getMessagesInProgress to false. My ListView is stacked from bottom.
UPDATE: When I say my firstVisibleItem does not update, I'm NOT talking about the view on the screen, I mean that the next time my OnScrollListener() gets called, the value passed in as firstVisibleItem is still the same as before, even though I have added 20 more downloaded items to the end of the list. so the row which was row position 1, should now be 21. (my list is stalked from bottom, its a message thread, so user starts at bottom of list and swipes down to see older messages which are at the top of the list. When firstVisibleItem < 10, I download 20 older messages and add to my linked list, so now the position of all the previous items should be +20. This doesnt happen and as a result, the first time firstVisibleItem becomes <10, my code continuously sends requests to the server for more messages)
I have not included my getView code, cuz this isn't a UI/visual issue. But here is the code from my custom BaseAadapter relating to "position"
#Override
public int getCount() {
return messages.size();
}
#Override
public Object getItem(int position) {
return messages.get(new Integer(position));
}
#Override
public long getItemId(int position) {
return position;
}
try in this way
when you add data in list then remove or update first visible item.
and In getView() of list, you are using if condition then write their else part too
get items from web view then add into list seems ok.and notifyDataSetChange() has been called in a correct way.
but I notice that you said
"my firstVisibleItem is not updated (ie still shows the previus value that triggered the request for more data " .
maybe you should check public View getView(int position, View convertView, ViewGroup parent) in your ListView adapter.It should like:
public View getView(int position, View convertView, ViewGroup parent) {
View item;
if (convertView == null) {
item = View.inflate(mContext, R.layout.your_list_item, null);
} else {
item = (View) convertView;
}
ListItem listItem=YouCustomList.get(position);
//update your date in UI like below
TextView textView=(TextView)item.findViewById(R.id.your_textView);
textView.setText(listItem.text);
return item;
}
so that it will update UI. if you do return when convertView!=null like :
//won't update!
if (convertView == null) {
item = View.inflate(mContext, R.layout.your_list_item, null);
} else {
item = (View) convertView;
return item;
}
then UI won't update until ListView destroyed and reconstructed.
The problem was that when new items get added to the list, the list would actually jump to the newly added, in order to maintain the same "irresistible".
I found a solution in another post that manually sets the new posotion of the view
int newFirstPosition = threadListView.getFirstVisiblePosition() + loopCount;
View v = threadListView.getChildAt(threadListView.getHeaderViewsCount());
int top = (v == null) ? 0 : v.getTop();
...
// for loop to add new data
...
messagesAdapter.notifyDataSetChanged();
threadListView.setSelectionFromTop(newFirstPosition, top);

Odd CheckedTextView Behavior

Okay, bear with me this is a very odd & complex issue and I'm having a hard time explaining it. I'm using Xamarin.Android (Monodroid) although the source of the issue is likely an android thing.
I have an Activity which pages through Fragments manually by adding and removing them.
next = Fragments[nextIndex];
FragmentTransaction ftx = FragmentManager.BeginTransaction ();
ftx.SetTransition (FragmentTransit.FragmentFade);
ftx.SetCustomAnimations (Resource.Animator.slide_in_right, Resource.Animator.slide_out_left);
ftx.Remove (CurrentFragment);
ftx.Add (Resource.Id.fragmentContainer, next, next.Tag);
ftx.Commit();
The Fragments have a LinearLayout, which is populated with Row Views at run-time. (ListViews introduced too many focus issues with validating text entry).
// Manual ListView
this.Layout.RemoveAllViewsInLayout ();
for (int n = 0; n < Adapter.Count; n++)
{
View view = Adapter.GetView(n,null,Layout);
if (view != null) {
if (view.Parent != Layout) {
Layout.AddView(view);
}
}
}
Some of these Row Views have within them a GridView. (The gridview does not scroll)
TextView title = view.FindViewById<TextView>(Resource.Id.titleView);
GridView grid = view.FindViewById<GridView>(Resource.Id.gridView);
if (grid.Adapter == null) {
InlineAdapter adapter = new InlineAdapter(view.Context, list, this.Adapter);
// Set Grid's parameters
grid.Adapter = adapter;
grid.OnItemClickListener = adapter;
grid.SetNumColumns(adapter.GetColumns(((Activity)view.Context).WindowManager));
grid.StretchMode = StretchMode.StretchColumnWidth;
}
grid.ClearChoices();
// Get Current Value(s)
if (list.Mode == ListMode.SingleSelection)
{
grid.ChoiceMode = ChoiceMode.Single;
for (int b = 0; b < list.Selections.AllItems.Count; b++)
{
grid.SetItemChecked(b, false);
}
grid.SetItemChecked(list.GetSelectedIndex(DataStore), true);
}
The grid views have in them CheckedTextViews (either single or multiple choice).
// From "InlineAdapter"
public override View GetView(int position, View convertView, ViewGroup parent)
{
GridView grid = ((GridView)parent);
int layout = (list.Mode == ListMode.MultiSelection) ? Resource.Layout.RowListInclusive : Resource.Layout.RowListExclusive;
CheckedTextView view = (CheckedTextView)convertView;
if (view == null)
{
view = (CheckedTextView)inflator.Inflate(layout, parent, false);
}
view.Text = items[position];
// Calabash
view.ContentDescription = list.ContentDescription + "." + Selections.ContentDescription(items [position]);
return view;
}
When a fragment is presented the first time (before it is removed) everything performs as expected.
When a fragment is presented the second and subsequent times (after it is removed and re-added) only the last grid view accepts user input. In addition, whatever is selected on this last grid view ALL of the grid views select. (e.g. if the last one selects choice 2, then all of the grid views change to selecting choice 2)
I have:
Verified that the underlying data is correct
Verified using .GetHash() that all of the CheckedTextViews, GridViews, and Adapters are unique for their given rows.
Verified that touches propagate to the correct CheckedTextViews, and modify the correct data.
Verified that NotifyDataSetChanged() is called for the correct GridView.
I am personally stumped to gump on this one.

Android Expandable List View - onClickListener for buttons within the child with persistent states

I am currently having an issue with the creation of my ExpandableListView. I'm unsure what is happening at the moment but I'll explain what I'm trying to do.
I programmatically allocate meals that fall on a certain day. Within each meal (the title is the group) is a child that contains a vote and order button. Both of which are changed to "Ordered" when a order is made on a meal. If another order is made the previous order button goes back to the 'order' state and the selected one goes to ordered. This is the same for vote. I go about this by setting an on click listener to my buttons that loop through all the other buttons and set their text to 'order'/'vote' and set the current buttons text to ordered.
This works fine in some extremely rare cases - but most of the time when I order/vote for an item it changes the selected one to 'ordered' and the last element in my expandable list view to ordered as well. Then if I order, lets say the 3rd element, close and reopen the second element it also changes to ordered and visa versa. Sometimes they all change back to order or all change to ordered. I'm finding it difficult to work out why this may be happening.
I believe it may have something to do with the getChildView function. As when I traverse through the list opening each element the view is never set for the last element even though it has been allocated the appropriate data.
Am I misunderstanding a major concept of the ExpandableListView or is there something else.
Here is the getChildView function that I believe there may be a fault. I'll also supply links to the other relevant classes below. If you need further information, please don't hesitate to ask.
//Static class view holder
static class ViewHolder {
protected Button oBut;
protected Button vBut;
protected TextView description;
}
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
//Getting the appropriate child for the group and position
ExpandListChild child = (ExpandListChild) getChild(groupPosition, childPosition);
View view = null;
//Set up if doesn't exist
if (convertView == null) {
Log.d("OnClickListener", "groupPosition: " + groupPosition + " childPosition: " + childPosition);
Log.d("OnClickListener", "View being set up for: " + child.getName() + " desc: " + child.getDescription());
LayoutInflater infalInflater = (LayoutInflater) context.getSystemService(context.LAYOUT_INFLATER_SERVICE);
view = infalInflater.inflate(R.layout.expandlist_child_item_todays, null);
//Attempt at viewholder
final ViewHolder holder = new ViewHolder();
holder.description = (TextView) view.findViewById(R.id.mealDescription);
holder.vBut = (Button)view.findViewById(R.id.vote);
holder.oBut = (Button)view.findViewById(R.id.order);
view.setTag(holder);
}
else {
view = convertView;
}
ViewHolder holder = (ViewHolder) view.getTag();
holder.description.setText(child.getDescription());
holder.description.setTag(child.getTag());
holder.oBut.setOnClickListener(new OrderOnClick(groups, (Activity)context, holder.oBut, view, child));
holder.vBut.setOnClickListener(new VoteOnClick(groups, (Activity)context, holder.vBut, view, child));
return view;
}
ExpandListAdapterTodays (extends and extends off my BaseExpandableListAdapter)
ExpandListAdapter (extends the BaseExpandableListAdapter)
VoteOnClick (Class that implements the changing of button text when a successfull vote has been placed)
the way that listviews on android work to be efficient is that they implement a "view recycling" method where if some view goes off screen, that same view is put back somewhere on the screen with all the necessary bits changed. That keeps things using much less resources, actually reusing the same resources, but wreaks havoc if you need states persisting in a specific order like you do. What you should do is to implement some sort of map or arraylist of which the position on the object corresponds to its position of the listview and then make changes through the made object. A little lame but its kinda like an adapter method for your adapter.
Now i apologize because i can't exactly visualize how the onlick method is supposed to work, but looking through it.. something like:
for (int i = 0; i < groups.size(); i++) {
....
but.setText("Vote");
would become
ArrayList<String> group_string_states = new ArrayList<String> ();
private void fillGroupStringStates () {
for(int i = 0; i < groups.length; i++) {
group_string_states.add("Order");
}
}
....
for (int i = 0; i < group_string_states.size(); i++) {
....
group_string_states.set(i, "Vote");
Then you do a conditional, like
if group_string_states.get[position] says "vote", then do this.
Well, that's how i'd attempt to do it. i hope it helped and i'm really sorry if it didn't.

Custom ListView adapter, strange ImageView behavior

I have a custom ListView Adapter, with which I'm creating rows for a list. My problem is though, that it doesn't seem to be distinguishing the ImageViews, from each other. It seems to be randomly picking ImageViews to chuck into place as I scroll up and down. The text information (omitted from this snippet) does not break. It works as one would expect.
Here is the relevant method of my Adapter:
public View getView( int position, View convertView, ViewGroup parent )
{
View v = convertView;
if( v == null )
{
LayoutInflater vi = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate( R.layout.generic_row, null );
}
// find the image
ImageView favImage = (ImageView)v.findViewById( R.id.toggle_favorite );
// when clicked...
favImage.setOnClickListener( new OnClickListener() {
#Override
public void onClick( View v )
{
// make the gray star a yellow one
int newImage = R.drawable.ic_star_yellow_embossed;
((ImageView)v).setImageBitmap(BitmapFactory.decodeResource(getContext().getResources(), newImage));
}
});
return v;
}
That behavior appears because the ListView recycles the row views as you scroll the list up and down, and because of this you get rows that were acted on by the user(the image was changed) in position were the image should be unmodified. To avoid this you'll have to somehow hold the status of the ImageView for every row in the list and use this status to set up the correct image in the getView() method. Because you didn't say how exactly did you implement your adapter I will show you a simple example.
First of all you should store your the statuses of the ImageView. I used an ArrayList<Boolean> as a member of the custom adapter, if the position(corresponding to the row's position in the list) in this list is false then the image is the default one, otherwise if it is true then the user clicked it and we should put the new image:
private ArrayList<Boolean> imageStatus = new ArrayList<Boolean>();
In your custom adapter constructor initialize this list. For example if you put in your adapter a list of something then you should make your imageStatus as big as that list and filled with false(the default/start status):
//... initialize the imageStatus, objects is the list on which your adapter is based
for (int i = 0; i < objects.size(); i++) {
imageStatus.add(false);
}
Then in your getView() method:
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.adapters_adapter_with_images, null);
}
// find the image
ImageView favImage = (ImageView) v
.findViewById(R.id.toggle_favorite);
// Set the image bitmap. If the imageStatus flag for this position is TRUE then we
// show the new image because it was previously clicked by the user
if (imageStatus.get(position)) {
int newImage = R.drawable.ic_star_yellow_embossed;
favImage.setImageBitmap(BitmapFactory.decodeResource(
getContext().getResources(), newImage));
} else {
// If the imageStatus is FALSE then we explicitly set the image
// back to the default because we could be dealing with a
// recycled ImageView that has the new image set(there is no need to set a default drawable in the xml layout)
int newImage = R.drawable.basket_empty; //the default image
favImage.setImageBitmap(BitmapFactory.decodeResource(
getContext().getResources(), newImage));
}
// when clicked... we get the real position of the row and set to TRUE
// that position in the imageStatus flags list. We also call notifyDataSetChanged
//on the adapter object to let it know that something has changed and to update!
favImage.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Integer realPosition = (Integer) v.getTag(); //get the position from the view's tag
imageStatus.set(realPosition, true); //this position has been clicked be the user
adapter.notifyDataSetChanged(); //notify the adapter
}
});
// set the position to the favImage as a tag, we later retrieve it
// in the onClick method
favImage.setTag(new Integer(position));
return v;
}
This should work well if you don't plan to dynamically modify the list(remove/add rows), otherwise you'll have to take care of also modifying that list of imageStatus to reflect the changes. You didn't say what was your row data, another approach(and the right one if you plan to do something if the user clicks that image(besides changing it)) is to incorporate the status of the image in the row's data model. Regarding this here are some tutorials:
Android ListView Advanced Interactive
or Commonsware-Android Excerpt (Interactive rows)
you need to define the default image right after finding its reference:
// find the image
ImageView favImage = (ImageView)v.findViewById( R.id.toggle_favorite );
//setting to default
favImage.setImageResource(R.drawable.default_image);
// when clicked...
favImage.setOnClickListener....
you need to do this because once the image is changed, and you scroll the ListView , it reappears because ListView recycles the item views. So you need to define it in getView to use a default image when the list is scrolled

Categories

Resources