I have a list adapter where the images load with a rotating progress animation until they are fetched from the net. When I scroll down in the listView, it appears that the animation spins a previous image…this is a Very Weird and undesirable effect. The only thing that should be spinning is the progress animation.
I use a class called ImageLoader to asynchronously netfetch the images:
final int stub_id=R.drawable.progress;
public void DisplayImage(String url, Activity activity, ImageView imageView,int size)
{
//Log.d("ImageLoader" , url);
this.size=size;
if(cache.containsKey(url)) {
//If this works, then why do the OLD images still spin??
imageView.clearAnimation();
//imageView.setBackgroundDrawable(null);
//imageView.setImageDrawable(null);
imageView.setImageBitmap(cache.get(url));
}
else
{
rotation = AnimationUtils.loadAnimation(activity, R.drawable.progress);
rotation.setRepeatCount(Animation.INFINITE);
imageView.startAnimation(rotation);
queuePhoto(url, activity, imageView);
//imageView.setImageResource(stub_id);
//imageView.setBackgroundResource(stub_id);
}
Resources r = activity.getResources();
int dip = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, r.getDisplayMetrics());
LayoutParams layoutParams = imageView.getLayoutParams();
layoutParams.height = dip;
layoutParams.width = dip;
imageView.setLayoutParams(layoutParams);
}
Now you might ask, what does queuePhoto do? This is the ONLY other place where the animation is started.
//Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
ImageView imageView;
public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
public void run()
{
if(bitmap!=null) {
imageView.clearAnimation();
imageView.setImageBitmap(bitmap);
}
else {
imageView.clearAnimation();
rotation = AnimationUtils.loadAnimation(context, R.drawable.progress);
rotation.setRepeatCount(Animation.INFINITE);
imageView.startAnimation(rotation);
//imageView.setImageResource(stub_id);
}
}
}
And now part of my list adapter’s getView:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if( convertView == null ){
vi = inflater.inflate(R.layout.trending_item, null);
holder=new ViewHolder();
holder.img1 = (ImageView) vi.findViewById(R.id.t_item_1);
holder.img2 = (ImageView) vi.findViewById(R.id.t_item_2);
holder.img3 = (ImageView) vi.findViewById(R.id.t_item_3);
vi.setTag(holder);
} else {
holder=(ViewHolder)vi.getTag();
}
JSONObject t1= ts.get(1);
if(t1 !=null) {
holder.img1.setTag(t1.getString("image_small"));
imageLoader.DisplayImage(t1.getString("image_small"), act, holder.img1,IMAGE_SIZE);
holder.img1.setTag(TAG_T t1.getString("id"));
holder.img1.setOnClickListener(ocl);
} else {
holder.img1.setImageDrawable(null);
holder.img1.setBackgroundDrawable(null);
holder.img1.setTag(null);
holder.img1.setTag(TAG_T, null);
}
UPDATE
I implemented JBM’s proposed solution, but I was not able to make it work, he recommends running on the UI thread.
public class RemoteImageView extends ImageView implements RemoteLoadListener {
final int stub_id=R.drawable.progress;
int size;
private HashMap<String, Bitmap> cache=new HashMap<String, Bitmap>();
Animation rotation;
private File cacheDir;
public RemoteImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public RemoteImageView(Context context, AttributeSet attrs) {
super(context, attrs);
//Make the background thead low priority. This way it will not affect the UI performance
photoLoaderThread.setPriority(Thread.NORM_PRIORITY-1);
//Find the dir to save cached images
if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),context.getString(R.string.app_name));
else
cacheDir=context.getCacheDir();
if(!cacheDir.exists())
cacheDir.mkdirs();
}
public RemoteImageView(Context context) {
super(context);
}
public void displayRemoteImage(String url, Activity activity, int size)
{
this.myUrl = url;
this.size=size;
Resources r = activity.getResources();
int dip = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, r.getDisplayMetrics());
LayoutParams layoutParams = this.getLayoutParams();
layoutParams.height = dip;
layoutParams.width = dip;
this.setLayoutParams(layoutParams);
if(cache.containsKey(url)) {
this.clearAnimation();
setImageBitmap(cache.get(url));
}
else
{
rotation = AnimationUtils.loadAnimation(activity, R.drawable.progress);
rotation.setRepeatCount(Animation.INFINITE);
this.startAnimation(rotation);
queuePhoto(url, activity, this);
}
}
@Override
public void onLoadSuccess(String url, Bitmap bmp) {
if (url.equals(myUrl)) {
setImageBitmap(bmp);
} else {
/* the arrived bitmap is stale. do nothing. */
}
}
@Override
public void onLoadFail(String url) {
if (url.equals(myUrl)) {
setImageBitmap(((BitmapDrawable)getResources().getDrawable(stub_id)).getBitmap());
} else {
/* the failed bitmap is stale. do nothing. */
}
}
String myUrl;
private void queuePhoto(String url, Activity activity, ImageView imageView)
{
//This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them.
photosQueue.Clean(imageView);
PhotoToLoad p=new PhotoToLoad(url, imageView);
synchronized(photosQueue.photosToLoad){
photosQueue.photosToLoad.push(p);
photosQueue.photosToLoad.notifyAll();
}
//start thread if it's not started yet
if(photoLoaderThread.getState()==Thread.State.NEW)
photoLoaderThread.start();
}
PhotosQueue photosQueue=new PhotosQueue();
public void stopThread()
{
photoLoaderThread.interrupt();
}
//stores list of photos to download
class PhotosQueue
{
private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>();
//removes all instances of this ImageView
public void Clean(ImageView image)
{
for(int j=0 ;j<photosToLoad.size();){
if(photosToLoad.get(j).imageView==image)
photosToLoad.remove(j);
else
++j;
}
}
}
//Task for the queue
private class PhotoToLoad
{
public String url;
public ImageView imageView;
public PhotoToLoad(String u, ImageView i){
url=u;
imageView=i;
}
}
class PhotosLoader extends Thread {
public void run() {
try {
while(true)
{
//thread waits until there are any images to load in the queue
if(photosQueue.photosToLoad.size()==0)
synchronized(photosQueue.photosToLoad){
photosQueue.photosToLoad.wait();
}
if(photosQueue.photosToLoad.size()!=0)
{
PhotoToLoad photoToLoad;
synchronized(photosQueue.photosToLoad){
photoToLoad=photosQueue.photosToLoad.pop();
}
Bitmap bmp=getBitmap(photoToLoad.url);
cache.put(photoToLoad.url, bmp);
Object tag=photoToLoad.imageView.getTag();
if(tag!=null && ((String)tag).equals(photoToLoad.url)){
BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
Activity a=(Activity)photoToLoad.imageView.getContext();
a.runOnUiThread(bd);
}
}
if(Thread.interrupted())
break;
}
} catch (InterruptedException e) {
//allow thread to exit
}
}
}
PhotosLoader photoLoaderThread=new PhotosLoader();
//Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable
{
Bitmap bitmap;
ImageView imageView;
public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
public void run()
{
if(bitmap!=null) {
imageView.clearAnimation();
imageView.setImageBitmap(bitmap);
}
else {
imageView.clearAnimation();
imageView.setImageResource(stub_id);
}
}
}
private Bitmap getBitmap(String url)
{
System.out.println("GET: " +url);
if(url== null) {
return null;
}
//I identify images by hashcode. Not a perfect solution, good for the demo.
String filename=String.valueOf(url.hashCode());
File f=new File(cacheDir, filename);
//from SD cache
Bitmap b = decodeFile(f);
if(b!=null)
return b;
//from web
try {
Bitmap bitmap=null;
InputStream is=new URL(url).openStream();
OutputStream os = new FileOutputStream(f);
Utils.CopyStream(is, os);
os.close();
bitmap = decodeFile(f);
return bitmap;
} catch (Exception ex){
ex.printStackTrace();
return null;
}
}
//decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f){
try {
//decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new FileInputStream(f),null,o);
//Find the correct scale value. It should be the power of 2.
int width_tmp=o.outWidth, height_tmp=o.outHeight;
int scale=1;
while(true){
if(width_tmp/2<size || height_tmp/2<size)
break;
width_tmp/=2;
height_tmp/=2;
scale*=2;
}
//decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize=scale;
return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
} catch (FileNotFoundException e) {
Log.d("ImageLoader",e.getMessage(),e);
}
return null;
}
}
The problem here is race between
ImageViewand yourDisplayImagefunction. As the list is scrolling up or down the rows get reused, so when a remote image arrives it often happens that its target view already belongs to a different row. The only reliable way of doing that is making you own classRemoteImageView extends ImageViewwhich handles fetching the image internally. Then RemoteImageView can do proper synchronization.Let your
queuePhotomethod take aRemoteLoadListenerinstead of ImageView, like this:Then make your
RemoteImageViewaRemoteLoadListenerand do all the stuff internally:So in your
getViewmethod you simply do this:UPDATE
Try first reducing complexity of the system. I’ve removed your photo queue and cache and replaced it with downloading the image from the web (this piece is based on your code). I didn’t run this code, I just typed it. But it should give you a clear idea.
PS: Note the
try - finallyblock when working with streams.