Archive

Archive for the ‘ui’ Category

Another fast scroller helper for ListView

November 22, 2011 1 comment

Android’s ListView has a built-in way to implement fast (indexed) scrolling.

As useful as it is, it has two limitations that sometimes can be critical:

- A section name is assumed to be a single letter.
- If the list content changes over time, it’s difficult to update the section list on the go.

The fast scroller helper class in the framework is closely integrated with AbsListView, and so it does not seem possible to just replace it with another implementation, or change the way it behaves.

I needed one of these for my current project, and found a fast scroller that was posted on Google Code by Android’s Dianne Hackborn.

This code is from 2008, before there was a fast scroller implementation in the framework, and so its way of interacting with the list is different. This class is derived from FrameLayout and should be used in the layout XML file as the list view’s parent.

I combined this code with the fast scroller from the Ice Cream Sandwich sources. The result looks like this:

This class is used the same way as the FastScrollView linked to above – it should be inserted into the layout XML as the list view’s parent.

It has a method for updating the section index, which can also optionally reset the current drag tracking state (or not, specified as a parameter).

The section overlay’s size and its font size are specified as dimension resources. Long (over 1 character) section names are displayed correctly.

Finally, I also fixed some rendering issues on Android 3.0 when running with hardware acceleration enabled.

The code is too large to post here, so I uploaded it to Google Code. A sample / test project is included.

You can get it here: a wiki page is here and the code is here.

Categories: android, ui

AlertDialog, custom ListView items, and Honeycomb

October 15, 2011 Leave a comment

Here is another compatibility tip for Honeycomb and previous versions. This one has to with custom ListView item layouts inside alert dialogs.

Ok, so I have a few places in my app that display an AlertDialog with a ListView inside, backed up by a adapter that provides custom item layouts. Getting the text color inside those layouts is a little tricky.

Prior to Honeycomb, dialogs always use a dark background and light text, regardless of which theme (light or dark) is used by their parent activity. This is true for AlertDialogs and plain Dialogs. However, lists inside AlertDialogs use an inverted (light) background. This is usually enabled automatically, or you can enable it yourself by calling AlertDialog.setInverseBackgroundForced. Now TextViews inside item views returned by your adapter can use black text, or, preferably, just in case the color is customized by the device manufacturer, a theme attribute reference (one of textAppearanceMediumInverse, or textColorPrimaryInverse, etc.).

Here is an alert dialog with a list view, running on the Sony Ericsson Xperia Arc. The background is not quite white, it’s a bit greyish (purplish?), and the text is pure black.

The item layout looks like this (only text views are shown):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/pick_attachment_root"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content">

	<TextView
		android:textAppearance="?android:attr/textAppearanceMediumInverse"
		android:id="@+id/pick_attachment_main_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:singleLine="true"
		android:ellipsize="middle"
		android:textStyle="bold" />

	<TextView
		android:textAppearance="?android:attr/textAppearanceInverse"
		android:id="@+id/pick_attachment_sub_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:layout_below="@id/pick_attachment_main_title" />

</RelativeLayout>

On Honeycomb and later, if you use the native, Holographic, themes, dialogs can be light or dark, matching the parent activity’s theme. AlertDialogs with lists look the same as regular dialogs with text, so this is what the above layout looks like on an Sony Tablet S running Android 3.2:

The text views turned invisible, white on white. How did this happen? The dialog’s background is no longer inverted, as the dialog’s theme matches the parent activity’s, but our text appearance references still assume inverted background: for a light parent theme, it’s “whatever text color is appropriate over the inverse of a light background”, in other words, “whatever is appropriate for a black background”, which is white. Switching the parent activity to a dark theme produces black text on black background by the same logic.

Ok, so for Honecomb, we’re supposed to use non-inverted theme attribute references. How to do this without duplicating our layouts inside res/layout-v11? My solution is this:

First, define these styles inside res/values-v4/list_styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<!-- Text for listboxes, inverted for Andorid prior to 3.0 -->

	<style name="MyListTextAppearanceSmall">
		<item name="android:textAppearance">?android:attr/textAppearanceSmallInverse</item>
	</style>

	<style name="MyListTextAppearanceDefault">
		<item name="android:textAppearance">?android:attr/textAppearanceInverse</item>
	</style>

	<style name="MyListTextAppearanceMedium">
		<item name="android:textAppearance">?android:attr/textAppearanceMediumInverse</item>
	</style>

</resources>

Then use these styles inside the layouts returned by the adapter backing up the listview inside the AlertDialog (… that Jack built…). The layouts themselves don’t need to be duplicated, mine are in the “basic” res/layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/pick_attachment_root"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content">

	<TextView
		style="@style/MyListTextAppearanceMedium"
		android:id="@+id/pick_attachment_main_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:singleLine="true"
		android:ellipsize="middle"
		android:textStyle="bold" />

	<TextView
		style="@style/MyListTextAppearanceDefault"
		android:id="@+id/pick_attachment_sub_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_toRightOf="@id/pick_attachment_icon"
		android:layout_toLeftOf="@id/pick_attachment_toggle"
		android:layout_below="@id/pick_attachment_main_title" />

