I want to show a listview with some texts and images. When i'm creating a view for listview, i'm calling method show of my PictureImageView, that downloads and showing image. Download is running in new thread in AsyncTask. But while image downloading i can't normally scroll listview, it's twitches.
To run AsyncTask in new thread i call executeOnExecutor method. I tried to call execute method, but then scroll stops at all till download is over.
Here my class.
public class PictureImageView extends LinearLayout {
private Drawable image_drawable = null;
private ImageView image = null;
...
protected String getImageURL() {
...
return uri;
}
public void show() {
if (image_drawable != null) {
image.setImageDrawable(image_drawable);
addView(image);
} else {
// target Android API >= 14 so executeOnExecutor works in another thread
new RequestTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, getImageURL());
}
}
protected void onResponse(Drawable image) {
if (image != null) {
image_drawable = image;
show();
}
}
class RequestTask extends AsyncTask<String, String, Drawable> {
#Override
protected Drawable doInBackground(String... urls) {
Drawable image = null;
HttpURLConnection connection = null;
InputStream connection_stream = null;
try {
URL url = new URL(urls[0]);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setUseCaches(true);
connection.connect();
int response_code = connection.getResponseCode();
//#see http://libs-for-android.googlecode.com/svn/reference/com/google/android/filecache/FileResponseCache.html
if (response_code == HttpURLConnection.HTTP_OK || response_code == -1) {
connection_stream = connection.getInputStream();
image = Drawable.createFromStream(connection_stream, null);
}
} catch (MalformedURLException e) {
} catch (IOException e) {
} finally {
if (connection != null) {
connection.disconnect();
}
if (connection_stream != null) {
try {
connection_stream.close();
} catch (IOException e) {
}
}
}
return image;
}
#Override
protected void onPostExecute(Drawable image) {
PictureImageView.this.onResponse(image);
}
}
}
How can i fix it? I guess, the problem is that there is no any another thread, but how to check it?
I've delt with this exact problem first hand. The twitching comes from updating the ListView each time a picture is downloaded. There are 2 different approaches I took to fix this. Depending on your project set up one my work
Approach 1: Minimize twitching by only updating once
In my case I used an AsyncTask as a seperate class with a call back to the starting activity. What I did was use a singleThreadExecutor so that the task to download each user's picture were serialy executed and a counter to track how many treads were started/left - increamenting each time I added one to the executor, decrementing each time the call back was called. For example
#Override
public void userPic(Bitmap pic){
if(pic != null){
//use picture
}
taskCounter--
if(taskCounter == 0){
updateUserListView();
}
}
By updating once all threads were done I was able to minimize the twitching by only refreshing the list once, thus allowing scroll and jumping back to the top only once all picutres were done
Approach 2: eliminate twitch by using mem cache
Eventually what I ened up doing was using a cache to store bitmaps. This approach completely eliminated the jumping issue beacuse the list was no longer being refreshed, rather the adapter was loading bitmaps from the cache only when views were recycled. I still used a seperate task with a call back
#Override
public void userPic(Bitmap pic){
if(pic != null){
memCache.addPicture(pic);
}
}
only this time rather than update the list directly, if a picture was downloaded I stored it to the cache. Then in my adapter code, I set the picutre field to update from cache if present
if(picture_view != null){
if(memCache.contains(u.getId()){
picture_view.setImageBitmap(memCache.getPicture(u.getId()));
} else {
picture_view.setImageBitmap(memCache.getPicture("default"));
}
this approach takes advatage of the fact that views are updated in a ListView automaticaly once they are recycled. As you scroll and the views are rebuilt, the adapter will automatically populate the fields with new data if it has changed.
Downsides - the list does not auto upate. If pictures are downloaded for fields that are currently visible, they will not be updated until you scroll away from that view. Also, slightly more set up in creating a cache. I chose to use a singelton pattern to do this since I was accessing the cache from multiple places (e.g. adding pictures in one place and getting in another).
Related
In the following situation: What is my alternative to using an AsyncTask?
I am using AsyncTask to load images into an adapter. The adapter row has a number of TextViews and one ImageView. The ImageView is where I load the image. The image is being loaded from the internet. The problem is that when I scroll, the wrong image would show in a row/cell -- until the correct image has had time to arrive. How do I prevent this image mismatching from ever happening? I am asking this question because I want to understand how this works: I don't just want to get some library that might do the work for me (many libraries I have already tried, have failed).
Once again, the problem: the AsyncTask causes images to load into the wrong row so that the user can clearly see that the images are looking for their final destination.
I hope the question is clear. For completeness, below is the method that I am calling inside the getView method of the adapter to load each image. The method is also inside the adapter. What is my alternative to using an AsyncTask?
private void loadImage(final ImageView photo, String imageUrl) {
new AsyncTask<String, String, Bitmap>() {
#Override
protected Bitmap doInBackground(String... param) {
try {
Bitmap b = callToServer(imageUrl);//takes long
return b;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
#Override
protected void onPostExecute(Bitmap ret) {
if (ret != null) {
photo.setImageBitmap(ret);
}
}
}.execute(imageUrl);
}
The most common approach is using the setTag(...) and getTag(...) methods of the view that you create in the adapter. Once created, you add a tag that you need to link to the image that is then asynchronously loaded. When that task is finished, you can check the tag of the view and if it's still the same as when the async task has started you can set the image. Otherwise you can dismiss the image. Remember that the same view is re-used instead of created when you scroll. So the tag will have changed then.
Here's a pretty good example: Asynchronous Image Loader in Android ListView
The problem is that the rows get recycled and so does the image view. Then when the server response returns the image view already belongs to another data object.
There are different approaches to this. My solution is to extend ImageView and keep track of the image you want to load.
class RemoteImageView extends ImageView {
private String _uri;
public synchronized void loadRemoteImage(String uri) {
_uri = uri;
loadImage(this, uri) ; //this is your async call
}
private synchronized void onImageLoaded(String uri, Bitmap image) {
if(uri.equals(_uri)) { //this will set only the correct image
setImageBitmap(image);
}
}
}
As for your loading function:
private void loadImage(final ImageView photo, final String imageUrl) {
new AsyncTask<String, String, Bitmap>() {
#Override
protected Bitmap doInBackground(String... param) {
try {
Bitmap b = callToServer(imageUrl);//takes long
return b;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
#Override
protected void onPostExecute(Bitmap ret) {
if (ret != null) {
photo.onImageLoaded(imageUri, ret);
}
}
}.execute(imageUrl);
}
And then withing your adapter you should call the loadRemoteImage(imageUri)
I also suggest you combine this with a bitmap cache so as to acccelerate the process of fetching the image and the addition of placeholders :)
Using my Custom Adapter - I am populating the listiew using an AsyncTask. The doInBackground updates the ArrayLists which are used for the Custom Adapter. The onProgressUpdate calls the adapter.notifyDataSetChanged();
When loading a lot of files, I wanted the UI to be responsive, but when you try to scroll when the list is still being populated, I get this error:
java.lang.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. Make sure your adapter calls notifyDataSetChanged() when its content changes.
#Override
protected Boolean doInBackground(DbxFileSystem... params) {
//Opens thumbnails for each image contained in the dropbox folder
try {
DbxFileSystem fileSystem = params[0];
numFiles = fileSystem.listFolder(currentPath).size();
for (DbxFileInfo fileInfo: fileSystem.listFolder(currentPath)) {
String filename = fileInfo.path.getName();
try{
if(!fileInfo.isFolder)
{
Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
pix.add(image);
paths.add(fileInfo.path);
publishProgress(1); //use this to update the ListView
}
else
{
//must be a folder if it has no thumb, so add folder icon
Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.dbfolder);
pix.add(image);
paths.add(fileInfo.path);
publishProgress(1);
}
catch(Exception e)
{
e.printStackTrace();
}
System.gc();
}
}
catch (Exception e) {
e.printStackTrace();
return false;
} finally {
}
return true;
}
#Override
protected void onProgressUpdate(Integer...progress) {
if(pix.size()==1) // //not ideal but works for now, only bind the adapter if its the first time we have looped through.
{
adapter = new ImageAdapter(getApplicationContext(), pix, paths, numFiles);
lstView.setAdapter(adapter);
}
adapter.notifyDataSetChanged();
lstView.requestLayout();
super.onProgressUpdate(progress);
}
Can anyone see what the problem is here? And what can I do to prevent it?
I was originally using a progressbar and only displayed the fill contents once they had all loaded, but I would much rather show the incremental load and let the user scroll even as its loading the content.
p.s. I see this is a common enough issue and have read several similar questions, but I still cannot work out what I need to change.
Your adapter has pix, numFiles and paths as data source and since you are modifying those collections in doInBackground() which is running on a non UI thread you get this exception.
new ImageAdapter(getApplicationContext(), pix, paths, numFiles); passes those collection via reference.
I have ListView with EventAdapter.
public class EventAdapter extends BaseAdapter {
...
public View getView(int position, View view, ViewGroup parent) {
...
cache.imgIcon.setImageDrawable(ImgCache.setImg(url, progressBar));
...
}
ImgCache its class for caching images.
public class ImgCache {
public static HashMap<String, Drawable> imgCache;
// get img from cache if exist, or download and put in cache
public static Drawable setImg(final String link, final ProgressBar progressBar) {
final Drawable[] image = {null};
if (imgCache.containsKey(link)) {
image[0] = imgCache.get(link);
progressBar.setVisibility(View.INVISIBLE);
} else {
new AsyncTask<Void, Void, Drawable>() {
#Override
protected Drawable doInBackground(Void... params) {
URL url = null;
try {
url = new URL(link);
URLConnection connection = url.openConnection();
image[0] = Drawable.createFromStream(connection.getInputStream(), "src");
} catch (Exception e) {
e.printStackTrace();
}
imgCache.put(link, image[0]);
return image[0];
}
#Override
protected void onPostExecute(Drawable result) {
progressBar.setVisibility(View.INVISIBLE);
}
}.execute();
}
return image[0];
}
}
What the problem is?
After I open my Activity with ListView all images begin loading. But after the loading is finished they don't displayed. It is looks like:
Then I try to scroll 2 items down and then return to previous position. After this manipulation I can see 2 upper items with images. Also all images down are also visible when I scroll to them.
According to your problem, it seems like you need to refresh your ListView after the images has been downloaded (because when you scroll they do appear):
adapter.notifyDataSetChanged();
AsyncTask is asynchronous so the flow for your app is:
ListView Item needs to be displayed -> Calls Adapter.getView(...) for List item -> if image is not in cache, execute AsyncTask and return (not waiting for result)
So, when you scroll down and back up, the Adapter.get(...) method is called again, however this time the image is in cache so it returns the Drawable object which is displayed
One way to resolve this issue would be to have a callback to the Adapter from the AsyncTask that will update the image once it is retrieved calling notifyDataSetChanged on the Adapter, setting specific Drawable directly or something similar (display a loading gif for images in the meanwhile?)
Or
Call the AsyncTask get(long timeout, TimeUnit unit) method which will block the man thread and wait for the AsyncTask to finish. After it is finished then it will return the result (your Drawable in this case). This will cause the main UI thread to hang while fetching images, so not optimal way to go about this.
The issue is that your view loads and populates your list OnCreate, but at that time your Async task hasn't returned your list yet so when getView calls your cache it's empty, due to android View Recycling when you scroll it calls getView again, this time your cache has been populated.
I recommend that onPostExecute you call NotifyDataSetChanged on your ListView adapter, this will force a redraw once your have your images.
Iv'e got an Android app that is using a list activity to display a list of items pulled from the internet. I First use an AsyncTask to load the list and that Async task finishes it calls a different async task to start loading the thumbnail pictures that go along with the list items. The problem I am having is that the user has access to a refresh button that they can press at any time and when it is pressed, the whole list of items is delete and the loading starts over. The Async task that loads the thumbnails could potentially still be running if this happens and may try to add a thumbnail to a non existing list item then. Iv'e tried synchronizing on the list, using a Boolean which after researching I realized would not work. I have also tried using a static atomic boolean to check if refresh has been hit to cancel the thumbnail loader. Any ideas?
public class LoadItems extends AsyncTask<Void, Void, Boolean> {
private Activity activity;
private static boolean loading = false;
public static final AtomicBoolean refreshing = new AtomicBoolean(false);
private static final String TAG = "LoadItems";
private int start;
private List<ListItem> items;
public LoadItems(Activity activity) {
this.activity = activity;
}
#Override
protected void onPreExecute() {
loading = true;
start = ItemViewer.itemList.size();
}
#Override
protected Boolean doInBackground(Void... arg0) {
items = WebFunctions.getMoreItems(activity);
return (items != null);
}
protected void onPostExecute(Boolean success) {
if (success) {
for (ListItem item: items) {
ItemViewer.itemList.add(item);
Log.d(TAG, "added item " + item.getTitle());
}
LoadThumbnails thumbnailLoader = new LoadThumbnails();
thumbnailLoader.execute(start, ItemViewer.itemList.size());
}
loading = false;
}
public void protectedExecute() {
if (!loading)
execute();
}
public void refresh() {
if (!refreshing.getAndSet(true)) {
WebFunctions.reset();
ItemViewer.itemList.removeAllItems();
execute();
}
}
}
public class LoadThumbnails extends AsyncTask<Integer, Void, Drawable> {
private int position;
private int end;
#Override
protected Drawable doInBackground(Integer... params) {
position = params[0];
end = params[1];
Drawable thumbnail = null;
synchronized(ItemViewer.itemList) {
if (LoadItems.refreshing.get())
cancel(true);
String url = ItemViewer.itemList.get(position).getThumbnailUrl();
if (!url.isEmpty())
thumbnail = WebFunctions.loadDrawableFromUrl(ItemViewer.activity, url);
}
return thumbnail;
}
protected void onPostExecute(Drawable d) {
synchronized (ItemViewer.itemList) {
if (LoadItems.refreshing.get())
cancel(true);
if (d != null)
ItemViewer.itemList.setThumbnail(position, d);
position++;
if (position < end) {
LoadThumbnails lt = new LoadThumbnails();
lt.execute(position, end);
}
}
}
}
This is pretty simple to solve. Whenever the user hits the refresh button, make sure you call cancel() on the last async tasks you have created before you create new tasks. For example,
private void onRefreshClick(View v) {
if(mLastLoadItemTask != null) mLastLoadItemTask.cancel(true);
if(mLastLoadThumbnailTask != null) mLastLoadThumbnailTask.cancel(true);
mLastLoadItemTask = new LoadItems(...);
mLastLoadItemTask.execute();
}
Then, in the onPostExecute of each of your async tasks, first check to see if they were cancelled by calling isCancelled(). If they were cancelled, make sure the onPostExecute method does no work by just returning. For example,
protected void onPostExecute(...) {
if(isCancelled()) return;
//Adding items to list
//Or start load thumbnail task
}
As you can see that should prevent any unintentional or stale updates because the onPostExecute methods and your cancel calls will all happen on the main therad. The last thing I would suggest is to alter your loadThumbs task to be able to stop doing work as soon as possibly by checking isCancelled() whenever it makes sense to do so.
The following steps might help:
cache the results, whatever you have previously pulled from the net should be saved and quickly restored back when your application is launched. this way you avoid long delays and empty screens on application startup, which, in turn, stops the user from pressing 'reload'
make a boolean variable reload_in_progress, set it to true when you start pulling data from the net, and set it to false when all thumbnails are ready. 'reload' click handler should ignore clicks when reload_in_progress is true.
show some king of progress bar to the user, so (s)he knows it's already reloading and does not push reload again.
almost forgot, never update data shown to the user "live", this leads to wonderful situations, when the user clicks on item while it's changing and doing something completely different from what (s)he expected. long updates should keep its data to themselves and quickly swap old data for the new one only when everything is ready.
First the problem:
I'm working on the application that uses multiple FragmentLists
within a customized FragmentStatePagerAdapter. There could be,
potentially substantial number of such fragments say between 20 and 40.
Each fragment is a list in which each item could contain text or image.
The images need to be uploaded asynchronously from the web and cached to temp memory cache and also to SD if available
When Fragment goes off the screen any uploads and current activity should be cancelled (not paused)
My first implementation followed well known image loader code from Google. My problem with that code is that it basically creates one instance of AsyncTask per image. Which in my case kills the app real fast.
Since I'm using v4 compatibility package I thought that using custom Loader that extends AsyncTaskLoader would help me since that internally implements a thread pool. However to my unpleasant surprise if I execute this code multiple times each following invocation will interrupt the previous. Say I have this in my ListView#getView method:
getSupportLoaderManager().restartLoader(0, args, listener);
This method is executed in the loop for each list item that comes into view. And as I stated - each following invocation will terminate the previous one. Or at least that's what happen based on LogCat
11-03 13:33:34.910: V/LoaderManager(14313): restartLoader in LoaderManager: args=Bundle[{URL=http://blah-blah/pm.png}]
11-03 13:33:34.920: V/LoaderManager(14313): Removing pending loader: LoaderInfo{405d44c0 #2147483647 : ImageLoader{405118a8}}
11-03 13:33:34.920: V/LoaderManager(14313): Destroying: LoaderInfo{405d44c0 #2147483647 : ImageLoader{405118a8}}
11-03 13:33:34.920: V/LoaderManager(14313): Enqueuing as new pending loader
Then I thought that maybe giving unique id to each loader will help the matters but it doesn't seem to make any difference. As result I end up with seemingly random images and the app never loads even 1/4 of what I need.
The Question
What would be the way to fix the Loader to do what I want (and is there a way?)
If not what is a good way to create AsyncTask pool and is there perhaps working implementation of it?
To give you idea of the code here's stripped down version of Loader where actual download/save logic is in separate ImageManager class.
public class ImageLoader extends AsyncTaskLoader<TaggedDrawable> {
private static final String TAG = ImageLoader.class.getName();
/** Wrapper around BitmapDrawable that adds String field to id the drawable */
TaggedDrawable img;
private final String url;
private final File cacheDir;
private final HttpClient client;
/**
* #param context
*/
public ImageLoader(final Context context, final String url, final File cacheDir, final HttpClient client) {
super(context);
this.url = url;
this.cacheDir = cacheDir;
this.client = client;
}
#Override
public TaggedDrawable loadInBackground() {
Bitmap b = null;
// first attempt to load file from SD
final File f = new File(this.cacheDir, ImageManager.getNameFromUrl(url));
if (f.exists()) {
b = BitmapFactory.decodeFile(f.getPath());
} else {
b = ImageManager.downloadBitmap(url, client);
if (b != null) {
ImageManager.saveToSD(url, cacheDir, b);
}
}
return new TaggedDrawable(url, b);
}
#Override
protected void onStartLoading() {
if (this.img != null) {
// If we currently have a result available, deliver it immediately.
deliverResult(this.img);
} else {
forceLoad();
}
}
#Override
public void deliverResult(final TaggedDrawable img) {
this.img = img;
if (isStarted()) {
// If the Loader is currently started, we can immediately deliver its results.
super.deliverResult(img);
}
}
#Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
#Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
// At this point we can release the resources associated with 'apps'
// if needed.
if (this.img != null) {
this.img = null;
}
}
}
Ok, so first things first. The AsyncTask that comes with android shouldn't drown out your app or cause it to crash. AsyncTasks run in a thread pool where there is at most 5 threads actually executing at the same time. While you can queue up many tasks to be executed , only 5 of them are executing at a time. By executing these in the background threadpool they shouldn't have any effect on your app at all, they should just run smoothly.
Using the AsyncTaskLoader would not solve your problem if you are unhappy with the AsyncTask loader performance. The AsyncTaskLoader just takes the loader interface and marries it to an AsyncTask. So it's essentially mapping onLoadFinished -> onPostExecute, onStart -> onLoadInBackground. So it's the same exact thing.
We use the same image loader code for our app that causes an asynctask to be put onto the threadpool queue each time that we try to load an image. In google's example they associate the imageview with its async task so that they can cancel the async task if they try to reuse the imageview in some sort of adapter. You should take a similar strategy here. You should associate your imageview with the async task is loading the image in the background. When you have a fragment that is not showing you can then cycle through your image views associated with that fragment and cancel the loading tasks. Simply using the AsyncTask.cancel() should work well enough.
You should also try to implement the simple image caching mechanism the async image view example spells out. We simply create a static hashmap that goes from url -> weakreference . This way the images can be recycled when they need to be because they are only held on with a weak reference.
Here's an outline of the image loading that we do
public class LazyLoadImageView extends ImageView {
public WeakReference<ImageFetchTask> getTask() {
return task;
}
public void setTask(ImageFetchTask task) {
this.task = new WeakReference<ImageFetchTask>(task);
}
private WeakReference<ImageFetchTask> task;
public void loadImage(String url, boolean useCache, Drawable loadingDrawable){
BitmapDrawable cachedDrawable = ThumbnailImageCache.getCachedImage(url);
if(cachedDrawable != null){
setImageDrawable(cachedDrawable);
cancelDownload(url);
return;
}
setImageDrawable(loadingDrawable);
if(url == null){
makeDownloadStop();
return;
}
if(cancelDownload(url)){
ImageFetchTask task = new ImageFetchTask(this,useCache);
this.task = new WeakReference<ImageFetchTask>(task);
task.setUrl(url);
task.execute();
}
......
public boolean cancelDownload(String url){
if(task != null && task.get() != null){
ImageFetchTask fetchTask = task.get();
String downloadUrl = fetchTask.getUrl();
if((downloadUrl == null) || !downloadUrl.equals(url)){
fetchTask.cancel(true);
return true;
} else
return false;
}
return true;
}
}
So just rotate through your image views that are in your fragment and then cancel them when your fragment hides and show them when your fragment is visible.