HELP SYSTEM

The Problem

Building on the HelpPopup, we wanted our application to show a series of texts explaining its use.  The system would be used in several ways:

 

 

The Solution

The Help System is a simple state machine, where a state can specify a successor and the onDismiss callback from the popup advances through the series.  Each state stores the setup for the HelpPopup - the message, the anchoring widget, and the position.  We add methods to start traversing, or to reshow the last, or force a message onto the screen.  The system has a large number of public functions to fit the diverse use cases.

Setting Up the System

 

These are functions to add and delete help states.

 

    addHelp(int stateID, int achorID, int textID, int scrollID, int pos,

            Integer nextStateID)

    addHelp(int stateID, int achorID, int textID, CharSequence msg, int pos,

            Integer nextStateID)

 

Add a state to the system.  Note that there can be more than one message associated with a state.  Each will be shown simultaneously in separate popups.  If there is a set, there can be only one nextStateID, or a RuntimeException is raised (it's OK to pass one nextStateID and the rest null).  There is essentially one namespace for all state identifiers, so avoid collisions between activities, dialogs, and fragments.

 

 

    deleteHelp(int stateID)

 

Remove the popups associated with stateID.  The history for this state is not cleared, so if the states are later registered they can still be considered 'shown'.  If this state is currently being displayed then it remains on the screen.

 

    deleteAllHelp()

 

Remove all help registrations.  The history is not cleared; use clearHistory for that.

 

    clearHistory(int stateID)

 

Forget if we've shown stateID before.  The next time showHelp(stateID) is called, the popup will appear.

 

    clearAllHistory()

 

Forget that we've shown any states.

 

Running the System

 

These are functions for using the system.

 

    showHelp(int stateID)

 

Show the help with the given stateID, and any follow-on states.  Messages will not be shown if they've already been seen, but the system will continue going down a series until the end, so if the messages stopped halfway through they will resume there.  stateID is remembered as the last one shown.

 

    showHelpWithoutFollowOn(int stateID)

 

Show just the help with the given stateID, but nothing else in the chain.  The message will not be shown if it's already been seen.  stateID is remembered as the last one shown.

 

    forceHelp(int stateID)

 

Show the help with the given stateID and any follow-on states, no matter if they've been seen or not.  The state is not remember as the last one shown.

 

    forceHelpWithoutFollowOn(int stateID)

 

Show just the help with the stateID, whether it's been seen or not.  The state is not remembered as the last one shown.

 

    forceHelpAndRemember(int stateID)

 

Show the help with the given stateID and any follow-on states, no matter if they've been seen or not.  stateID is remembered as the last one shown.

 

    reshowLast()

 

Show the last message that was remembered, and any follow-on states, whether they've been seen or not.  This is equivalent to forceShow(lastStateID).

 

    registerRootView(View v, StoreAction storeAction)

 

Change the root view to v for searching for the state's anchor.  Some view hierarchies are not visible to the application, like dialogs, and we need to supply the root view for them in order to find the anchors.  Pass null for v to change back to the main activity's layout.  Note that remembered states are not necessarily valid when switching the root views.  Use StoreAction.SAVE to copy the last state shown into a backup variable when switching away from the activity, and StoreAction.RESTORE to bring it back, when switching back.  StoreAction.FORGET does not make the backup; if you don't have the possibility to re-show help messages then this is the correct value to pass.  The root view holds until the next call to this method.

 

    setNormalHelpDisabled(boolean disablehelp, boolean registerAsLast)

 

Global on/off switch for the help system (off if disablehelp is false).  When off this blocks normal help messages.  Forced messages will still appear.  If registerAsLast is true then any showHelp requests will still be remembered for reshowLast().

 

    isNormalHelpDisabled()

 

Returns true if we're blocking help messages.

 

    isHelpStateDefined(int stateID)

 

Returns true if there are any number of help messages defined for stateID, false if there are none.

 

Lifecycle

 

    notifyUIUp(Integer stateID)

 

The help system starts by waiting for the UI to come up.  It will remember the first state shown during this period, but it does not try to create a popup because the anchoring widget will not be visible.  Call this when you know layout is done and the anchor is visible, preferably by registering a global layout listener in the main activity's onCreate(), where v is the top widget in the main hierarchy:

 

    v.getViewTreeObserver().addOnGlobalLayoutListener(

      new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override

        public void onGlobalLayout() {

          helpsys.notifyUIUp(null);

        }

      });

 

If there was no previous state shown, then the system will display stateID if it's not null.

 

    waitForUIUp()

 

Reset the system so it thinks it's still waiting for the notifyUIUp() call.  Use this from onPause() or onStop() when the activity is no longer visible.  Call before you try to show a state, and of course follow it at some point with notifyUIUp().

 

    destroy()

 

Close down the system.  Any popups on the screen are dismissed, the internal history cleared, and the system waits for the UI up notification.  State definitions still exist, so it's OK to call this from onStop() and then to come back from onResume().

 

    savePopupState(Bundle savedstate)

 

