notifyDataSetChanged() without freezing mainthread - android

I have listview and custom adapter that uses my own objects to draw the listitem.
From the other head I have a service that is gathering information realtime and every 0.1s my activity calls for the service information and than redraw listview by calling myCustomAdapter.notifyDataSetChanged() method.
This is bad for me because the objects are really large and my UI thread freezes for some time probably less than 0.01s but still feels bad user experience.
The resource I'm updating is one circle that is drawn by custom drawing class in canvas. Does any of you know how to handle this problem ? Is there some way to update data and redraw listview without stopping my UI thread?

You should try to reuse your convertView, like this:
public View getView (int position, View convertView, ViewGroup parent){
//inflate the view if it is null
if( convertView == null ){
convertView = inflater.inflate(R.layout.my_list_item, parent, false);
}
//make the changes on your convertView that are changed from row to row,
//such as a text in a TextView
return convertView;
}

Related

Use of condition if(view==null) in CustomAdapters

I am writing a custom adapter for ListView which extends BaseAdapter and in this method
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View vi = convertView;
if (vi == null)
vi = inflater.inflate(R.layout.x, null);
}
why is the check if(vi==null) performed. getView() is called for row when it comes in the visible area of the screen. So vi will be null everytime getview() is called? So why is the check necessary here.?
P.S. I was getting some problem on coloring some specific rows of the listview, but when i removed this check, everything works fine. Thats why i am wondering over its usefullness.
The convertView parameter may be a recycled view (for example, after scrolling down, the top rows become invisible, so their View objects are not destroyed, but recycled and passed as parameters for reuse).
However, the very first time a draw request comes, there is no view (e.g. the first time the screen with the list is loaded). Hence, in this case convertView has no value because nothing has been recycled (it is null), in which case you must create the rows using the inflater.
convertView : is for recycling. Let's say you have a listview which can only display 10 item at a time, and currently it is displaying item 1 -> item 10. When you scroll down one item, the item 1 will be out of screen, and item 11 will be displayed. To generate View for item 11, the getView() method will be call, and convertView here is the view of item 1 (which is not neccessary anymore). So instead create a new View object for item 11 (which is costly), why not re-use convertView? => we just check convertView is null or not, if null create new view, else re-use convertView.

How can i smooth listView scrolling?

I am writing a media player application using Media extractor API. So my decoder decodes the data and shows content on surface. This is working fine. I have one list view. When i scroll this listview, it showing effect on decoder thread. So i am getting some disturbance. How can i resolve that. Is there any way to run my listView Adapter( getView() method) on separate thread?
I saw this link. It may not helpful to me.
http://developer.android.com/training/improving-layouts/smooth-scrolling.html Because My list item contain only one textView.
#Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView txt_Effects;
ImageView proImage;
if(convertView == null){
LayoutInflater inflater= (LayoutInflater)context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
convertView= inflater.inflate(R.layout.row_listview, null);
txt_Effects= (TextView)convertView.findViewById(R.id.txt_row);
proImage= (ImageView)convertView.findViewById(R.id.proImage_row);
}
else{
txt_Effects= (TextView)convertView.findViewById(R.id.txt_row);
proImage= (ImageView)convertView.findViewById(R.id.proImage_row);
}
txt_Effects.setText(data[position]);
txt_Effects.setTextColor(Color.WHITE);
proImage.setVisibility(View.GONE);
if(position == previousSelectionIndex){
txt_Effects.setTextColor(Color.MAGENTA);
prevSelectedTextView= txt_Effects;
}
if(position>= startProIndex && position <= endProIndex){
proImage.setVisibility(View.VISIBLE);
}
return convertView;
}
You should perform all "heavy" operations outside of getView() method. Consider using AsyncTask or ThreadPool for doing your "decoding" and then update your ListView in the UI thread.
You may want to use the ViewHolder pattern to improved the performance of loading your list items. Refer the following:
Android ViewHolder Pattern Example
Holder Pattern
Hope it helps.

How do I update the text of a button to a listview through another class without modifying the text of the other buttons on the list?

