HELP POPUP

The Problem

We wanted to show text messages explaining how to use the app.  The standard tool would be a Toast message, but this has a few problems: it's temporary, it doesn't align to anything (if we want to point at a button to explain what it does), and there can be only one on the screen at a time.  A Popup window solves some of these problems, but doesn't allow for explicit positioning (alignment is only in one direction).

The Solution

We build the HelpPopup around a Popup window, filling it with a TextView which we can style.  We determine the size of the contents and the position of the anchoring target before showing it, and use that to position the popup.  We add a callback when the popup is dismissed so we can build a general help system (see Help).  We also can draw an arrow in the popup pointing at the anchoring widget.

 

 
HelpPopup example
 
 

Code

The tarball/zip file contains the source file HelpPopup.java and resource files for the content's layout, style, and text coloring, and a frame to draw around the popup.  Place these files in a library (as the path indicates, we use a library called 'widgets'), or copy them directly into your project.  You can customize the resource files, or replace them completely (which might mean changing their reference IDs in internal parameters in the class).

 

To create the popup, create a new instance of HelpPopup.  It will populate the TextView, calculate its size, and determine where to place the popup before showing it.  The popup requires a tap anywhere on the screen, or a press on the trackball or D-Pad, to dismiss.

 

The signature for the constructor has a fair number of arguments:

 

    HelpPopup(Activity app, View root, int anchorID, int pos, int msgID,

              int parentID, OnHelpDismissListener listener)

 

    HelpPopup(Activity app, View root, int anchorID, int pos, CharSequence msg,

              int parentID, OnHelpDismissListener listener)

 

We have defined two different sets of constants to position the popup.  At a high level, you can use  compass directions - N, E, SW, and so forth.  These put the help window outside the anchor in the corner requested.  There are also four corners for the inside - NEIN, SEIN, SWIN, SWIN.  These twelve constants are abbreviations for the lower-level flags.  Here we define PLACE_[ABOVE | BELOW | LEFT | RIGHT]_[OUT | IN].  The OUT parameters will sit outside the anchor, the IN inside.  These can be bitwise combined.  We have also defined PLACE_CENTER to center the popup above the anchor, PLACE_1_4 to center it horizontally at 1/4 of the height from the top, and PLACE_3_4 to center it horizontally at 3/4 the height.

 

 
Popup positioning constants
 
 

By default the help window will include a small arrowhead pointing at the anchor (although not for the three center placements).  You can remove it by adding PLACE_NOARROW to the position flags.

 

There are two other methods available publicly:

 

    didPopupLaunch()

    dismissWithoutCallback()

 

didPopupLaunch returns true if the popup is on the screen, false if not (for example, if the anchor wasn't visible).  dismissWithoutCallback removes the popup without calling the listener

 

The files needed for the popup are:

 

 

Test Program

The test program draws a box on the screen, and has a grid of buttons below it.  (You'll need a large screen resolution to see everything.)  Pressing a button will show a help text in the same relative position with the position code as the text.  The test is to simply check the position of the popup and its text.

 

 
HelpPopup test program
 
 

The files for the test activity are:

 

Note that strings for the UI are hard-coded in the XML layout and TestActivity.

 

Code Notes

No known restrictions on API level.  Tested back to API 8.

 

Source

Source as tarball / zip file.

 

Under The Hood

Before creating the popup, we have to determine the layout of the help window and its size so we can position the popup correctly.  At the top level we do:

 

We make three tests that the anchor is visible.  First, check if the widget is being shown.  Second, check that the anchor point is visible in the parent's visible area (if it's provided) or on screen (if not).  Third, check that the widget is visible.  We have several built-in parameters to guide this - change the constants at the top of the class to alter the behavior.  If requireAllOnScreen is OnScreen.PART then the test passes if any part of the anchoring widget is visible.  If OnScreen.ALL, then most of the widget must be on the screen - the parameters ALLON_FRAC and ALLON_BIGFRAC define "most".  If OnScreen.STRICT_ALL, then the whole widget must be visible.  In practice OnScreen.ALL is sufficient.  In the NH 4000 Footers app we sometimes have large text boxes as anchors, and we only need half visible in the scroll view to show a help text.

 

The calculation of the anchor position and the arrow is straightforward.

 

Determining the size of the help window is a bit tricky since we are doing it before the layout of the widget.  We need to simulate the layout phase, and use a StaticLayout, adjusting the width of the text and checking the height to satisfy the goals for the aspect ratio and the text fitting on the screen.  If the text width is less than the parameter value MAXLINELEN then we show it one line, otherwise we start with a square box (calculated from the area of the single-line text times the line height) and scale the width to the change in height as we reduce the number of lines, until the width is larger than the height or the text runs off the screen.  This can leave extra space on the right edge when done, so we trim it to the size of the longest line.

 

We use an external layout file to create the widget to put in the popup.  The internal parameter ID_CONTENTS defines the reference ID (R.layout.*) for the file.  The popup does assume that this is a TextView.  You can apply a style in the layout to the text in the window; this was the cleanest way we found to do so.  The popup does not support drawables embedded in the view if you're drawing arrows, which are set as the background compound drawable.  We wrap the TextView in a FrameLayout so we can apply a second drawable, a frame, around it.  The parameter ID_FRAME defines the reference ID (R.drawable.*) for it.  You can use the built-in Toast frame (android.R.drawable.toast_frame), or use the one supplied with the sample code (which is smaller and positions better on the screen).

Known Issues

Determining the size of the text window could be better.  It would be easy enough to add support for different aspect ratios (changing the exit test when iterating the width so that it's a given fraction of the height), but we still have to grapple with Android's line-breaking algorithm, which seems crude.  The procedure will happily leave a single word or two on a line, when the layout would have a better balance if it were a bit wider, with all lines more evenly filled.  Help texts with short words have better break points for layout.

The help popup will often generate warning/error messages on the debugger log:

 

    Input-JNI   Sending finished signal for input channel 'xxx PopupuWindow:yyy

    (client)' since it is being unregistered while an input message is in progress.

 

    Input-JNI   Ignoring finish signal on channel that is no longer registered.

 

We have been unable to block these errors, which occur deep in Android's innards.  The problem is that the event queue is not empty when dismissal occurs, and there is no way to clear the queue, or to wait until it's empty before dismissing, or to shut it off and delay dismissal.  Looking at the source, the messages are just warnings that this is happening, and the handler will drop the extra events.  We have changed dismissal to occur after UP events, not down or motion, which minimizes the chance that the queue still has something in it, but quick taps will still raise the warning.

 

While testing with the Monkey we had occasional null pointer exceptions in the built-in PopupWindow code.  If you set a background for the popup, then the code wraps the contents you pass in a FrameLayout in which it pastes the background (like we do for the frame).  The frame is given handlers for touches and key events, and as the Monkey is spamming keys the popup can get in a state where it has no default key dispatcher (see the dispatchKeyEvent() method to the inner class PopupViewContainer).  We avoid this by using our own handlers to the frame we create.  There is likely some functionality missing (we did not copy the code for accessibility events and drawable states), but it's not needed for this simple text-based help system.

Feedback

If you have questions or comments about the code, or improvements, please contact us.