</RelativeLayout>

Now all that’s left to do is to redefine these three styles inside res/values-v11/list_styles.xml and to use regular, not inverted, text appearance references:

<?xml version="1.0" encoding="utf-8"?>
<resources>

	<!-- Text for listboxes, non-inverted starting with Android 3.0 -->
	
	<style name="MyListTextAppearanceSmall">
		<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
	</style>
	
	<style name="MyListTextAppearanceDefault">
		<item name="android:textAppearance">?android:attr/textAppearance</item>
	</style>

	<style name="MyListTextAppearanceMedium">
		<item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
	</style>

</resources>

The same dialog now looks like this:

And when the parent activity uses a theme dervied from @android:style/Theme.Holo, the dialog items will turn black, with light text.

Categories: android, ui

Custom themes for Honeycomb and backwards compatibility

October 7, 2011 5 comments

Here is a couple of things I learned while recenly updating my apps to have the native look on tablets running Android Honeycomb (3.*).

My app lets the user select a UI theme (there are two: light and dark), so I had something like this in my res/values/styles.xml:

<declare-styleable name="WifiTheme">
	<attr name="radar_text" format="color" />
	<attr name="radar_alpha" format="color" />
</declare-styleable>

<style name="WifiTheme.Light" parent="@android:style/Theme.Light">
	<item name="radar_text">#FF202020</item>
	<item name="radar_alpha">#80000000</item>
</style>

<style name="WifiTheme.Dark" parent="@android:style/Theme">
	<item name="radar_text">#FFFFFFFF</item>
	<item name="radar_alpha">#A0000000</item>
</style>

I would then apply one of these themes, from inside onCreate, to my activities.

Now, Honeycomb has new “Holographic” UI themes that look different from 2.* themes, and users expect an application running on a tablet to use those. The two new themes are @android:style/Theme.Holo.Light and @android:style/Theme.Holo.

It’s possible to duplicate the XML snippet above in res/values-11/styles.xml, replacing the parent themes, but that would mean duplicating all the values for those themes. The snippet above has just two extra attributes, but there could be many more of them.

Ok, so here is a trick I came up with. Just like with program code, what we need to get out of a tight spot is an extra level of abstraction.

First, I defined the following themes in res/values/theme_compat.xml:

<style name="ThemeCompat">
</style>

<style
	name="ThemeCompat.Dark"
	parent="@android:style/Theme">
</style>

<style
	name="ThemeCompat.Light"
	parent="@android:style/Theme.Light">
</style>

The first one is just to keep the Android style hierarchy system happy, the other two are where the real work is. You can think of them as defining a base class (in programming terms) for my custom themes. My custom theme declarations are now derived from the themes shown above, and look like this:

<style name="WifiTheme.Light" parent="@style/ThemeCompat.Light">
	<item name="radar_text">#FF202020</item>
	<item name="radar_alpha">#80000000</item>
</style>

<style name="WifiTheme.Dark" parent="@style/ThemeCompat.Dark">
	<item name="radar_text">#FFFFFFFF</item>
	<item name="radar_alpha">#A0000000</item>
</style>

And for when the application runs on Android 3.*, I have the following declarations in res/values-11/theme_compat.xml:

<style
	name="ThemeCompat.Dark"
	parent="@android:style/Theme.Holo">
</style>

<style
	name="ThemeCompat.Light"
	parent="@android:style/Theme.Holo.Light">
</style>

What this does is replaces the “base class” of my custom themes with those native to Honeycomb. Now my custom theme declarations don’t need to be duplicated, and are automatically derived at runtime from the most appropriate system themes.

Here is another thing. AlertDialogs with custom content views.

AlertDialogs can be sometimes tricky, because they have their own styling and can even impose it on their content. But I feel that using them in place of regular dialogs makes an application’s UI better. The light panel behind the buttons, the title bar with the icon, the separator line below that – those may seem insignificant little things, but I feel they really make an app look better. Some manufacturers (Samsung, Sony Ericsson, perhaps others) make changes to stock Andorid UI to add their own color and graphics accents, and so it becomes even more important to use AlertDialog to better blend in with the device’s UI.

And this is another thing where Honeycomb is different from previous Android versions. Prior to 3.0, AlertDialogs always use a dark background (unless overriden for lists), even if the parent activity’s theme is light. Starting with 3.0, alert dialogs have the same overall light/dark color scheme as the base activity.

If your alerts use custom content views, it becomes tricky, because those views need to be inflated using an appropriate Context, so they match the dialog’s color scheme.

It’s really easy to end up with white text on white background, unless you use the following method, new with API level 11: AlertDialogBuilder.getContext().

