I want to update the UI from a timer. That's not a problem at all but when it comes to Gallery/ListViews it gets difficult. I have a Gallery with custom BaseAdapter. I need a counter for every (gallery) item (every item counts different depending on the items data). That counter should run outside of the main thread. In addition I don't want to run 10 threads for 10 items of the gallery when just one item is visible. It's not a problem to define a Handler and start a thread (the counting) in the adapters getView()-method when a item/view gets visible. I can think of something like the following code:
public class MyAdapter extends BaseAdapter {
static class ViewHolder {
//...
public Handler myHandler;
}
public View getView(int position, View convertView, ViewGroup parent) {
//...
// getView() gets called indefinite so first remove callback because it may be added already
holder.myHandler.removeCallbacks(myRunnable);
holder.myHandler.postDelayed(myRunnable, 0);
//...
}
}
The problem is to remove the callback for a view thats not visible anymore because in getView() I get noticed when a view becomes visible but I have no clue how to get the view (and thereby the holder and it's handler) that became invisible to remove the callback.
Is there a(nother) approach to solve that?
Found a clean solution (if someone needs something like that). I needed to update a TextView to set the new counter value (every second).
In BaseAdapters getView I add the TextViews(s) to a WeakHashMap. The TextView is the key of the map. A key/value mapping will be removed if the key is no longer referenced. So I do not cause memory leaks. The GarbageCollector does the work.
A thread counts "counter objects" down and refreshes the TextViews with the corresponding values (GarbageCollector runs all the time so in my case the map consists mostly of 3 items because the gallery shows just one item. Due to GarbageCollection the map gets immediately cleared by "unused" TextViews specially when scrolling fast through the list/gallery)
BaseAdapter's getView():
private WeakHashMap<TextView, Integer> counterMap = new WeakHashMap<TextView,Integer>();
private Handler counterHandler = new Handler();
public View getView(...) {
//...
counterMap.put(holder.tvCounter, position);
//...
}
The counter:
// Thread decrementing the time of the counter objects and updating the UI
private final Runnable counterTimeTask = new Runnable() {
public void run() {
//...
// decrement the remaining time of all objects
for (CounterData data : counterDataList)
data.decrementTimeLeftInSeconds();
// iterate through the map and update the containing TextViews
for (Map.Entry<TextView, Integer> entry : counterMap.entrySet()) {
TextView tvCounter = entry.getKey();
Integer position = entry.getValue();
CounterData data = counterDataList.get(position);
long timeLeftInSeconds = data.getTimeLeftInSeconds();
tvCounter.setText(" " + timeLeftInSeconds);
}
if (yourCondition)
counterHandler.postDelayed(this, 1000);
}
};
Start/stop the counter:
public void startCounter() { counterHandler.post(counterTimeTask); }
public void stopCounter() { counterHandler.removeCallbacks(counterTimeTask); }
Related
I am retrieving some data in an Async class called from a Custom ArrayAdapter. When i add a new comment, i update the comment text view and that works ok, but after i reload the entire list the updates don't appear anymore. I can see in the logcat that there is a new comment nr, but not on the UI.
Shouldn't this : answersListView.invalidateViews be enough? I am trying to update that single row from the listview, to escape the issue with not updating the comment nr after a while.
private void updateView(int index) {
System.out.println("index: " + index);
View v = answersListView.getChildAt(index - answersListView.getFirstVisiblePosition());
if (v == null)
return;
final TextView nrComments = (TextView) v.findViewById(com.dub.mobile.R.id.showCommentsTxt);
if (nrComments != null) {
if (nrComments.getText().toString().trim().length() == 0) {
// first comment
nrComments.setText("1 comments");
} else {
// comments exist already
int newNr = Integer.parseInt(nrComments.getText().toString().trim()
.substring(0, nrComments.getText().toString().trim().indexOf("comments")).trim()) + 1;
nrComments.setText(newNr + " comments");
}
System.out.println("final nr of comments: " + nrComments.getText());
nrComments.setVisibility(View.VISIBLE);
}
answersListView.getAdapter().getView(position, v, answersListView);
v.setBackgroundColor(Color.GREEN);
answersListView.invalidateViews();
}
And :
updateView(position);
adapter.notifyDataSetChanged();
The answer is pretty much what #Rami said in a comment to your question. You don't update directly the views, you just need to update the data. In your adapter, you override the getView method, in there is where you make all this changes, you don't need the updateViews method.
Let me try to explain how it works.
The listView uses an Adapter.
The List view ask the Adapter "give me the view in X position" with the getView method.
The adapter creates that view, is returned to the ListView, and that what is shown.
The Adapter itself, should contain a List with the data you want to show.
Those views (rows) are created and destroyed everytime one of those views become visible or invisible in the screen, or if you call the notifyDataSetChanged method.
So now, the thing is, if you change, let's say, the object in the position 5, of the adapter List for a different object with new data, then next time the ListView ask the Adapter to give me the view in the position 5, the adapter is going to create the View (row) with the new data. That's it.
you need to update the data in the UI thread.
if you have context in your Async class use
runOnUiThread(new Runnable() {
#Override
public void run() {
//update here
}
});
or
android.os.Handler handler = new android.os.Handler();
handler.post(new Runnable() {
#Override
public void run() {
//update here
}
});
That's not how you change data for an item in a list view. You must have used some array of strings to fill data in textview in the getView function of the adapter. You only need to change that array and then call notifydatasetchanged on adapter. Views are recycled by adapter so above does not make sense
getView (...)
{
TextView tv = new TextView(context);
tv.setText(myData[index]); // You need to change myData array contents
}
I know the reason why the getView method of an Adapter is called more than once, but is there a way for knowing which of the returned view will be actually displayed on the activity?
Until now I put all the returned view linked to the same position in a list and, every time I need to modify a shown view I modify all the views corresponding to that position (one of those will be the right one...).
Surely is not the best way...
Here a piece of code of my adapter:
class MyAdapter extends BaseAdapter {
Vector<ImageView> vectorView[] = new Vector<ImageView>[25];
public MyAdapter(Activity context) {
...
}
public doSomeStuffOnAView(int position) {
// needs to know which view corresponds to the given position
// in order to avoid the following for cycle
for (ImageView iv: vector[position]) {
// do something
}
}
public View getView(int position, ...) {
ImageView childView = ...;
if (vector[position]==null) {
vector[position]=new Vector<ImageView>();
}
vector[position].add(childView);
return childView;
}
}
The method getView(...) might be called more than once for each position, but just one returned view per position will be shown on the activity. I need to know which of these. I thought it was the one returned the last time getView has been called for a position, but it is now always true.
one way but it is not standard.. you can directly check with integer variable i.e. hardcoded type. You have list of views in your xml file. jst cross check & compare with integer variable.
Let say I have my view that I use it as a toggle button. When user clicks it, I change the background via setBackgroundResource(). The number of list is around 15 items and ListView can show only around 7 items on screen.
At first, I try to use ListView.getChildAt(position) but when position is more than 7 it returns NullPointer. eventhough ListView.getCount() returns 15. But that's make sense because it show only visible child.
Then I solve it by loop through all Data that binds to this Adapter, change the boolean value, and call notifyDataSetChange()
So the number of loop will be 15 for update data + 7 show visible view.
The best way should be 15 and that's done.
Is there anyway to achieve this?
Thank
Forget your child index. You should just toggle some type of flag in your adapter.
Then when your getView method is called again it will redraw your list.
i.e.:
public class YourAdapter extends BaseAdapter {
private boolean useBackgroundTwo = false;
.. constructor ..
#Override
public View getView (int position, View convertView, ViewGroup parent) {
...
...
View background = findViewById(...);
int backgroundResource = R.drawable.one;
if(useBackgroundTwo){
backgroundResource = R.drawable.two;
}
background.setBackgroundResource(backgroundResource);
....
}
public void useNewBackground(){
this.useBackgroundTwo = true;
notifyDataSetChanged();
}
public void useOldBackground(){
this.useBackgroundTwo = false;
notifyDataSetChanged();
}
}
Then in your activity code:
((YourAdapter) listview.getAdapter()).useNewBackground();
Taking it further, you could use an enum instead of a boolean and have multiple methods setBackgroundGreen(), setBackgroundRed() or you could pass in the drawable you want to use setItemBackground(R.drawable.one); The choice is yours.
Api: Adapter
I have an android activity that consists of a List View and a Text View. The Text view displays information about the summed contents of the list view, if the items in the list view are updated then the Text View needs to reflect the change.
Sort of like an observer pattern but with one component reflecting the changes of many rather than the other way round.
The tasks are displayed as items in the list view and the progress can be updated using a seekbar to set the new progress level.
The following method populates the TextView so I need to call this again from inside the SeekBar onStopTrackingProgress Listener.
private void populateOverviewText() {
TextView overview = (TextView) findViewById(R.id.TaskOverview);
if (!taskArrayList.isEmpty()) {
int completion = 0;
try {
completion = taskHandler.getProjectCompletion();
overview.setText("Project Completion = " + completion);
} catch (ProjectManagementException e) {
Log.d("Exception getting Tasks List from project",
e.getMessage());
}
}
}
I realise if I want to call the method from the List Adapter then I'll need to make the method accessible but I'm not sure the best approach to do this.
Either make this method public
or you can pass activity to adapter through which you can find the button and update text, or can directly pass Button to adapter...
Using DataSetObserver
I was looking for a solution that didn't involve altering the adapter code, so the adapter could be reused in other situations.
I acheived this using a DataSetObserver
The code for creating the Observer is incredibly straight forward, I simply add a call to the method which updates the TextView inside the onChanged() method.
Attaching the Observer (Added to Activity displaying list)
final TaskAdapter taskAdapter = new TaskAdapter(this, R.id.list, taskArrayList);
/* Observer */
final DataSetObserver observer = new DataSetObserver() {
#Override
public void onChanged() {
populateOverviewText();
}
};
taskAdapter.registerDataSetObserver(observer);
Firing the OnChanged Method (Added to the SeekBar Listener inside the adapter)
public void onStopTrackingTouch(SeekBar seekBar) {
int progress = seekBar.getProgress();
int task_id = (Integer) seekBar.getTag();
TaskHandler taskHandler = new TaskHandler(DBAdapter
.getDBAdapterInstance(getContext()));
taskHandler.updateTaskProgress(task_id, progress);
mList.get(position).setProgress(progress);
//need to fire an update to the activity
notifyDataSetChanged();
}
I've got a custom listview, each entry contains spinning ProgressBar and single line textview. I'm always updating only the first item in the list. The problem is that every time I call notifyDataSetChanged all visible views are either recreated or recycled as convertView.
There are 2 problems with this approach:
all entries are recreated and that's slow - and that's not required as I'm updating only the textview in first entry
the ProgressBar animation restarts every time
So I though that I'll just keep the reference to first View in adapter and update it manually. It worked, but it randomly throws IllegalStateException ("The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread.")
How do I solve it? Is there a way to notify ListView that only first entry changed? Even if there is, ProgressBar animation will still fail. Is there any other way to update it manually?
Thanks
Whenever i need to update one single item on a listview i use this:
public void updateItemAt(int index) {
View v = listView.getChildAt(index - listView.getFirstVisiblePosition());
if (v != null) {
//updates the view
}
}
The downside is that you need to hold a reference to the listView in your adapter.
As this code is usually called from a background task, you have to use a handler to call it:
Handler myHandler = new Handler() {
public void handleMessage(Message msg) {
int index = msg.what;
myAdapter.updateItemAt(index);
};
};
//when task is complete:
myHandler.sendEmptyMessage(index);