Animation

App Polish

Often we complete the functional aspect of an Android project, and when it comes to adding some sparkle to the UI to really make it appealing to the user inspiration can be a little lacking. In this article we’ll start of with a really functional but boring app, and turn it in to something much more exciting.

Let’s start by having a quick look at the app in question. It is a simple ScrollView containing a TextView, which contains some rather boring text:

polish

With an app which is functionally quite simple and uninspiring, it’s often difficult to think of ways to liven things up for the user.

First let’s consider our user. There have been numerous studies of user behaviour which have all concluded that users are both stupid and lazy. Therefore we can first look at how we can make our users’ lives easier. The only real interaction that the user has with our app is they are required to scroll down in order to read the content. That seems like something that we can improve upon.

We can create an animation which will gradually scroll the text to the bottom. This will reduce the need for the user to even interact with our app. Perfect!

Attaching an animation is really quite easy, we just need to animate the scrollY property on our ScrollView, but we need to know the height of the TextView within the ScrollView first, so we use an OnGlobalLayoutListener to start the animation just before the first frame is drawn following a layout change (this technique has already been covered in this post):

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	mScroll = (ScrollView) findViewById(R.id.scroll);
	mText = (TextView) findViewById(R.id.text);
	mContent = findViewById(R.id.content);
	mImage = (ImageView) findViewById(R.id.image);
	final ViewTreeObserver vto = mText.getViewTreeObserver();
	if (vto != null) {
		vto.addOnGlobalLayoutListener(
			new ViewTreeObserver.OnGlobalLayoutListener() {
			@Override
			public void onGlobalLayout() {
				ViewTreeObserver vto = 
					mText.getViewTreeObserver();
				if (vto != null) {
					vto.removeOnGlobalLayoutListener(this);
				}
				Animator anim = ObjectAnimator.ofInt(mScroll, 
					"scrollY", 0, 
					mContent.getBottom());
				anim.setDuration(30000);
				anim.setInterpolator(
					new LinearInterpolator());
				anim.start();
			}
		});
	}
}

One question here is the duration of the animation. Surely different users read at different speeds? Of course they do, so I performed some user testing in my local bar. As a result of consuming large quantities of beer whilst performing this user testing, I came to the conclusion that the testing was pointless, and that 30 seconds seemed about right. (There have been numerous studies of developer behaviour which have all concluded that developers can be as lazy as users).

So, we’ve removed the need to the user to have to actually scroll the text, but how about rewarding them for actually getting to the end? How about an animated GIF? Everyone loves animated GIFs, don’t they?

Unfortunately animated GIFs are not supported natively in Android, but Johannes Borchardt wrote some great tutorials on different techniques for supporting animated GIFs in Android. So rather than re-invent the wheel, I’ll adopt one of his methods (I should repeat the quote about developers being lazy here, but I can’t be bothered).

I have elected to use Johannes’ GifDecoder approach (please read his posts if you require an explanation of the workings – developers are lazy, blah, blah, blah…), and we now add some additional code to our Activity class to load and the individual frames and set them on an ImageView:

final Runnable mUpdateResults = new Runnable() {
	public void run() {
		if (mTmpBitmap != null && 
			!mTmpBitmap.isRecycled()) {
			mImage.setImageBitmap(mTmpBitmap);
		}
	}
};

@Override
protected void onPause() {
	stopRendering();
	super.onPause();
}

@Override
protected void onResume() {
	super.onResume();
	InputStream is = null;
	try {
		is = getAssets().open("animation.gif");
		playGif(is);
	} catch (IOException e) {
		Log.e(TAG, "Error loading animated GIF", e);
	} finally {
		if (is != null) {
			try {
				is.close();
			} catch (Exception e) {
				Log.w(TAG, "Error closing stream", e);
			}
		}
	}
}

private void playGif(InputStream stream) {
	mGifDecoder = new GifDecoder();
	mGifDecoder.read(stream);

	mIsPlayingGif = true;

	new Thread(new Runnable() {
		public void run() {
			final int n = mGifDecoder.getFrameCount();
			final int ntimes = mGifDecoder.getLoopCount();
			int repetitionCounter = 0;
			do {
				for (int i = 0; i < n; i++) {
					mTmpBitmap = mGifDecoder.getFrame(i);
					int t = mGifDecoder.getDelay(i);
					mHandler.post(mUpdateResults);
					try {
						Thread.sleep(t);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				if (ntimes != 0) {
					repetitionCounter++;
				}
			} while (mIsPlayingGif && 
				(repetitionCounter <= ntimes));
		}
	}).start();
}

public void stopRendering() {
	mIsPlayingGif = false;
}

So, we now have our animated reward in place for the user. We can now see the full user experience:

wow

such improve

Important note: If you have any comments or questions regarding some of the design decisions in this article, I would kindly refer you to the publication date (below) of this article which may explain things.

The source code for this article can be found here.

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

4 Comments

  1. Animator anim = ObjectAnimator.ofInt(mScroll, “scrollY”, 0, Content.getBottom()/2);
    anim.setDuration(15000);
    anim.setStartDelay(3000);

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.