Since this method doesn’t exist prior to API level 11, it’s possible to use Java reflection to stay compatible with older Android versions. Turns out there is another way: by subclassing AlertDialog, and overriding onCreate(), it’s possible to call getContext() for the AlertDialog itself. That method exists since API level 1, and returns a correctly themed Context for both 3.* and prior versions of Android. A further shortcut is to call getLayoutInflater(), which also exists since API level 1.

With this in mind, here is a simple alert dialog:

public class DlgAbout extends AlertDialog {

	public DlgAbout(Context context) {
		super(context);
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {

		setTitle(R.string.about_title);
		setCancelable(true);

		LayoutInflater inflater = getLayoutInflater();

		View contentGroup = inflater.inflate(R.layout.about_content, null);
		setView(contentGroup);

		super.onCreate(savedInstanceState);
	}
}

The dialog’s layout can use views without any specific color customizations:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"

.....

<!-- Note the absence of android:textColor, etc -->
		<TextView
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:text="@string/about_1"
			android:gravity="center_horizontal" />
......

The code to use this dialog is below. Not really that much different from using AlertDialog.Builder, and is actually somewhat better because the code for the dialog is placed in its own class (above).

@Override
protected Dialog onCreateDialog(int id) {
	if (id == DIALOG_ID_ABOUT) {
		/*
		 * About the app
		 */
		return new DlgAbout(this);
	}
.........
}
Categories: android, ui

Implementing long-clickable preferences

August 29, 2011 10 comments

The built-in Preference class has a method to receive clicks, onClick, but no method to receive long clicks. In my current project, I actually have a need for this, and found a way to implement it.

PreferenceActivity is actually a ListActivity, with a special adapter behind the scenes. The usual (not long) clicks are processed by using the usual ListView mechanism, setOnItemClickListener. The code to set this up is in PreferenceScreen:

public final class PreferenceScreen extends PreferenceGroup implements AdapterView.OnItemClickListener.... {

    public void bind(ListView listView) {
        listView.setOnItemClickListener(this);
        listView.setAdapter(getRootAdapter());
        
        onAttachedToActivity();
    }

    public void onItemClick(AdapterView parent, View view, int position, long id) {
        Object item = getRootAdapter().getItem(position);
        if (!(item instanceof Preference)) return;
        
        final Preference preference = (Preference) item; 
        preference.performClick(this);
    }
}

It would be really easy to subclass PreferenceScreen and override bind to add a long-item-click listener to the list view, except this class is final. Because of this, I ended up adding the following code into my PreferenceActivity subclass:

public class BlahBlahActivity extends PreferenceActivity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {

		super.onCreate(savedInstanceState);

		addPreferencesFromResource(R.xml.account_options_prefs);

		ListView listView = getListView();
		listView.setOnItemLongClickListener(new OnItemLongClickListener() {
			@Override
			public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
				ListView listView = (ListView) parent;
				ListAdapter listAdapter = listView.getAdapter();
				Object obj = listAdapter.getItem(position);
				if (obj != null && obj instanceof View.OnLongClickListener) {
					View.OnLongClickListener longListener = (View.OnLongClickListener) obj;
					return longListener.onLongClick(view);
				}
				return false;
			}
		});
	}
}

Now I can have a Preference subclass that implements View.OnLongClickListener, which is automatically invoked for long clicks:

public class BlahBlahPreference extends CheckBoxPreference implements View.OnLongClickListener {

	@Override
	public boolean onLongClick(View v) {
		// Do something for long click
		return true;
	}
}
Categories: android, ui

Simple animated panels

April 26, 2011 Comments off

A common UI element in Android applications is an animated panel that provides access to certain actions. A panel like this should only be shown temporarily, perhaps triggered by other UI state changes initiated by the user.

You can see an example of this in GMail application: when you select one or more messages, a panel slides up from the bottom with buttons to act on those selected messages.

While using a PopupWindow is cetainly an option for this, it can be quite convenent to define the panel as a view group within the activity’s layout.

With a unified layout, the activity can obtain a reference to the panel’s view group with findViewById, and change its visibility state with View.setVisibility, either to View.VISIBLE or View.GONE.

Just changing the panel’s visibility isn’t by itself animated, especially when going from VISBLE to GONE: view animations are only shown if the view is visible. When hiding the panel, it’s necessary to first run the “slide out” animation, and only then set the visibility to GONE.

So – here is a simple class that makes it very easy to implement animated panels. It is constructed with a reference to the panel’s parent ViewGroup, and the type of animation (from right / from bottom), and has methods to animate the panel to slide in / out.

Here is a simple example of how this might look:

The class keeps track of the panel’s state, including currently running animations. When requested to hide the panel, it runs a “hide” animation first, and when the animation completes, but not sooner, changes the panel’s visibility to View.GONE.

public class PanelAnimation {

	public static final int WHERE_RIGHT = 0;
	public static final int WHERE_BOTTOM = 1;

