Monday, September 6, 2010

Soft keyboard hardship


The plug-in design of soft (virtual) keyboards is definitely one of Android's better architecture decisions. For one thing it allows Swype to exist, and I have no wish to return to a Swype-less existence.

But there is a curious omission from the APIs that work with it: there is no API for knowing when the soft keyboard is dismissed or shown. (See the official word from Android engineer Dianne Hackborn here).

It's curious because you can ask InputMethodManager to show the keyboard - most likely to be wanted when a text field receives focus - and you can ask it to hide the keyboard. But you can't tell at runtime whether the keyboard is shown or hidden.

The reason you'd want this in the first place is to exercise control over your layout when the keyboard appears and disappears. You do get a primitive degree of control over this via windowSoftInputMode, but if you want to optimize your layout with respect to the reduced amount of space you get once the keyboard's stolen half the screen, you're strangely out of luck.

The recommended way to get this information is to override the onSizeChanged() or onLayout() of a top-level view and basically guess at whether the keyboard is there or not. Here's an example of this:

public class MainLayout extends LinearLayout {

public MainLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
int height = bottom-top;
Activity activity = (Activity)getContext();
Rect rect = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rect.top;
int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
int diff = (screenHeight - statusBarHeight) - height;
boolean softKeyboardShown = (diff>0);
}
}
}

Here we get the height of the screen and subtract the height of the system status bar to get the size in pixels of the area of screen that the activity should be filling. If there's a mismatch between that size and the activity's height, that must be because the soft keyboard is showing.

This works well, despite the assumption about where the system status bar is. The real catch is that this code runs in the middle of a layout pass. Since the reason you want to do this at all is so you can adjust layout, the middle of a layout pass is the least desirable place possible. As soon as you change the size or position of anything, you end up having to queue a requestLayout() call on a posted runnable to get your adjustment to work. And that introduces a horrible flicker as the activity spends a fraction of a second in the intermediate layout.

Really we want to know about the keyboard change before any layout change occurs. After much research it seems this might - MIGHT - be possible.

The trick is to subclass EditText and override onCreateInputConnection(). This function is called before the soft keyboard is shown, and before the layout is changed to make room for it. So that's one half of the problem solved already.

The other half - to know when the soft keyboard is about to disappear - is harder. So hard in fact, that I haven't got a real solution yet. The closest I've got is to wrap the InputConnection object returned by onCreateInputConnection and hook into its finishComposingText() function. This is called the moment the soft keyboard detaches from a view.

Unfortunately finishComposingText() isn't necessarily called when the keyboard is going to be hidden, only when it's stopped being attached to a given EditText. If the focus is merely changing from one EditText to another, then the keyboard won't be going anywhere.

So as I finish this first article on this irritating subject, I have an EditText-based class which mostly works:

public class EditTextEx extends EditText {

public EditTextEx(Context context, AttributeSet attrs) {
super(context, attrs);
}

static boolean keyboardDefinitelyShown = false;
static boolean ignoreNextFinishComposingTextEvent = false;
static long timeWeLostFocus;
@Override
protected void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
long now = android.os.SystemClock.uptimeMillis();
if (!gainFocus) {
timeWeLostFocus = now;
keyboardDefinitelyShown = false;
ignoreNextFinishComposingTextEvent = false;
}
else {
long timeSinceLosingFocus = now - timeWeLostFocus;
keyboardDefinitelyShown = (timeSinceLosingFocus <>
ignoreNextFinishComposingTextEvent = keyboardDefinitelyShown;
}
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
}

private class MyInputConnection extends InputConnectionWrapper {
MyInputConnection(InputConnection conn) {
super(conn, true);
}
@Override
public boolean finishComposingText() {
final MyActivity activity = (MyActivity)getContext();
if (ignoreNextFinishComposingTextEvent) {
ignoreNextFinishComposingTextEvent = false;
}
else {
if (keyboardDefinitelyShown) {
keyboardDefinitelyShown = false;
activity.onSoftKeyboardShown(false);
}
}
return super.finishComposingText();
}
}
@Override public InputConnection onCreateInputConnection (EditorInfo outAttrs) {
MyActivity activity = (MyActivity)getContext();
keyboardDefinitelyShown = true;
activity.onSoftKeyboardShown(true);
return new MyInputConnection(super.onCreateInputConnection(outAttrs));
}
}

This class exploits the fact that onFocusChanged(false) will always be called before onFocusChanged(true). If the focus is switching between two EditTextEx instances, these events will occur within a very short space of time, and so we know that the keyboard isn't going to be hidden when the next finishComposingText() occurs.

