Miscellaneous UI Tricks

While well-written GUI frameworks are better organized than XKCD’s take on home organization, there are always a handful of tidbits that do, indeed, get categorized as “miscellaneous”.

Prerequisites

Understanding this chapter requires that you have read the core chapters of this book. Having an appreciation for XKCD is welcome, but optional.

Full-Screen and Lights-Out Modes

Full-screen mode, in Android parlance, means removing any system-supplied “bars” from the screen: title bar, action bar, status bar, system bar, navigation bar, etc. You might use this for games, video players, digital book readers, or other places where the time spent in an activity is great enough to warrant removing some of the normal accouterments to free up space for whatever the activity itself is doing.

Lights-out mode, in Android parlance, is where you take the system bar or navigation bar and dim the widgets inside of them, such that the bar is still usable, but is less visually distracting. This is a new concept added in Android 3.0 and has no direct analogue in Android 1.x or 2.x.

Android 1.x/2.x

To have an activity be in full-screen mode, you have two choices:

  1. Having the activity use a theme of Theme.NoTitleBar.Fullscreen (or some custom theme that inherits from Theme.NoTitleBar.Fullscreen)
  2. Execute the following statements in onCreate() of your activity before calling setContentView():


requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 
                     WindowManager.LayoutParams.FLAG_FULLSCREEN);

The first statement removes the title bar or action bar. The second statement indicates that you want the activity to run in full-screen mode, hiding the status bar.

Android 4.0+

Things got significantly more messy once we started adding in the system bar (and, later, the navigation bar as the replacement for the system bar). Since these bars provide the user access to HOME, BACK, etc., it is usually important for them to be available. Android’s behavior, therefore, varies in how you ask for something to happen and what then happens, based upon whether the device is a phone or a tablet.

The Activities/FullScreen sample project tries to enumerate some of the possibilities. On an Android 4.0 device, we have three RadioButtons:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <RadioGroup
    android:id="@+id/screenStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RadioButton
      android:id="@+id/normal"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:checked="true"
      android:text="@string/display_normal"/>

    <RadioButton
      android:id="@+id/lowProfile"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/display_low_profile"/>

    <RadioButton
      android:id="@+id/hideNav"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/display_hide_navigation"/>
  </RadioGroup>

  <Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:text="@string/something_at_the_bottom"/>

</RelativeLayout>
(from Activities/FullScreen/app/src/main/res/layout/main.xml)

Sample UI, As Initially Launched on Android 4.0
Figure 635: Sample UI, As Initially Launched on Android 4.0

…while on Android 4.1 or higher, we have another two possibilities:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <RadioGroup
    android:id="@+id/screenStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <RadioButton
      android:id="@+id/normal"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:checked="true"
      android:text="@string/display_normal"/>

    <RadioButton
      android:id="@+id/lowProfile"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/display_low_profile"/>

    <RadioButton
      android:id="@+id/hideNav"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/display_hide_navigation"/>

    <RadioButton
      android:id="@+id/hideStatusBar"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/hide_status_bar"/>
    
    <RadioButton
      android:id="@+id/fullScreen"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/display_full_screen"/>
  </RadioGroup>

  <Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:text="@string/something_at_the_bottom"/>

</RelativeLayout>
(from Activities/FullScreen/app/src/main/res/layout-v16/main.xml)

Sample UI, As Initially Launched on a Nexus 4/Android 4.2
Figure 636: Sample UI, As Initially Launched on a Nexus 4/Android 4.2

Controlling the full-screen and lights-out modes is managed via a call to setSystemUiVisibility(), a method on View. You pass in a value made up of an OR’d (|) set of flags indicating what you want the visibility to be, with the default being normal operation. Hence, in the screenshot above, you see a Nexus 4 in normal mode. Here is the same UI on a Nexus 10 in normal mode:

Sample UI, As Initially Launched on a Nexus 10/Android 4.2
Figure 637: Sample UI, As Initially Launched on a Nexus 10/Android 4.2

Lights-out, or low-profile mode, is achieved by calling setSystemUiVisibility() with the View.SYSTEM_UI_FLAG_LOW_PROFILE flag. This will dim the navigation or system bar, so the bar is there and the buttons are still active, but that they are less visually intrusive:

Sample UI, Lights-Out Mode, Nexus 4/Android 4.2
Figure 638: Sample UI, Lights-Out Mode, Nexus 4/Android 4.2

Sample UI, Lights-Out Mode, Nexus 10/Android 4.2
Figure 639: Sample UI, Lights-Out Mode, Nexus 10/Android 4.2

You can temporarily hide the navigation bar (or system bar) by passing View.SYSTEM_UI_FLAG_HIDE_NAVIGATION to setSystemUiVisibility(). The bar will disappear, until the user touches the UI, in which case the bar reappears:

Sample UI, Hidden-Navigation Mode, Nexus 4/Android 4.2
Figure 640: Sample UI, Hidden-Navigation Mode, Nexus 4/Android 4.2

Sample UI, Hidden-Navigation Mode, Nexus 10/Android 4.2
Figure 641: Sample UI, Hidden-Navigation Mode, Nexus 10/Android 4.2

Similarly, you can hide the status bar by passing View.SYSTEM_UI_FLAG_FULLSCREEN to setSystemUiVisibility(). However, despite this flag’s name, it does not affect the navigation or system bar:

Sample UI, Full-Screen Mode, Nexus 4/Android 4.2
Figure 642: Sample UI, “Full-Screen” Mode, Nexus 4/Android 4.2

Sample UI, Full-Screen Mode, Nexus 10/Android 4.2
Figure 643: Sample UI, “Full-Screen” Mode, Nexus 10/Android 4.2

Hence, to hide both the status bar and the navigation or system bar, you need to pass both flags (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION):