	public PanelAnimation(ViewGroup viewGroup, int where) {
		mViewGroup = viewGroup;
		mContext = viewGroup.getContext();
		mWhere = where;
		mState = STATE_HIDDEN;
	}

	public boolean isShownOrShowing() {
		return mState == STATE_SHOWING || mState == STATE_SHOWN;
	}

	public void toggle() {
		if (mState == STATE_SHOWN) {
			hide();
		} else if (mState == STATE_HIDDEN) {
			show();
		}
	}

	public void showNow() {
		mState = STATE_SHOWN;
		mViewGroup.setVisibility(View.VISIBLE);
	}

	public void show() {
		if (mState != STATE_SHOWN) {
			mState = STATE_SHOWING;

			switch (mWhere) {
			default:
			case WHERE_RIGHT:
				mShowAnimation = AnimationUtils.makeInAnimation(mContext, false);
				break;
			case WHERE_BOTTOM:
				mShowAnimation = AnimationUtils.loadAnimation(mContext, R.anim.slide_in_up);
				break;
			}

			mShowAnimation.setAnimationListener(new AnimationListener() {
				@Override
				public void onAnimationStart(Animation animation) {
				}

				@Override
				public void onAnimationRepeat(Animation animation) {
				}

				@Override
				public void onAnimationEnd(Animation animation) {
					mState = STATE_SHOWN;
				}
			});
			mViewGroup.startAnimation(mShowAnimation);
			mViewGroup.setVisibility(View.VISIBLE);
		}
	}

	public void hide() {
		if (mState != STATE_HIDDEN) {
			mState = STATE_HIDING;

			switch (mWhere) {
			default:
			case WHERE_RIGHT:
				mHideAnimation = AnimationUtils.makeOutAnimation(mContext, true);
				break;
			case WHERE_BOTTOM:
				mHideAnimation = AnimationUtils.loadAnimation(mContext, R.anim.slide_out_down);
				break;
			}

			mHideAnimation.setAnimationListener(new AnimationListener() {
				@Override
				public void onAnimationStart(Animation animation) {
				}

				@Override
				public void onAnimationRepeat(Animation animation) {
				}

				@Override
				public void onAnimationEnd(Animation animation) {
					mState = STATE_HIDDEN;
					mViewGroup.setVisibility(View.GONE);
				}
			});
			mViewGroup.startAnimation(mHideAnimation);
		}
	}

	/**
	 * Attachment panel animation
	 */
	private static final int STATE_HIDDEN = 0;
	private static final int STATE_SHOWING = 1;
	private static final int STATE_SHOWN = 2;
	private static final int STATE_HIDING = 3;

	private int mState = STATE_HIDDEN;

	private ViewGroup mViewGroup;
	private Context mContext;
	private int mWhere;

	private Animation mShowAnimation;
	private Animation mHideAnimation;
}

This class uses two animations defined as resources, used when the panel is positioned at the bottom.

<?xml version="1.0" encoding="utf-8"?>
<!-- res/anim/slide_in_up.xml -->
<translate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:interpolator="@android:anim/accelerate_interpolator"
	android:fromYDelta="100%"
	android:toYDelta="0%"
	android:duration="250" />
<?xml version="1.0" encoding="utf-8"?>
<!-- res/anim/slide_out_down.xml -->
<translate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:interpolator="@android:anim/accelerate_interpolator"
	android:fromYDelta="0%"
	android:toYDelta="100%"
	android:duration="250" />

When the panel is on the right, this class uses standard Android animations (also used for activity animations).

To use this class, first add panel views to your activity’s layout .xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">

	<!-- Normal content -->

	<ListView
		android:id="@+id/message_list"
		android:layout_width="fill_parent"
		android:layout_height="0dp"
		android:layout_weight="1" />

	<!-- Button panel -->

	<LinearLayout
		style="@style/ButtonPanel"
		android:id="@+id/message_list_ops_panel"
		android:visibility="gone">

		<TextView
			style="@style/ButtonLikeText"
			android:id="@+id/message_list_op_delete"
			android:text="@string/message_list_op_delete" />

		<TextView
			style="@style/ButtonLikeText"
			android:id="@+id/message_list_op_as_read"
			android:layout_marginLeft="4dp"
			android:text="@string/message_list_op_as_read" />

		<TextView
			style="@style/ButtonLikeText"
			android:id="@+id/message_list_op_as_unread"
			android:layout_marginLeft="4dp"
			android:text="@string/message_list_op_as_unread" />

	</LinearLayout>

</LinearLayout>

Now in Java, get a refrence to the ButtonPanel ViewGroup and construct PanelAnimation for it:

class SomeActivity {
	@Override
	public void onCreate(Bundle savedInstanceState) {
		setContentView(R.layout.some_layout_here);

		mMessageOpsPanel = (ViewGroup) findViewById(R.id.message_list_ops_panel);
		mMessageOpsPanelAnimation = new PanelAnimation(mMessageOpsPanel,
				PanelAnimation.WHERE_BOTTOM);

	}

