One of the most commonly used widgets when developing an Android application is the ImageView. The widget provides an easy way to display an image within a layout with one caveat: the image is loaded on the UI thread. While this doesn’t make a lot of difference when you are referencing local resources, if the source for your image is somewhere in the cloud, setting the imageURI property could result in the dreaded ANR (Application Not Responding) (Figure A).

Figure A

Any experienced Android developer knows that the issue can be easily avoided by throwing the image download and processing onto an AsyncTask, but after cutting and pasting the same code about a bazillion times I decided to try and come up with a better way.

The result is a down and dirty extension of the native ImageView class that allows an image to be set via the imageURI attribute, programmatically or via XML. I hope you find it as useful as I do. Feel free follow along with the step-by-step tutorial below, or, download and import the entire project directly into Eclipse.

1. Create a new Android project in Eclipse. Target Android 4.0 (ICS) or higher.

2. Modify the manifest to include the internet permission.

AndroidManifest.xml







3. In the /res/layout folder, reference our custom widget.

activity_main.xml

4. Add the URI attribute to the /res/values/attrs.xml file. If there isn’t an attrs file in your project, create it.

attrs.xml





5. Define a drawable to use as a loading indicator while the remote image is downloading. This goes in the /res/drawable folder.

spinner.xml





6. With our layouts and resources in place, let’s create the SmarterImageView class within the /src folder. The guts of the class should look quite familiar — it’s that AsyncTask you’ve been writing over and over in your existing Android projects.

SmarterImageView.java
package com.authorwjf.smarterimageview;

import java.io.InputStream;
import java.lang.ref.WeakReference;

import com.authorwjf.smartimageview.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;

public class SmarterImageView extends ImageView {

private Uri mUri;

public SmarterImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a=getContext().obtainStyledAttributes(attrs, R.styleable.SmarterImageView);
String uriString = a.getString(R.styleable.SmarterImageView_uri);
a.recycle();
setImageURI(Uri.parse(uriString));
}

private void showSpinner() {
setImageResource(R.drawable.spinner);
RotateAnimation anim = new RotateAnimation(0.0f, 360.0f , Animation.RELATIVE_TO_SELF, .5f, Animation.RELATIVE_TO_SELF, .5f);
anim.setInterpolator(new LinearInterpolator());
anim.setRepeatCount(Animation.INFINITE);
anim.setDuration(3000);
setAnimation(anim);
startAnimation(anim);
}

@Override
public void setImageURI(Uri uri) {
mUri = uri;
showSpinner();
new ImageDownloader(((ImageView) this)).execute(mUri);
}

private static class ImageDownloader extends AsyncTask {
WeakReference mRef;

public ImageDownloader(ImageView bmImage) {
mRef = new WeakReference(bmImage);
}

protected Bitmap doInBackground(Uri... uris) {
String url = uris[0].toString();
Bitmap image = null;
InputStream in;
try {
in = new java.net.URL(url).openStream();
image = BitmapFactory.decodeStream(in);
} catch (Exception e) {
e.printStackTrace();
}
return image;
}

protected void onPostExecute(Bitmap result) {
if (mRef.get()!=null) {
mRef.get().clearAnimation();
mRef.get().setImageBitmap(result);
}
}
}

}

7. Since we are setting the remote image URI in the XML layout, the generated MainAcitvity.java file will serve our purposes just fine.

MainAcitvitiy.java
package com.authorwjf.smarterimageview;

import com.authorwjf.smartimageview.R;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}

That does it! The image URI specified in the layout is a shortened link to an image of the Android mascot hosted on TechRepublic (Figure B). Replace it with your own remote resource in the layout or at runtime via the setImageURI method to use the widget in your projects.

Figure B