Crossing things off lists in Android 0.9 SDK

Monday September 15, 2008 at 10:18 PM

A few months ago I wrote some code to let you “cross-off” things in a ListView. By wiping your finger left-to-right over an item it will add a strike-through effect to the text, and right-to-left would reverse the effect. Also, we’d like to store the crossed-off status in a backend database.

First, an overview of the problem. The ListView already captures clicks and long-touches for its items, and catches vertical scrolling and flinging to browse through the list. We’re interested in capturing any horizontal events while still letting normal touch events pass through to the ListView. The best way to handle this is by creating a wrapper View that will hold the ListView. Then we can use onInterceptTouchEvent() to watch for our cross-off actions, or otherwise ignore the touch events and let them trickle down to the ListView. (Thanks to Romain Guy for helping me find the correct way to capture these events.) Here’s the core of that capture code:

protected MotionEvent downStart = null;

public boolean onInterceptTouchEvent(MotionEvent event) {

	switch(event.getAction()) {
	case MotionEvent.ACTION_DOWN:
		// keep track of the starting down-event
		downStart = MotionEvent.obtain(event);
		break;
	case MotionEvent.ACTION_MOVE:
		// if moved horizontally more than slop*2, capture the event for ourselves
		float deltaX = event.getX() - downStart.getX();
		if(Math.abs(deltaX) > ViewConfiguration.getTouchSlop() * 2)
			return true;
		break;
	}

	// otherwise let the event slip through to children
	return false;
}

public boolean onTouchEvent(MotionEvent event) {

	// check if we crossed an item
	float targetWidth = this.getWidth() / 4;
	float deltaX = event.getX() - downStart.getX(),
		deltaY = event.getY() - downStart.getY();

	boolean movedAcross = (Math.abs(deltaX) > targetWidth);
	boolean steadyHand = (Math.abs(deltaX / deltaY) > 2);

	if(movedAcross && steadyHand) {
		boolean crossed = (deltaX > 0);

		// figure out which child view we crossed
		ListView list = (ListView)this.findViewById(android.R.id.list);
		int position = list.pointToPosition((int)downStart.getX(), (int)downStart.getY());

		// pass crossed event to any listeners
		for(OnCrossListener listener : listeners) {
			listener.onCross(position, crossed);
		}

		// and return true to consume this event
		return true;
	}

	return false;
}

Using this approach lets us capture these cross-off gestures while still letting the ListView behave normally. I’m using this technique in my CompareEverywhere application, but earlier today I wrote a quick TodoList app to show it in action.

There are two other cool things we’re doing in the process: using a ViewBinder to custom render the created time of each item, and a stateful drawable to handle the checkmarks shown on each item. The ViewBinder correctly sets the strike-through text effect based on the COL_CROSSED database column, and also shows a custom caption with a format similar to “4 hours ago” based on the COL_CREATED column.