	private ViewGroup mMessageOpsPanel;
	private PanelAnimation mMessageOpsPanelAnimation;
}

Finally, showing and hiding the panel can be done like this:

	private void onMessageOpsChange(boolean visible) {
		if (visible) {
			mMessageOpsPanelAnimation.show();
		} else {
			mMessageOpsPanelAnimation.hide();
		}
	}

And that’s really it.

Categories: android, ui

An EditText for entering IP addresses

February 27, 2011 4 comments

So I needed to create a custom EditText for entering IP addresses. I wanted it to do two things: one, display the numpad-style IME, and two, accept only digits and the dot character.

There is no pre-defined EditText input type for this, and although using android:digits="0123456789." comes pretty close, but doesn’t quite do it for me, since it doesn’t switch the IME into the numpad mode.

As it turns out, implementing special rules for IP address entry is not too difficult, it just takes a special KeyListener.

The docs say that KeyListener is an interface for converting text key events into edit operations on an Editable class.

There are a bunch of pre-defined KeyListener subclasses, which are accessible to SDK applications, and we’re going to use one of them as the base class for our IPAddressKeyListener.

Associating an EditText with our key listener is done like this:

	EditText edAddress = (EditText) findViewById(R.id.advanced_network_address);
	edAddress.setKeyListener(IPAddressKeyListener.getInstance());

Almost ready to show the code. Just a quick reminder on what this class gives us:

  • Switches the on-screen keyboard (the IME) to numpad mode
  • Only allows digits 0 through 9 and the dot character
  • The first character has to be a digit (not a dot)
  • At most, 3 dot characters are allowed

The code:

public class IPAddressKeyListener extends NumberKeyListener {

	private char[] mAccepted;
	private static IPAddressKeyListener sInstance;

	@Override
	protected char[] getAcceptedChars() {
		return mAccepted;
	}

	/**
	 * The characters that are used.
	 * 
	 * @see KeyEvent#getMatch
	 * @see #getAcceptedChars
	 */
	private static final char[] CHARACTERS =

	new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' };

	private IPAddressKeyListener() {
		mAccepted = CHARACTERS;
	}

	/**
	 * Returns a IPAddressKeyListener that accepts the digits 0 through 9, plus the dot
	 * character, subject to IP address rules: the first character has to be a digit, and
	 * no more than 3 dots are allowed.
	 */
	public static IPAddressKeyListener getInstance() {
		if (sInstance != null) return sInstance;

		sInstance = new IPAddressKeyListener();
		return sInstance;
	}

	/**
	 * Display a number-only soft keyboard.
	 */
	public int getInputType() {
		return InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL;
	}

	/**
	 * Filter out unacceptable dot characters.
	 */
	@Override
	public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
			int dend) {
		CharSequence out = super.filter(source, start, end, dest, dstart, dend);

		if (out != null) {
			source = out;
			start = 0;
			end = out.length();
		}

		int decimal = -1;
		int dlen = dest.length();

		// Prevent two dot characters in a row
		if (dstart > 0 && dest.charAt(dstart - 1) == '.') {
			decimal = dstart - 1;
		}
		if (dend < dlen && dest.charAt(dend) == '.') {
			decimal = dend;
		}

		// Up to three dot charcters, and no more
		if (decimal == -1) {
			int decimalCount = 0;
			for (int i = 0; i < dstart; i++) {
				char c = dest.charAt(i);

				if (c == '.') {
					decimalCount++;
					decimal = i;
				}
			}
			for (int i = dend; i < dlen; i++) {
				char c = dest.charAt(i);

				if (c == '.') {
					decimalCount++;
					decimal = i;
				}
			}

			if (decimalCount < 3) {
				decimal = -1;
			}
		}

		SpannableStringBuilder stripped = null;

		for (int i = end - 1; i >= start; i--) {
			char c = source.charAt(i);
			boolean strip = false;

			if (c == '.') {
				if (i == start && dstart == 0) {
					strip = true;
				} else if (decimal >= 0) {
					strip = true;
				} else {
					decimal = i;
				}
			}

			if (strip) {
				if (end == start + 1) {
					return ""; // Only one character, and it was stripped.
				}

				if (stripped == null) {
					stripped = new SpannableStringBuilder(source, start, end);
				}

				stripped.delete(i - start, i + 1 - start);
			}
		}

		if (stripped != null) {
			return stripped;
		} else if (out != null) {
			return out;
		} else {
			return null;
		}
	}
}

Some additional validation is still necessary (like checking that individual byte values are less than or equal to 255), but that can be done when the user leaves the edit control.

Categories: android, ui

Customizing AlertDialog, an update – removing the title

February 27, 2011 6 comments

A follow-up to an earlier post about customizing AlertDialog.

Jonas Petersson asked whether it’s possible to remove an alert dialog’s title. It is. Add a call to requestWindowFeature in your subclass’s onCreate, make sure it comes before the call to super.onCreate():

	@Override
	protected void onCreate(Bundle savedInstanceState) {

		setView...
		setButton...

		/*** This removes the title ***/
		requestWindowFeature(Window.FEATURE_NO_TITLE);

		super.onCreate(savedInstanceState);
	}

