Animation / App Widget / Lockscreen / RemoteViews

TextClock Version 2 – Part 4

In the previous article we updated our layouts and animations to use ViewFlipper ready for our animations. In this article we’ll get this hooked up so that the time transition now animate on both the app and lock screen widgets.

Performing the animations via RemoteViews is actually quite tricky and I make absolutely no apologies if the code to do this appears a little hacky. When we our IntentService gets woken up by an Alarm event, we need to determine which of the fields have changed and only animate those fields. Normally this would be pretty straightforward because we could simply compare the current text for that field with getText() of the TextView, and only perform the animation when the value changes. Unfortunately, RemoteViews does not permit us access to getText() on our TextView, in fact it does not permit us to view the state of any of the View objects in the widget layout.

We therefore need to keep track of the current text within each field ourselves and use that to perform the comparisons. Because we’re using an IntentService, we cannot hold these values within the Service itself because it will die once the time update is complete and a new instance will be created on the next update. Therefore we’ll need to persist this data, and we’ll use private SharedSettings to do that:

private void updateTime(Calendar date) {
	Log.d(TAG, "Update: " + 
		DATE_FORMAT.format(date.getTime()));
	SharedPreferences prefs = 
		getSharedPreferences(PREFS, MODE_PRIVATE);

	String[] words = TimeToWords.timeToWords(date);

	String[] last = new String[3];
	last[0] = prefs.getString(PREFS_HOURS, "");
	last[1] = prefs.getString(PREFS_TENS, "");
	last[2] = prefs.getString(PREFS_MINUTES, "");
	boolean changed = false;

	AppWidgetManager manager = 
		AppWidgetManager.getInstance(this);
	if (manager != null) {
		ComponentName name = new ComponentName(this, 
			TextClockAppWidget.class);
		int[] appIds = manager.getAppWidgetIds(name);
		for (int id : appIds) {
			int layoutId = R.layout.appwidget;
			if (Build.VERSION.SDK_INT >= 
				Build.VERSION_CODES.JELLY_BEAN) {
				if (getAppWidgetCategory(manager, id) == 
					WIDGET_CATEGORY_KEYGUARD) {
					layoutId = R.layout.keyguard;
				}
			}
			RemoteViews v = 
				new RemoteViews(getPackageName(), 
				layoutId);
			changed |= updateTime(words, v, last);
			manager.updateAppWidget(id, v);
		}
	}
	if (changed) {
		SharedPreferences.Editor editor = prefs.edit();
		editor.clear();
		editor.putString(PREFS_HOURS, words[0]);
		if(words.length > 1) {
			editor.putString(PREFS_TENS, words[1]);
		}
		if(words.length > 2) {
			editor.putString(PREFS_MINUTES, words[2]);
		}
		if (Build.VERSION.SDK_INT >= 
			Build.VERSION_CODES.GINGERBREAD) {
			editor.apply();
		} else {
			editor.commit();
		}
	}
}

Hopefully this should be pretty straightforward: We read the last hours, tens, and minutes strings from SharedPreferences. We call updateTime() providing the current values for these, the RemoteViews object that we’re updating, and the last values that we read from SharedPreferences. The return value from this method indicates whether there has been a delta from the last values to the current values.

Once we have completed updating the widget instances, we check wether a delta has occurred. If so, we write the current values to SharedPreferences.

The updateTime() method calls the update() method for each of the fields passing in the appropriate IDs for the appropriate ViewFlipper, and its two child TextViews which will be used to perform the animation. Once again the update() method returns an indication of whether there is a delta between the current and last values which updateTime() aggregates to provide its own return value:

private boolean updateTime(String[] words, 
	RemoteViews views, String[] last) {

	boolean changed;
	changed = update(views, R.id.hoursFlipper,
			R.id.hours, R.id.hoursNext,
			last[0], words[0]);
	changed |= update(views, R.id.tensFlipper,
		R.id.tens, R.id.tensNext,
		last[1],
		words.length > 1 ? words[1] : "");
	changed |= update(views, R.id.minutesFlipper,
		R.id.minutes, R.id.minutesNext,
		last[2],
		words.length > 2 ? words[2] : "");
	return changed;
}

The update() method performs an OS version check because on of the RemoteViews methods that we require was introduced in API 12, so we’ll only support animation where we can. The widget will still on older devices, but the animations will only work on devices of API 12 and later. The return value is a comparison on the current and new values to return whether there is a delta:

private boolean update(RemoteViews views, 
	int flipper, int current, int next, 
	String curVal, String newVal) {
	if (Build.VERSION.SDK_INT >= 
		Build.VERSION_CODES.HONEYCOMB_MR1) {
		animate(views, flipper, 
			current, next, curVal, newVal);
	} else {
		views.setTextViewText(R.id.hours, 
			newVal);
	}
	return !curVal.equals(newVal);
}

Finally, the animate() method is annotated with @TargetApi to enable us to compile this for backward compatibility. It sets the values of both of the child TextViews to their appropriate values. If there is a delta, it sets the current displayed child to the first, and then immediately calls showNext() to trigger the animation on the ViewFlipper.

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
private void animate(RemoteViews views, 
	int flipper, int current, int next, 
	String curVal, String newVal) {

	views.setTextViewText(current, curVal);
	views.setTextViewText(next, newVal);
	if (!curVal.equals(newVal)) {
		views.setDisplayedChild(flipper, 0);
		views.showNext(flipper);
	}
}

All done! The animations will now work on all API 12 and later devices:

Now that we have our animations working, but there is a problem with our widgets. The more observant may have spotted that the time transitions do not appear to be in sync with the system clock. Sometimes the widget is up to 40 seconds slower than the system time in the status bar. In the next article we’ll investigate why this is happening.

The source code for this article can be found here, and TextClock is available from Google Play. Version 2.0.2 has recently been published which contains the changes in this article, but there will be a number of minor updates over the coming weeks as we add additional functionality to the app.

© 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.

2 Comments

  1. I get this error when I try to compile your code:

    error: Error: No resource found that matches the given name (at ‘inAnimation’ with value ‘@anim/

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.