How do I update the text of a button to a listview through another class without modifying the text of the other buttons on the list?
Ex:
ITEM 1 [DOWNLOAD]
ITEM 2 [DOWNLOADING... 60%]
ITEM 3 [DOWNLOADING... 40%]
ITEM 4 [DOWNLOAD]
Actually, works but scrolling the listview, other buttons have your values changed too..
I need to create a list of mídia ready for download, but when I click in a download button, the download starts, the percent updates but other buttons have your text changed too...
I would like to update the text "downloadBt" buttons of the listView items through my class Downloader, making progress on them: Downloading ... 30%
What is the best form to make this?
Solution:
Create a new instance of List:
private static class ViewHolder {
protected ImageView cover;
protected TextView issueNumber;
protected TextView details;
protected Button downloadBt;
protected Button moreBt;
protected View convertView; <<<
protected ViewGroup parent; <<<
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
list.get(position).setListPosition(position);
if (convertView == null) {
LayoutInflater inflater = context.getLayoutInflater();
convertView = inflater.inflate(R.layout.list_item_list_issue, null);
viewHolder = new ViewHolder();
viewHolder.cover = (ImageView) convertView.findViewById(R.id.issue_list_item_cover);
viewHolder.issueNumber = (TextView) convertView.findViewById(R.id.issue_list_item_number);
viewHolder.details = (TextView) convertView.findViewById(R.id.issue_list_item_details);
viewHolder.downloadBt = (Button) convertView.findViewById(R.id.list_item_issue_download);
viewHolder.moreBt = (Button) convertView.findViewById(R.id.list_item_issue_more);
viewHolder.parent = parent;
viewHolder.convertView = convertView;
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.issueNumber.setText(list.get(position).getIssue());
viewHolder.downloadBt.setText(list.get(position).getDownloadStatus());
try{
vList.remove(position);
}catch(Exception e){
e.printStackTrace();
}
vList.add(position, viewHolder);
return convertView;
}
And in my Downloader class, send the position and the Adapter instance. I have been created a method refresh in my adapter class:
public void refresh(int position){
if((listView.getFirstVisiblePosition() <= position)&&(position <= listView.getLastVisiblePosition()))
getView(position, vList.get(position).convertView, vList.get(position).parent);
}
And this update only a selected item in the listview.
!!UPDATE!!
It is clear from the log that you are trying to update the button from a different thread other than UI thread. The UI objects/views should be updated only from the UI thread and UI thread should not be used for long running operations. These are basics you are supposed to know. I strongly recommend you go through the docs and/or videos for better understanding.
The solution is to use AsyncTask instead of Runnable and AsyncTask has a method progressUpdate() that runs on the UI thread. Alternatively, you can have your Runnable as a subclass in Activity and use the method runOnUiThread() for updating the view.
I also notice problem with your thread pools as you associate them with the objects in list view, which means, if there are 10 convertView objects, you'll have 10 thread pools with 50 threads. This will degrade the performance and you're essentially using only one thread from this pool. You need to get strong with your basics and revisit the solution.
Inside a list view, the item objects are reused, which is the convertView argument in the getview()method in the Adapter. So, when convertView is null, you create the item view object for the first time and as you scroll through, the same object is re-used instead of creating a new one as this saves CPU cycles( and hence battery) in terms of avoiding garbage collection and creating new objects. In other words, if you have a list of 100 items and only 10 items can be fit on your screen only 10 objects will be created even though you scroll through all the 100 items as the same 10 objects are re-used as the screen responds to scrolling. This is true for the child views as well(i.e., the buttons as well).
From your code, I see that you are using Button objects in your download threads. Button objects are also re-used. Check if this could be the problem.
Check this video if you haven't already.
The problem is that you are trying to update the button itself, you should update the items wrapped by adapter and call on the main thread adapter.notifyDataSetChanged(); in order to get your list view refreshed (trigger other calls to getView for visible positions). So your ComicDownloader should accept a comic object and a reference to the adapter itself.
Sundeep is right, convertView (toghether with the button in it) gets reused (and this is a normal and wanted behaviour) and you should'n rely on it.
This line: viewHolder.downloadBt.setText(list.get(position).getDownloadStatus()); should be called just before returning the convertView, and not only when convertView is null, doing so you make sure the returned view is up to date according to requested position, no matter if is reused or fresh created.
EDIT:
In your DownloadListener implementation, IssueListAdapter.this.notifyDataSetChanged(); should be called on the main thread. Currently, it's called directly from your run() call, wich is executed in background. Because this listener already knows which comic is updated, you can have a singleton for all your comics. Also, since your ComicDownloader is final, it will be related to first position that 'created' it, that means that wen you'll start a download, your button will update correct, but the comic downloaded behind may not be the one you think it is. To solve this, I recomend to create a ComicDownloader when button is clicked (you can also share one clickListener instance and retreive current comic for pressed button - setting a tag related to current position for button is a correct way to do it).
In your ComicDownloader class, executorService should be static (one pool shared across downloaders).

Async image loading, check if an image is recycled

This question came to me after reading this: Performance tips (specifically the part named "Async loading"). Basically he's trying to save info about a row to see if it's been recycled yet and only set the downloaded image if the row is still visible. This is how he saves the position:
holder.position = position;
new ThumbnailTask(position, holder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
Where ThumbnailTask constructor is:
public ThumbnailTask(int position, ViewHolder holder) {
mPosition = position;
mHolder = holder;
}
In onPostExecute() he then does the before mentioned check:
if (mHolder.position == mPosition) {
mHolder.thumbnail.setImageBitmap(bitmap);
}
I just don't see how this gives any result. The Holder and the position are set in the constructor at the same time, to the same value (the position in the holder is the same as the position in mPosition). They don't get changed during the AsyncTask (it's true that the position might change in getView(), but the ones stored in the AsyncTask as private members are never manipulated). What am I missing here?
Also saving the position doesn't seem like a good option in the first place: I believe that it's not guaranteed to be unique, and if I recall correctly it resets itself to 0 after scrolling. Am I thinking in the right direction?
Background (you probably know this, but just in case): An adapter contains a collection of objects and uses info from these objects to populate Views (each view is a line item in the list). The list view is in charge of displaying those views. For performance reasons the ListView will recycle views that are no longer visible because they scrolled off the top or the bottom of the list. Here's how it does it:
When the ListView needs a new view to display it calls the Adapter's getView with an integer argument "position" to indicate which object in the Adapter's collection it wants to see (position is just a number from 1 to N -1) where N is the count of objects in the adapter.
If it has any views that are no longer visible, it will pass one of them in to the Adapter, too, as "convertView" This says "reuse this old view rather than creating a new one". A big performance win.
The code in the article attaches a ViewHolder object to each view it creates that, among other things, contains the position of the object requested by the ListView. In the article's code, this position is stashed away inside the ViewHolder along with a pointer to the field within the view that will contain the image. The ViewHolder is attached to the View as a tag (a separate topic).
If the view gets recycled to hold a different object (at a different position) then ListView will call Adapter.getView(newPosition, oldView...) The code in the article will store new position into the ViewHolder attached to the oldView. {make sense so far?) and start loading this new image to put into the view.
Now in the article, it is starting an AsyncTask to retrieve data that should go into the view) This task has the position (from the getView call) and the holder (from the oldView). The position tells it what data was requested. The holder tells it what data should currently be diplayed in this view and where to put it once it shows up.
If the view gets recycled again while the AsyncTask is still running, the position in the holder will have been changed so these numbers won't match and the AsyncTask knows it's data is no longer needed.
Does this make it clearer?
When AsyncTask is passed with ViewHolder and position it is given value of position (say 5) and value of reference (not a copy) to ViewHolder object. He also puts current position in ViewHolder (said 5), but the whole "trick" here is that for recycled views, the old ViewHolder object is also re-used (in linked article):
} else {
holder = convertView.getTag();
}
so whatever code references that particular ViewHolder object, will in fact check against its position member value at the moment of doing check, not at the moment of object creation. So the onPostExecute check makes sense, because position value passed to task constructor remains unchanged (in our case it has value of 5) as it is primitive, but ViewHolder object can change its properties, if view will be reused before we reach onPostExecute.
Please note we do NOT copy ViewHolder object in the constructor of the task, even it it looks so. It's not how Java works :) See this article for clarification.
Also saving the position doesn't seem like a good option: I believe
that it's not guaranteed to be unique, and it resets itself to zero
after scrolling. Is this true?
No. Position here means index in *source data set, not visible on the screen. So if you got 10 items to display, but your screen fits only 3 at the time, your position will be in range 0-9 and visibility of the rows does not matter.
As I understand you are trying to cancel the async-loading-task of the image when the view recycled, and no longer on screen.
To achieve that you can set up an RecyclerListener to the listview. It will be invoked when the listview don't need this view (when is not on screen), just before it passes it as a recycled view to the Adapter.
within this listener you can cancel your download task:
theListView.setRecyclerListener(new RecyclerListener() {
#Override
public void onMovedToScrapHeap(View view) {
for( ThumbnailTask task : listOfAllTasks )
task.viewRecycled(task);
}
});
and within ThumbnailTask :
public void viewRecycled(View v){
if(mHolder.theWholeView == v)
v.cancel();
}
Don't for to implement the cancel.
Note that its not the best approach since you should keep track of all your asynctask tasks. note that you could also cancel the task within the adapter where you also get the
public View getDropDownView (int position, View recycledView, ViewGroup parent){
//.. your logic
}
but note that this might require you to allocate the ThumbnailTask within the adapter with is not good practice.
note that you could also use image loading libraries that do eveything for you, from async download to chaching. for instance : https://github.com/nostra13/Android-Universal-Image-Loader
The accepted answer and Marcin's post already describe perfectly what's supposed to happen. However, the linked webpage does not and the google site on this topic is also very vague and only a reference for people who already know about the "trick". So here's the missing part, for future references, which shows the necessary additions to getView().
// The adapter's getView method
public View getView(int position, View convertView, ViewGroup parent) {
// Define View that is going to be returned by Adapter
View newViewItem;
ViewHolder holder;
// Recycle View if possible
if (convertView == null) {
// No view recycled, create a new one
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
newViewItem = inflater.inflate(R.layout.image_grid_view_item, null);
// Attach a new viewholder
holder = new ViewHolder();
holder.thumbnail = (ImageView) newViewItem.findViewById(R.id.imageGridViewItemThumbnail);
holder.position = position;
newViewItem.setTag(holder);
} else {
// Modify "recycled" viewHolder
holder = (ViewHolder) convertView.getTag();
holder.thumbnail = (ImageView) convertView.findViewById(R.id.imageGridViewItemThumbnail);
holder.position = position;
// Re-use convertView
newViewItem = convertView;
}
// Execute AsyncTask for image operation (load, decode, whatever)
new LoadThumbnailTask(position, holder).execute();
// Return the ImageView
return newViewItem;
}
// ViewHolder class, can be implemented inside adapter class
static class ViewHolder {
ImageView thumbnail;
int position;
}

ListView in ArrayAdapter order get's mixed up when scrolling

I have a ListView in a custom ArrayAdapter that displays an icon ImageView and a TextView in each row. When I make the list long enough to let you scroll through it, the order starts out right, but when I start to scroll down, some of the earlier entries start re-appearing. If I scroll back up, the old order changes. Doing this repeatedly eventually causes the entire list order to be seemingly random. So scrolling the list is either causing the child order to change, or the drawing is not refreshing correctly.
What could cause something like this to happen? I need the order the items are displayed to the user to be the same order they are added to the ArrayList, or at LEAST to remain in one static order. If I need to provide more detailed information, please let me know. Any help is appreciated. Thanks.
I was having similar issues, but when clicking an item in the custom list, the items on the screen would reverse in sequence. If I clicked again, they'd reverse back to where they were originally.
After reading this, I checked my code where I overload the getView method. I was getting the view from the convertedView, and if it was null, that's when I'd build my stuff. However, after placing a breakpoint, I found that it was calling this method on every click and on subsequent clicks, the convertedView was not null therefore the items weren't being set.
Here is an example of what it was:
public View getView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.listitemrow, null);
RssItem rssItem = (RssItem) super.getItem(position);
if (rssItem != null)
{
TextView title = (TextView) view.findViewById(R.id.rowtitle);
if (title != null)
{
title.setText(rssItem.getTitle());
}
}
}
return view;
}
The subtle change is moving the close brace for the null check on the view to just after inflating:
public View getView(int position, View convertView, ViewGroup parent)
{
View view = convertView;
if (view == null)
{
LayoutInflater vi = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = vi.inflate(R.layout.listitemrow, null);
}
RssItem rssItem = (RssItem) super.getItem(position);
if (rssItem != null)
{
TextView title = (TextView) view.findViewById(R.id.rowtitle);
if (title != null)
{
title.setText(rssItem.getTitle());
}
}
return view;
}
I hope this helps others who experience this same problem.
To further clarify the answer of farcats below in more general way, here is my explanation:
The vi.inflate operation (needed here for parsing of the layout of a row from XML and creating the appropriate View object) is wrapped by an if (view == null) statement for efficiency, so the inflation of the same object will not happen again and again every time it pops into view.
HOWEVER, the other parts of the getView method are used to set other parameters and therefore should NOT be included within the if (view == null) statement.
Similarily, in other common implementation of this method, some textView, ImageView or ImageButton elements need to be populated by values from the list[position], using findViewById and after that .setText or .setImageBitmap operations.
These operations must come after both creating a view from scratch by inflation and getting an existing view if not null.
Another good example where this solution is applied for BaseAdapter appears in BaseAdapter causing ListView to go out of order when scrolled
The ListView reuses view objects when you scroll. Are you overriding the getView method? You need to make sure you set each property for every view, don't assume that it will remember what you had before. If you post that method, someone can probably point you at the part that is incorrect.
I have a ListView, AdapterView and a View (search_options) that contains EditText and 3 Spinners. ListView items are multiple copies of (search_options) layout, where user can add more options in ListView then click search to send sql query built according to users options.
I found that convertView mixing indecies so I added a global list (myViews) in activity and passed it to ArrayAdapter. Then in ArrayAdapter (getView) I add every newly added view to it (myViews).
Also on getView instead of checking if convertView is null, I check if the global list (myViews) has a view on the selected (position).. It totally solved problems after losing 3 days reading the internet!!
1- on Activity add this:
Map<Integer, View> myViews = new HashMap<>();
and then pass it to ArrayAdapter using adapter constructor.
mSOAdapter = new SearchOptionsAdapter(getActivity(), resultStrs, myViews);
2- on getView:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (!myViews.containsKey(position)) {
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(R.layout.search_options, parent, false);
/// ...... YOUR CODE
myViews.put(position, view);
FontUtils.setCustomFontsIn(view, getContext().getAssets());
}else {
view = myViews.get(position);
}
return view;
}
Finally no more mixing items...

Categories

Resources