And this is what it looks like:

Categories: android, ui

Customizing AlertDialog

February 19, 2011 5 comments

Android has two ways to create dialogs: for simple ones, there is AlertDialog and its builder, for more complex there is Dialog.

AlertDialog looks very nice: the bottom panel (for buttons) is highlighted, the title bar has an icon, the font sizes are just right. But at the same time, AlertDialog can be too restrictive. You can replace the content view and configure the buttons, but you can’t change the logic of how these buttons work, and you can’t get to individual UI components to modify their behavior.

Or so I thought.

Whenever I needed a dialog with some logic to it (such as disabling buttons until the user provides correct input), I used to use plain Dialog. It did what I needed, but my dialogs looked different than AlertDialog, as you can see in the Android Development Guide.

The resources that Android uses for AlertDialog are: 1) private, and can’t be referenced directly from an application using “@android:drawable/imgname” and 2) can be customized by the hardware vendors (my Samsung Galaxy S uses a bluish scheme and a different title bar icon). For those two reasons, I had to find a way to customize AlertDialog without trying to copy those resources into my project and rebuild the way the UI looks with my own code.

Sidebar: I’m really not too happy with how some Andorid resources can’t be referenced from an application. Especially menu icons: for example, ic_menu_close_clear_cancel and ic_menu_add are accessible under “@drawable:android”, but ic_menu_refresh, or ic_menu_attachment are private and have to be copied into the project. The trouble, of course, is that hardware manufacturers can customize those, and your application will look inconsistent).

Ok, so it turns out, you can subclass AlertDialog, and get all the benefits with respect to its appearance, and still implement the behavior logic as needed.

public class MyPrettyDialog extends AlertDialog {

	private ListView mListView;
	private MyAdapter mAdapter;
	private Button mAcceptButton;

	public MyPrettyDialog(Context context) {
		super(context);
	}

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		ListView listView = new ListView(mContext);
		listView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
				LayoutParams.WRAP_CONTENT, 1.0f));
		listView.setId(android.R.id.list);

		MyAdapter adapter = new MyAdapter();

		listView.setAdapter(adapter);
		listView.setOnItemClickListener(adapter);

		mListView = listView;
		mAdapter = adapter;

		setInverseBackgroundForced(true);
		setView(listView);
		setTitle(R.string.mytitle);
		setCancelable(false);

		setButton(DialogInterface.BUTTON_POSITIVE, "Positive",
				new OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						// This is replaced later
					}
				});

		setButton(DialogInterface.BUTTON_NEGATIVE, "Negative",
				new OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						// Do something
					}
				});

		super.onCreate(savedInstanceState);

		/*
		 * Replace the click listener
		 */
		mAcceptButton = getButton(BUTTON_ATTACH);
		mAcceptButton.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// Do something, the dialog is NOT closed automatically
			}
		});
	}
}

The most important part here is the overriden onCreate(). It is called automatically by the framework as nedded (just like for Activities). Creating this dialog is as simple as when using AlertDialog.Builder:

	@Override
	protected Dialog onCreateDialog(int id) {
		if (id == MY_PRETTY_DIALOG) {
			MyPrettyDialog dialog = new MyPrettyDialog(this);
			return dialog;
		}
		return super.onCreateDialog(id);
	}

The real value of the above code is that we can get references to UI widgets and keep them in the dialog (mListView, mAcceptButton).

With this, we can for example disable the “Accept” button until the user provides required data. Also, AlertDialog, by default, closes right after the user clicks any of the buttons, and you can’t avoid that. If you would like to do some validation (keeping the “Accept” button enabled, and explaining to the user what he needs to fill in and how), you can do that by replacing the click handler by the button with your own, as the code above does at the end of onCreate.

And this is what I actually ended up with – an AlertDialog with pretty involved processing logic, that still has the look the users expect.

Categories: android, ui

Large (tablet) screen impact on Android Application UI

January 29, 2011 Leave a comment

A few days Google released a perview of the next version of Andorid, 3.0 (Honeycomb), targeted at tablet devices.

Honeycomb contains new framework features that make it easier to create UIs that scale to large screen sizes. However, there are already tablets out there, like the Samsung Galaxy Tab, which run earlier versions of Android. Applications running on these earlier Android versions cannot take advantage of those new Honeycomb features (at least not yet). So, what effects does running on a large screen have on the UI of an Android application?

I’ve only done Galaxy Tab tests with the emulator, so my observations are somewhat limited, but still, here they are.

The Galaxy Tab reports itself as an HDPI device, even though its actual screen density is 170 dpi. This is to account for the larger display pixel count, and greater viewing distance.