The information needed for the UI is stored in the savedstate bundle.  The shown history is not - this is like the information you want to preserve across an onStop(), not onDestroy().  Call this from onSaveInstanceState().

 

    restorePopupState(Bundle savedstate)

 

Retrieve the UI information from the savedstate bundle.  If the system is waiting for the UI to come up, then the state that was showing is set as the one to re-show, even if there was another previous showHelp() attempt.  If the UI is ready, then the last state is shown again.  Call this from onCreate() or onRestoreInstanceState().

 

    saveHelpState()

 

Write the shown help history to the application's shared preferences.  Call this from an activity when it pauses.  The database keys all begin with "help_".  Note that the state definitions are not saved, only which have been shown.  The history is bit-encoded, so keep the identifiers as small integers to avoid taking a lot of room in preferences.

 

    restoreHelpState()

 

Unpack the help system history.  This does not cause the last state that was showing to show again; call showState() to do that.

 

Using the System

 

We set up the help system in the activity's onCreate().  The code will look something like

 

    Help helpsys;

    @Override

    public void onCreate(Bundle savedstate) {

      helpsys = new Help(this);

      registerHelp();

      if (null != savedstate) {

        helpsys.restorePopupState(savedstate);

      }

      if (!isHelpEnabled()) {

        helpsys.setNormalHelpDisabled(true, false);

      }

      helpsys.restoreHelpState();

      View topview = findViewById(R.id.app_top);

      topview.getViewTreeObserver().addOnGlobalLayoutListener(

        new ViewTreeObserver.OnGlobalLayoutListener() {

          @Override

          public void onGlobalLayout() {

            helpsys.notifyUIUp(null);

          }

        });

    }

 

Here helpsys is a class variable, registerHelp() sets up all the states (basically a long list of addHelp() calls), and isHelpEnabled() checks an application setting to see if the user wants to see help messages.

 

In onPause() we save the help setup to preferences.

 

    @Override

    public void onPause() {

      helpsys.saveHelpState();

      helpsys.waitForUIUp();

    }

 

In onDestroy() we shut it down.

 

    @Override

    public void onDestroy() {

      helpsys.destroy();

    }

 

And in onSaveInstanceState() we store the UI-specific part of the system.

 

    @Override

    public void onSaveInstanceState(Bundle savedstate) {

      helpsys.savePopupState(savedstate);

    }

 

It's also useful to define wrappers to showHelp() and forceHelp() that will correctly switch between the activity's view and another root.

 

    void showHelp(int stateID) {

      showHelpOnDialog(stateID, null);

    }

    void showHelpOnDialog(int stateID, View dlgView) {

      helpsys.registerRootView(dlgView, StoreAction.SAVE);

      helpsys.showHelp(stateID);

    }

 

And with this you can start help sequences or show warning messages as needed.  For example, to show tutorial messages for a dialog we'll put a showHelpOnDialog() call in the onShow() listener for the dialog, which is called after layout is done and the anchoring widgets are visible.

 

Use Scenarios

 

Looking back at the requirements, how do we put this system to use?

 

 

Sample Code

The tarball/zip file contains one source file, Help.java.  The HelpPopup package is not included; get it here.

 

Test Program

The test program has two screen.  The main activity displays five buttons.  When it starts the first time you'll see a sequence of help texts pointing to each.  Some of the help states have multiple pop-ups on them.  There are four help states.  The first gives an introduction centered on the activity, the second explains the top two buttons (texts to the left and right), the third the third and fourth buttons (placed northwest and northeast), and the fourth the bottom button (placed above).  Once you're done tapping through the messages you can use the buttons.

 

 

Help test program main activity
 
 
Help test program
 
 

The dialog help sequence has two states.  The first is an introduction centered on the frame, the second has two messages for the two widgets inside.  The button calls reshowLast(), which repeats the series even if it's been seen.

 

The help messages will re-appear if you rotate the screen at any time, with one exception: the reshow sequence on the dialog does not start again, because the dialog starts a normal cycle when it appears which, since the help has already shown, means nothing is visible.

 

The files for the test activity are:

 

Code Notes and Issues

 

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

 

If you follow the usage guidelines for dialogs, then the help system will restore itself after a rotation for the sequence that's started from the dialog's onShow().  Maintaining warning messages after a rotation is more difficult, since you'll need to save the fact that a popup was showing and its state, and re-create it with the dialog.  It's not impossible to recover warnings, but it does take storing more information globally.  In this case it might help to add access to the stateShowing variable in the Help class.

 

Bringing up the help system in the correct state was one of the hardest parts of the NH 4000 Footers app to get right, because of the interplay between the fragments and activities coming up.  Be sure to verify in your testing that help messages re-appear, the correct ones are shown for reshowLast(), and the history is correct as the activity pauses, resumes, stops, and goes through configuration (orientation) changes.

Source

Source as tarball / zip file.

Feedback

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