Developer

Pro tip: Build a better Android ImageView widget

After cutting and pasting the same code numerous times because of a caveat with the Android ImageView widget, William J. Francis came up with a down and dirty extension that solves the issue.

android-logo-generic-092413.jpg

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

anrandroiderror101414.png

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.authorwjf.smartimageview"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="19" />
    
    <uses-permission android:name="android.permission.INTERNET" /> 

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.authorwjf.smarterimageview.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

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

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:gravity="center" >

    <com.authorwjf.smarterimageview.SmarterImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:maxWidth="200dp"
        android:scaleType="centerInside"
        app:uri="http://goo.gl/G8nVYB"/>

</RelativeLayout>

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
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<declare-styleable name="SmarterImageView">
	    <attr name="uri" format="string"/>
	</declare-styleable>
</resources>

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
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
    <gradient android:startColor="#FFFFFF" android:endColor="#808080" android:angle="270"/>
    <stroke android:width="2dp" android:color="#50808080" />
    <size android:width="25dp" android:height="25dp" />
</shape>

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<Uri, Void, Bitmap> {
		WeakReference<ImageView> mRef;
		
		public ImageDownloader(ImageView bmImage) {
			mRef = new WeakReference<ImageView>(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

smarterimgandroidwidget2014.png

About William J. Francis

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

Editor's Picks

Free Newsletters, In your Inbox