Sample UI, True Full-Screen Mode, Nexus 4/Android 4.2
Figure 644: Sample UI, True Full-Screen Mode, Nexus 4/Android 4.2

Sample UI, True Full-Screen Mode, Nexus 10/Android 4.2
Figure 645: Sample UI, True Full-Screen Mode, Nexus 10/Android 4.2

Note that showing and hiding the ActionBar is also possible, via calls to show() and hide(), respectively.

Offering a Delayed Timeout

Android makes it easy for activities to keep the screen on while the activity is in the foreground, by means of android:keepScreenOn and setKeepScreenOn().

However, these are very blunt instruments, and too many developers simply ask to keep the screen on constantly, even when that is not needed and can cause excessive battery drain. That is because it is very easy to always keeps the screen on.

Say, for example, you are playing a game. Keeping the screen on while the game is being played is probably a good thing, particularly if the game does not require constant interaction with the screen. However, if you press the in-game pause button, the game might keep the screen on while the game is paused. This might lead you to press pause, put down your tablet (expecting it to fall asleep in a normal period of time), and then have the tablet keep going and going and going… until the battery runs dead.

Whether you use setKeepScreenOn() or directly use a WakeLock, it is useful to think of three tiers of user interaction.

The first tier is when your app is doing its “one big thing”: playing the game, playing the video, displaying the digital book, etc. If you expect that there will be periods of time when the user is actively engaged with your app, but is not interacting with the screen, keep the screen on.

The second tier is when your app is delivering something to the user that probably would get used without interaction in the short term, but not indefinitely. For example, a game might reasonably expect that 15 seconds could be too short to have the screen time out, but if the user has not done anything in 5-10 minutes, most likely they are not in front of the game. Similarly, a digital book reader should not try to keep the screen on for an hour without user interaction.

The third tier is when your app is doing anything other than the main content, where normal device behavior should resume. A video player might keep the screen on while the video is playing, but if the video ends, normal behavior should resume. After all, if the person who had been watching the video fell asleep, they will not be in position to press a power button.

The first and third tiers are fairly easy from a programming standpoint. Just acquire() and release() the WakeLock, or toggle setKeepScreenOn() between true and false.

The second tier — where you are willing to have a screen timeout, just not too quickly — requires you to add a bit more smarts to your app. A simple, low-overhead way of addressing this is to have a postDelayed() loop, to get a Runnable control every 5-10 seconds. Each time the user interacts with your app, update a lastInteraction timestamp. The Runnable compares lastInteraction with the current time, and if it exceeds some threshold, release the WakeLock or call setKeepScreenOn(false). When the user interacts again, though, you will need to re-acquire the WakeLock or call setKeepScreenOn(true). Basically, you have your own inactivity timing mechanism to control when you are inhibiting normal inactivity behavior or not.

To see the second tier in action, take a look at the MiscUI/DelayedTimeout sample project.

The UI is a simple button. We want to keep the screen awake while the user is using the button, but let it fall asleep after a period of inactivity that we control. To accomplish this, we will use a postDelayed() loop, to get control every 15 seconds to see if there has been user activity:

package com.commonsware.android.timeout;

import android.app.Activity;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;

public class MainActivity extends Activity implements Runnable {
  private static int TIMEOUT_POLL_PERIOD=15000; // 15 seconds
  private static int TIMEOUT_PERIOD=300000; // 5 minutes
  private View content=null;
  private long lastActivity=SystemClock.uptimeMillis();

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    content=findViewById(android.R.id.content);
    content.setKeepScreenOn(true);
    run();
  }

  @Override
  public void onDestroy() {
    content.removeCallbacks(this);

    super.onDestroy();
  }

  @Override
  public void run() {
    if ((SystemClock.uptimeMillis() - lastActivity) > TIMEOUT_PERIOD) {
      content.setKeepScreenOn(false);
    }

    content.postDelayed(this, TIMEOUT_POLL_PERIOD);
  }

  public void onClick(View v) {
    lastActivity=SystemClock.uptimeMillis();
  }
}
(from MiscUI/DelayedTimeout/app/src/main/java/com/commonsware/android/timeout/MainActivity.java)

In onCreate(), we call setKeepScreenOn(true) to keep the screen on, regardless of what the user’s default timeout is. Then, we call the run() method from our Runnable interface (implemented on the activity itself). run() sees if 5 minutes has elapsed since the last bit of user activity (initially set to be the time the activity launches). If 5 minutes has elapsed, we revert to normal screen-timeout behavior with setKeepScreenOn(false). We also schedule ourselves, as a Runnable, to get control again in 15 seconds, to see if 5 minutes has elapsed since the last-seen activity. Our button’s onClick() method simply updates the last-seen timestamp, and onDestroy() cleans up our postDelayed() loop by calling removeCallbacks() to stop invoking our Runnable.

The net is that the device’s screen will remain on for 5 minutes since the last time the user taps the button, even if the user’s default screen timeout is set to shorter than 5 minutes. Yet, at the same time, we do not keep the screen on forever, causing unnecessary battery drain.

Note that to test this, you will probably need to unplug your USB cable after installing the app on the device (since many developers have it set up to keep the screen on while plugged in). Also, you will need to set your device’s screen timeout to be under 5 minutes, if it is not set that way already.

This is a primitive implementation, missing lots of stuff that you would want in production code (e.g., it never calls setKeepScreenOn(true) if we flipped it to false but then tap the button). And the complexity of determining if the user interacted with the screen will be tied to the complexity of your UI.

That being said, by having a more intelligent use of WakeLock and setKeepScreenOn(), you can deliver value to the user while not accidentally causing excessive battery drain. Users do not always remember to press the power button, so you need to make sure that just because the user made a mistake, that you do not make it worse.