Tuesday, August 2, 2011

The Soft Keyboard

Android's APIs do not give us a method with which we can determine if the soft keyboard is showing or not.  We can programmatically request that the keyboard be shown or hidden with simple API calls, but no such calls exist for determining if the keyboard is visible or not.

With a simple trick, we can devise a way by which we can reliably determine if the keyboard is visible and even be notified when it is shown or hidden.

For this, we look to the android:windowSoftInputMode attribute of an Activity.  When set to the value "adjustResize", Android automatically changes the height of our activity's window to accommodate for the soft keyboard.  That is, when the keyboard is shown, our activity's window height is decreased by the height of the keyboard.  Likewise, when the keyboard is hidden, our activity's window height is increased by the height of the keyboard.

Knowing that Android will resize our window (and thus our top level view) when the keyboard is shown or hidden, we can create a top level layout which handles layout changes and determines if the soft keyboard is visible by comparing its height against the height of the display - if the height of the layout is less than the height of the display (minus the status bar height), we know that the keyboard is showing.

A sample top level layout might be defined as:

package com.cannedcoding;
public class KeyboardAwareLinearLayout extends LinearLayout {
  private boolean mKeyboardVisible = false;

  public boolean isKeyboardVisible() {
    return mKeyboardVisible;
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r,
    int b) {
    super.onLayout(changed, l, t, r, b);

    updateKeyboardVisible();
  }

  private void updateKeyboardVisible() {
    int viewHeight = getHeight();
    int displayHeight = getDisplayHeight();

    //For the purposes of making this example more concise, 

    //we'll use a fixed max size for our status bar.
    //In practice, this should be determined programmatically
    int maxStatusBarHeight = 38;

    mKeyboardVisible =
        (viewHeight + maxStatusBarHeight < displayHeight);
  }

  private int getDisplayHeight() {
    WindowManager windowManager = (WindowManager) getContext()
      .getSystemService(Context.WINDOW_SERVICE);

    Display display = windowManager.getDefaultDisplay();

    return display.getHeight();
  }
}


Then, in our activity's layout xml, we define our top level layout as:

<?xml version="1.0" encoding="utf-8"?>
<com.cannedcoding.KeyboardAwareLinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/top_level_layout"
  android:layout_height="fill_parent"
  android:layout_width="fill_parent">
  <!-- Child Views -->
</com.cannedcoding.KeyboardAwareLinearLayout>


Note that we define our view's height to be "fill_parent" ("match_parent" in later versions of Android OS) as we want to ensure that our view attempts to fill the entire window.  This behavior allows us to compare the height of the view to the height of the display and determine the keyboard visibility.

Finally, in our activity, we simply find the top level view and voila, we can determine if our keyboard is visible.

KeyboardAwareLinearLayout layout =
  (KeyboardAwareLinearLayout)
  findViewById(R.id.top_level_layout);

layout.isKeyboardVisible();


If we would like to be notified when the keyboard is shown or hidden, we can simply define an abstract class:

public abstract class KeyboardVisibleChangedHandler {
  abstract public void onKeyboardVisibleChanged(boolean
    keyboardVisible);
}


We then update our KeyboardAwareLinearLayout class to contain a list of said handlers as well as methods for adding / removing them.

Finally, we update our updateKeyboardVisible method:

private void updateKeyboardVisible() {
  int viewHeight = getHeight();
  int displayHeight = getDisplayHeight();
  int maxStatusBarHeight = 38;

  boolean oldValue = mKeyboardVisible;

  mKeyboardVisible =
    (viewHeight + maxStatusBarHeight < displayHeight);

  if (oldValue != mKeyboardVisible) {
    //for each KeyboardVisibleChangedHandler, call
    //onKeyboardVisibleChanged(mKeyboardVisible)
  }
}

Monday, July 25, 2011

android:layout_weight="1"

A common ui layout used in Android development is the Linear Layout.  It does what its name suggests and lays out its child views (ui elements) sequentially, in either a vertical or horizontal orientation (as specified by the android:orientation attribute).  As a developer, you can choose to specify the sizes of your views or let the OS take care of them for you (by specifying either "wrap_content" or "fill_parent" / "match_parent" for the layout_height / layout_width values).

Let's consider a full screen ui where we have a header containing the title of our content, a footer with one button in it, and a text description which occupies the rest of the space (e.g. a product description view in a shopping app).

We could define said layout as:

<LinearLayout

  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical">


  <TextView
    android:layout_width="fill_parent"

    android:layout_height="wrap_content"
    android:text="Title" />


  <TextView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:text="Some description of our content" />


  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="OK" />

</LinearLayout>


We have defined a LinearLayout with a vertical orientation.  We then, in desired layout order, defined our title TextView, our description TextView, and our OK Button (footer).

The title TextView and OK Button both specify their heights to be "wrap_content".  This tells the views' parent to size them just tall enough to fit their content (their text + any padding).

The description TextView specifies its height to be "fill_parent" (replaced with "match_parent" in later versions of Android OS).  This tells the view's parent to fill in any remaining space that the parent may have with the TextView.

So, we would expect to see our title text at the top of the screen, our OK button at the bottom of the screen, and our description politely occupying the area in the middle.  No such luck.  If you were to try the above, you would see that our title does indeed appear at the top of our view.  However, our description occupies the rest of the screen, pushing the OK button off into the ether.

Enter the rabbit hole.

It turns out, if we simply add a layout_weight other than 0 to our description text view, everything falls neatly into place.  Our updated text view looks like:

  <TextView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    android:text="Some description of our content" />