Still, compared to a typical 4″ 480×800 hdpi phone screen, the Galaxy Tab can fit more on the screen, since it has more pixels: using a 72×72 icon as a measurement unit, a 480×800 screen can fit 6.6 accross and 11.11 down. The Galaxy Tab can fit 8.33 down and 14.22 across.

This makes images look smaller relative to the rest of the screen, even though they are larger in real world units, compared to a 4″ hdpi phone, and have same pixel size.

Screen metrics for a 4″ 480 by 800 screen

Pixels Inches Actual DPI Icon size, pixels Icon size, inches Icons per screen
480 2,06 233,01 72px 0,31″ 6,67
800 3,43 233,01 72px 0,31″ 11,11

Screen metrics for a 7″ 600 by 1024 screen

Pixels Inches Actual DPI Icon size, pixels Icon size, inches Icons per screen
600 3,54 169,49 72px 0,42″ 8,33
1024 6,04 169,49 72px 0,42″ 14,22

What this means is…

I don’t think you can expect an application running on the Tab (or another tablet) to look just like it looks on a phone, only larger. So far, this has been the case for phones of varying screen sizes and densities, but for a tablet device, this is no longer true.

And here are some screenshots of my application running on an HVGA device, on a WVGA800 device, and the Galaxy Tab, all resized to same height (320px):

HVGA - original size

The original 480 by 320 HVGA screen.

WVGA800 - resized

This is 800 by 480, resized to 533 by 320 (same height as the HVGA screenshot above).

Looks very, very much like the HVGA screenshot (although the text in spinners is not clipped, as they are a little larger).

Galaxy Tab - resized

This is 1024 by 600, resized to 546 by 320 (same height as the HVGA screenshot above). Doesn’t look like the screens above, does it?

The UI controls on the right still line up with list view on the left, but they are much smaller relative to the screen.

Measured in inches, the entire screen is larger, and the buttons (being the same size in pixels) are also larger than those on a 4″ WVGA800 device.

The Galaxy Tab can fit more information on the screen. The list could have about one and a half rows more.

Categories: android, ui

Creating a checkable image button

January 11, 2011 4 comments

To continue with the theme of custom styleable controls, here is how to implement your own image button that behaves like a radio button or a check box.

I wanted to make it easy (for myself) to create a bunch of these buttons, differing only by the image they use, and to avoid having to create state-dependent variations of those images.

In other words, the background uses state-dependent drawables, while the foreground image is fixed (per button).

This is what it looks like:

Creating these in XML was easy:

<your.package.name.CheckableImageButton
	android:id="@+id/checkable_image_1"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:background="@drawable/checkable_image_selector"
	android:src="@drawable/checkable_icon_1" />

<your.package.name.CheckableImageButton
	android:id="@+id/checkable_image_2"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:background="@drawable/checkable_image_selector"
	android:src="@drawable/checkable_icon_2" />

The images checkable_icon_1, checkable_icon_2 only differ by color, and are plain PNG files.

Declaring the styleable

<!-- Goes into res/values/styles.xml -->
<declare-styleable name="CheckableImageButton">
	<attr name="is_checked" format="boolean" />
        
	<attr name="personality">
            	<enum name="radio" value="0" />
            	<enum name="check" value="1" />
        	</attr>
</declare-styleable>  

The personality value will be used to make the button behave like a check box (which toggles its state when touched) or a radio button (which always goes into checked state, regardless of current state).

The code

Android’s ImageButton doesn’t support the notion of a checked state, but otherwise is pretty close.

Here is my subclass:

public class CheckableImageButton extends ImageButton implements Checkable {

	private boolean mChecked;
	private boolean mBroadcasting;
	private int mPersonality;
	private OnCheckedChangeListener mOnCheckedChangeListener;

	private static final int[] CHECKED_STATE_SET = { R.attr.is_checked };

	private static final int PERSONALITY_RADIO_BUTTON = 0;
	private static final int PERSONALITY_CHECK_BOX = 1;

	public CheckableImageButton(Context context) {
		this(context, null);
	}

	public CheckableImageButton(Context context, AttributeSet attrs) {
		super(context, attrs);

		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CheckableImageButton);

		mPersonality = a.getInt(R.styleable.CheckableImageButton_personality, PERSONALITY_RADIO_BUTTON);
		boolean checked = a.getBoolean(R.styleable.CheckableImageButton_is_checked, false);
		setChecked(checked);