It works, but I run into problems with ViewFlippers and what happens to focus when the screen's turned off then on. Am sure these are solveable but for the minute I'm out of time.

Wednesday, July 28, 2010

ListView.setEmptyView() gotcha

When you set a ListView's "empty view" programmatically, you can end up scratching your head as to why your empty view actually doesn't appear when the list is empty.

If this happens, then what you forgot is that you must manually add your empty view to your view hierarchy, cos ListView won't do it for you. Although it's obvious when you think about it, the documentation doesn't mention this detail, and Googling shows at least one person had the problem.

Here's the code with the lines that it's all too easy to forget in bold...

TextView emptyView = new TextView(context);
emptyView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
emptyView.setText("This appears when the list is empty");
emptyView.setVisibility(View.GONE);
((ViewGroup)list.getParent()).addView(emptyView);
list.setEmptyView(emptyView);


Friday, July 23, 2010

Tip: Developing with a Samsung Galaxy S

It's amazingly non-obvious how to get development going with a Samsung Galaxy S. The USB driver in the Android SDK doesn't work (IIRC it only recognizes some subset of HTC models), and Samsung don't seem to ship one of their own anywhere. I spent a number of hours Googling and trying various broken drivers that did got me nowhere.

Until I found something that worked. The trick is to install a piece of Samsung software called "New PC Studio" (download from http://www.samsungmobile.co.uk/support/softwaremanuals/newPCStudio.do). Follow the install instructions, and run it up. When you connect your phone (after being prompted to by the app UI of course) it'll install a bunch of drivers for you.

Now, one of these drivers will fail to install, and New PC Studio will inform you that your phone is "unrecognized". Don't worry, because before it failed it successfully installed a working USB driver that Eclipse and ADB can use, which was all you wanted anyway. You can then switch back to Eclipse/ADB and ignore New PC Studio for the rest of your life.

Thursday, July 15, 2010

Views do not have an "android:enabled" attribute

So trying to use it for View-derived classes in your layout XML does nothing.

Trying to use non-existent attributes normally causes a compiler error, but not in the case of android:enabled. I'm guessing that this is because it is valid for a very few types not used in layout files - e.g., Menu, Application - and the schema isn't very tightly enforced.

Therefore, if you want a button or something to begin life disabled until some event occurs, you can't do that in XML... you have to do it in code, i.e. use setEnabled(false) in your onCreate().

(I found this out the hard way... spent two hours convinced I'd found a bug in StateListDrawable or because my apparently-disabled buttons were being drawn just as if they were enabled. Which of course they were. Doh!).

Thursday, June 3, 2010

Animations do not affect layout

Yesterday I wanted to implement a cool transition effect. If you've used an iPhone you've seen things like it scores of times... you push a button and your window smoothly glides from one state (a normal, default, 'viewing' state, say) into a secondary state... perhaps one in which a list which normally only shows 3 items is expanded to show 10 items, pushing less important widgetry out of the way while the user makes their selection. Press the button again and it glides back to the original layout.

Since slick, ultra-smooth animation was what I was after, I made the error of thinking that android.view.animation.* would be just what was needed. I played around with the basic stock Animation types for a while, and just couldn't figure out why, while I could do all sorts of cool things, I couldn't do the one simple thing I wanted : resizing.

Sure, I could trivially send Buttons and TextViews skipping merrily across the screen, fade them out, fade them back in, zoom them in and out, even rotate them by arbitrary angles... so why not resize? And why did they snap back to their original locations when the animation ended?

It was only after looking into the source code for Android's animation package that I realized what was going on... with hindsight it's blindingly obvious, but if you found this article because you're wondering the same thing, allow me set you straight:

Animations only ever affect how a view is displayed. Can't emphasize that enough. It does not affect the view's position or size. That button might look as if it's moving, but that's an illusion... it's real position and size are unchanged. All the animation system does is add a couple of last-minute alterations to how the view is displayed when your window is drawn... the layout is completely unaffected.

That's why views snap back to their original position/zoom/alpha when an animation ends... they never actually changed in the first place. Similarly, that's why there's no ResizeAnimation type in the package... if you can't touch layout at all, resizing is impossible.

IMHO layout animations are a missing part of the SDK. Perhaps some day it'll be addressed, but for now you have to roll your own. It's not that hard to do, and I hope to demonstrate how - using those lovely Interpolator types for extra gorgeousness - in a follow-up article.