public boolean setViewValue(View view, Cursor cursor, int columnIndex) {

	switch(view.getId()) {
	case android.R.id.content:
		// binding to parent container should set the crossed value
		ImageView icon = (ImageView)view.findViewById(android.R.id.icon);
		TextView text1 = (TextView)view.findViewById(android.R.id.text1),
			text2 = (TextView)view.findViewById(android.R.id.text2);

		// read crossed status and set text flags for strikethrough
		boolean crossed = Boolean.valueOf(cursor.getString(columnIndex));
		if(crossed) {
			icon.setImageState(new int[] { android.R.attr.state_checked }, true);
			text1.setPaintFlags(text1.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
			text2.setPaintFlags(text2.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
		} else {
			icon.setImageState(new int[] { }, true);
			text1.setPaintFlags(text1.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
			text2.setPaintFlags(text2.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
		}
		return true;

	case android.R.id.text2:
		// binding to second textview should format time nicely
		long created = cursor.getLong(columnIndex);
		long now = System.currentTimeMillis() / 1000;

		int minutes = (int)((now - created) / 60);
		String nice = view.getContext().getString(R.string.bind_minutes, minutes);
		if(minutes >= 60) {
			int hours = (minutes / 60);
			nice = view.getContext().getString(R.string.bind_hours, hours);
			if(hours >= 24) {
				int days = (hours / 24);
				nice = view.getContext().getString(R.string.bind_days, days);
			}
		}

		((TextView)view).setText(nice);

		return true;
	}

	// otherwise fall through to other binding methods
	return false;

}

And finally the code needed to connect the above ViewBinder to our SimpleCursorAdapter:

this.cross = (CrossView) this.findViewById(R.id.crossview);
this.list = (ListView) this.findViewById(android.R.id.list);

this.cross.addOnCrossListener(this);

// build adapter to show todo cursor
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.item_todo, cursor,
	new String[] { db.FIELD_LIST_TITLE, db.FIELD_LIST_CREATED, db.FIELD_LIST_CROSSED },
	new int[] { android.R.id.text1, android.R.id.text2, android.R.id.content });
adapter.setViewBinder(new CrossBinder());

list.setAdapter(adapter);

And it really is that simple. The SimpleCursorAdapter shows our todo list, and the ViewBinder handles showing COL_CREATED correctly, and assigning the overall crossed-off state based on COL_CROSSED.

There is some additional code in our Activity to handle onCross() events and update the database and ListView as needed. Finally I threw in a context menu to handle deleting and crossing-off items on phones without a touchscreen, and a normal menu for adding new items.

The last cool thing is the stateful drawable that I’m using to automatically change the icon based on crossed-off status and also on selection:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
	<item android:state_selected="true" android:state_checked="true"
		android:drawable="@drawable/indicator_check_mark_dark_invert" />
	<item android:state_checked="true"
		android:drawable="@drawable/indicator_check_mark_dark" />

	<item android:state_selected="true" android:drawable="@drawable/ic_text_dot_c_invert" />
	<item android:drawable="@drawable/ic_text_dot_c" />
</selector>

The ImageView automatically works its way down that list until it finds a drawable that matches all the state requirements, which makes it super easy to handle darkening icons when an item is selected. We also used ImageView.setImageState() earlier in our ViewBinder to correctly set the checked state.

If you’re interested, here’s the full Eclipse project source under a GPLv3 license, and also an APK ready to be installed. And here’s some video of the app in action:

Quick database row editor in Android 0.9 SDK

Thursday September 11, 2008 at 3:46 PM

For several Android projects I’ve needed a quick way of editing database rows without building an entire GUI. In the 0.9 SDK, we saw the introduction of the Preferences framework for storing simple application data, along with the PreferenceActivity family of classes for rapidly creating editable GUIs with almost zero effort. Wouldn’t it be awesome if we could edit database rows just as easily?

It is possible if you’re willing to do a little hacking. Essentially we’re providing a fake SharedPreferences up to a PreferenceActivity window. Instead of reading and saving from the application preferences, our SharedPreferences class will be using a specific database row for its storage. To make it all work, we then override the PreferenceActivity.getSharedPreferences() method to return our fake SharedPreferences instance.

Below is some example code of this approach being used in ConnectBot, an Android SSH client that I’ve been working on. First the host_prefs.xml file where we define the “preferences” that will be written to our database. Notice that I’m setting android:key to the database column names, which will make pairing the data up much easier later.

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
	<EditTextPreference
		android:key="nickname"
		android:title="Nickname"
		/>
	<ListPreference
		android:key="color"
		android:title="Color category"
		android:entries="@array/list_colors"
		android:entryValues="@array/list_colors"
		/>
	<CheckBoxPreference
		android:key="usekeys"
		android:title="Use SSH keys"
		/>
	<PreferenceCategory android:title="Connection settings">
		<EditTextPreference
			android:key="username"
			android:title="Username"
			/>
		<EditTextPreference
			android:key="hostname"
			android:title="Host"
			/>
		<EditTextPreference
			android:key="port"
			android:title="Port"
			/>
	</PreferenceCategory>
</PreferenceScreen>

Now let’s look at the code we’ll be using for our PreferenceActivity. Nothing too special except we are overriding the getSharedPreferences(), which is how the PreferenceActivity will get its data source. In this example we’re doing a quick hack by passing the exact _id into the onCreate() as Intent.EXTRA_TITLE. Another way to handle this would be to pass around ContentProvider URIs, which would help clean up this code in several places.

With that approach, the best way to create the SharedPreferences instance would be by passing the URI as the name string to the getSharedPreferences() call. We could easily set the name string our PreferenceActivity requests by calling getPreferenceManager().setSharedPreferencesName(uri.toString()).

public class HostEditor extends PreferenceActivity
	implements OnSharedPreferenceChangeListener {
	protected CursorPreferenceHack pref;

	@Override
	public SharedPreferences getSharedPreferences(String name, int mode) {
		Log.d(this.getClass().toString(), String.format("getSharedPreferences(name=%s)", name));
		return this.pref;
	}

	@Override
	public void onCreate(Bundle icicle) {
		super.onCreate(icicle);

		HostDatabase db = new HostDatabase(this);
		int id = this.getIntent().getIntExtra(Intent.EXTRA_TITLE, -1);

		// TODO: we could pass through a specific ContentProvider uri here
		//this.getPreferenceManager().setSharedPreferencesName(uri);

		this.pref = new CursorPreferenceHack(db.getWritableDatabase(), db.TABLE_HOSTS, id);
		this.pref.registerOnSharedPreferenceChangeListener(this);

		this.addPreferencesFromResource(R.xml.host_prefs);
		this.updateSummaries();
	}

	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
		// update values on changed preference
		this.updateSummaries();
	}

	protected void updateSummaries() {
		// for all text preferences, set summary as current database value
		for(String key : this.pref.values.keySet()) {
			Preference pref = this.findPreference(key);
			if(pref == null) continue;
			if(pref instanceof CheckBoxPreference) continue;
			pref.setSummary(this.pref.getString(key, ""));
		}
	}
}

And finally the CursorPreferenceHack source. It’s pretty simple, and just wraps the SharedPreferences calls as needed when converting them over into Cursor calls. The resulting GUI is easy to change, just remember to set your android:key values to database column names so they can be resolved correctly:

So there you have it, this approach allows you to make rapid GUI interfaces to safely update your database-backed information. It’s definitely a hack for now, but it makes these GUI interfaces a snap. I think the Preferences framework itself might use databases for its normal storage, so they might generalize this family of classes in the future.

Watch my blog using Google Reader: Add to Google

Valid XHTML and CSS