		a.recycle();
	}

	public void toggle() {
		setChecked(!mChecked);
	}

	@Override
	public boolean performClick() {
		if (mPersonality == PERSONALITY_RADIO_BUTTON) {
			setChecked(true);
		} else if (mPersonality == PERSONALITY_CHECK_BOX) {
			toggle();
		}
		return super.performClick();
	}

	public boolean isChecked() {
		return mChecked;
	}

	/**
	 * <p>
	 * Changes the checked state of this button.
	 * </p>
	 * 
	 * @param checked
	 *            true to check the button, false to uncheck it
	 */
	public void setChecked(boolean checked) {
		if (mChecked != checked) {
			mChecked = checked;
			refreshDrawableState();

			// Avoid infinite recursions if setChecked() is called from a listener
			if (mBroadcasting) {
				return;
			}

			mBroadcasting = true;
			if (mOnCheckedChangeListener != null) {
				mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
			}

			mBroadcasting = false;
		}
	}

	/**
	 * Register a callback to be invoked when the checked state of this button changes.
	 * 
	 * @param listener
	 *            the callback to call on checked state change
	 */
	public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
		mOnCheckedChangeListener = listener;
	}

	/**
	 * Interface definition for a callback.
	 */
	public static interface OnCheckedChangeListener {
		/**
		 * Called when the checked state of a button has changed.
		 * 
		 * @param button
		 *            The button view whose state has changed.
		 * @param isChecked
		 *            The new checked state of button.
		 */
		void onCheckedChanged(CheckableImageButton button, boolean isChecked);
	}

	@Override
	public int[] onCreateDrawableState(int extraSpace) {
		final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
		if (isChecked()) {
			mergeDrawableStates(drawableState, CHECKED_STATE_SET);
		}
		return drawableState;
	}

	@Override
	protected void drawableStateChanged() {
		super.drawableStateChanged();
		invalidate();
	}

	static class SavedState extends BaseSavedState {
		boolean checked;

		SavedState(Parcelable superState) {
			super(superState);
		}

		private SavedState(Parcel in) {
			super(in);
			checked = (Boolean) in.readValue(null);
		}

		@Override
		public void writeToParcel(Parcel out, int flags) {
			super.writeToParcel(out, flags);
			out.writeValue(checked);
		}

		public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
			public SavedState createFromParcel(Parcel in) {
				return new SavedState(in);
			}

			public SavedState[] newArray(int size) {
				return new SavedState[size];
			}
		};
	}

	@Override
	public Parcelable onSaveInstanceState() {
		Parcelable superState = super.onSaveInstanceState();
		SavedState ss = new SavedState(superState);
		ss.checked = isChecked();
		return ss;
	}

	@Override
	public void onRestoreInstanceState(Parcelable state) {
		SavedState ss = (SavedState) state;

		super.onRestoreInstanceState(ss.getSuperState());
		setChecked(ss.checked);
		requestLayout();
	}
}

The button adds a new drawable state to the base class, which is exposed via onCreateDrawableState.

Switchable personality is supported by getting a value from XML in the constructor:

mPersonality = a.getInt(R.styleable.CheckableImageButton_personality, PERSONALITY_RADIO_BUTTON);

and doing different things in performClick depending on whether the personality is a checkbox or a radiobutton:

	@Override
	public boolean performClick() {
		if (mPersonality == PERSONALITY_RADIO_BUTTON) {
			setChecked(true);
		} else if (mPersonality == PERSONALITY_CHECK_BOX) {
			toggle();
		}
		return super.performClick();
	}

The background drawable selector with states

This goes into res/drawable/checkable_image_selector.xml, and can shared by all CheckableImageButton objects.

<?xml version="1.0" encoding="utf-8"?>
<selector
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:sample="http://schemas.android.com/apk/res/your.package.name">
	<item
		android:state_pressed="true"
		android:drawable="@drawable/checkable_image_button_state_pressed" />
	<item
		sample:is_checked="true"
		android:drawable="@drawable/checkable_image_button_state_checked" />
	<item
		android:drawable="@android:color/transparent" />
</selector>

Note the use of sample namespace for is_checked. This links the attribute to the styleable in styles.xml and then on to R.styleable.CheckableImageButton in the constructor.

Finally, background drawables for individual states

This is the drawable for pressed state. You can see in the second screenshot near the top of this page. The magic colors for the gradient were picked from nine-patch drawables supplied with the SDK. The stroke color is a nice shade of pale blue I happen to like.

<?xml version="1.0" encoding="utf-8"?>
<shape
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:shape="rectangle">
	<corners
		android:radius="4dp" />
	<padding
		android:left="4dp"
		android:right="4dp"
		android:top="4dp"
		android:bottom="4dp" />
	<gradient
		android:angle="90"
		android:startColor="#FFFFC700"
		android:centerColor="#FFFFA600"
		android:endColor="#FFFFC700" />
	<stroke
		android:width="3dp"
		android:color="#FF80B0E0" />
</shape>

The “non-pressed” checked state is simplier, as it doesn’t need a gradient fill:

<?xml version="1.0" encoding="utf-8"?>
<shape
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:shape="rectangle">
	<corners
		android:radius="4dp" />
	<padding
		android:left="4dp"
		android:right="4dp"
		android:top="4dp"
		android:bottom="4dp" />
	<solid
		android:color="#00000000" />
	<stroke
		android:width="3dp"
		android:color="#FF80B0E0" />
</shape>

And that’s about it. Hope this was useful.

Categories: android, ui
Follow

Get every new post delivered to your Inbox.

Join 57 other followers