User Interface Design Part 5: Common Code and Handling The Screen

In the last article I covered the “Model-View-Controller” code; the way we can think about building interfaces by separating our code into the “model”–the code that does stuff–from the “view”–the code that draws stuff–and the “controller”–the code that controls the behavior of your code.

And we sketched the model code: the low level code which handles the thermostat, storing the schedule and tracking the time. Now let’s start putting it together by building common code for drawing the stuff on our screen.


One element of our user interface we haven’t really discussed yet is the entire screen itself.

Our user interface contains a number of display screens:

Home Screen Final

Temperatures

Schedule Picker Screen

But we’ve given no consideration as to how we will switch screens, how we will move back, or the “lifecycle” of each of our screens. Further, in our limited environment, as we’re not building a hierarchy of views, our “view” that is to be controlled is essentially the bits and pieces of code that draw and manage our screen–and it’d be nice if we had a way to manage this in a consistent fashion.

For our class we will make use of C++’s “inheritance”, and create a base class that can be overridden for all of our screens. This will allow us to put all the repetitive stuff in one place, so we can reduce the size of our app and reduce the amount of code we have to write.


So what should our screen do?

Switch between screens

Well, our current interface design has this notion of multiple screens and a “back” or “done” button which allows us to pop back to the previous screen. So let’s start by building a class that helps track this for us.

class AdaUIPage
{
    public:

        /*
         *  Global management
         */

        static void     pushPage(AdaUIPage *page);
        void            popPage();
};

These simple methods can track the list of displays that we are showing, maintaining a “stack” of screens, with the currently visible screen on top.

When we tap on a button–such as the fan button on our main screen–we load our new temperature setting screen “on top” of our main screen, effectively pushing our new screen on a stack:

Screen Stack

When we tap the “Done” button, we can then “pop” our stack, leaving us the main screen.

The code for these methods are very simple. First, we need a global variable that represents the top of the stack, and for each page we need a class field which represents the page after this one on the stack:

...
        static void     pushPage(AdaUIPage *page);
        void            popPage();
    protected:
        /*
         *  Linked list of visible pages.
         */
        
        AdaUIPage       *next;
        static AdaUIPage *top;
...

And our methods for adding a page are very simple:

void AdaUIPage::pushPage(AdaUIPage *page)
{
    page->next = top;
    top = page;
}

void AdaUIPage::popPage()
{
    if (next) {
        top = next;
    }
}

Notice what happens. When we call ‘pushPage’ we cause that page to be the top page, and we save the former top page away as our next page. And when we pop a page, we simply set the top page to the page that was behind this page.

Now when we show or hide the top page, we’d like a way to signal to the page that it should redraw itself. After all, thus far we’ve just manipulated a linked list.

There are two ways we could do this. Our first option is that we could simply call a ‘draw’ method to draw the page. But it may be that our page code may want to do some work before we show the page. So a second option is to use an “event-driven environment.”

Handle Events

All modern user interfaces are event-driven. An “event-driven environment” is just a fancy way of saying that our code will run in a loop, and look for things to do. If it finds something that needs doing, it then calls some piece of code that says “hey, this needs doing.”

And we’ve seen this before on other Arduino sketches.

All Arduino sketches start with a “setup” call which you can use to set up your sketch, and then it repeatedly calls the “loop” call. And the idea is that in your “loop” call you write code similar to:

void loop()
{
    if (IsButtonPressed(1)) {
        DoSomethingForButtonOne();
    } else if (IsButtonPressed(2)) {
        DoSomethingForButtonTwo();
    } ...
}

In other words, in your loop you look at all the things that need to be done, and if something triggers an event, you call the appropriate routine to handle that event.

Well, we’re going to do the same thing here. And for our screen, there are two primary events we want to track: if we need to redraw the screen, and if the user tapped on the screen.

So let’s add a new method, processEvents which we can call from our Arduino sketch’s loop function. And while we’re at it let’s also add code to track if the screen needs to be redrawn. To do we create a new field which sets a bit if the screen needs drawing and (because it’ll be useful later) if just the content area needs redrawing. We’ll also add two methods to set the bit indicating we need to redraw something:

...
        static void     pushPage(AdaUIPage *page);
        void            popPage();

        static void     processEvents();

        /*
         *  Page management
         */

        void            invalidate()
                            {
                                invalidFlags |= INVALIDATE_DRAW;
                            }
        void            invalidateContents()
                            {
                                invalidFlags |= INVALIDATE_CONTENT;
                            }
    protected:
        /*
         *  Linked list of visible pages.
         */

        AdaUIPage       *next;
        static AdaUIPage *top;

    private:
        void            processEvents();

        uint8_t         invalidFlags;       // Invalid flag regions
...

Our periodic events then can check to see if we need to redraw something by checking if we even have a top page–and if we do, then ask the page to handle events.

void AdaUIPage::processEvents()
{
    if (top == NULL) return;
    top->processPageEvents();
}

And for our process page internal method, here we check if the page needs redrawn and do the page drawing.

/*  processPageEvents
 *
 *      Process events on my page
 */

void AdaUIPage::processPageEvents()
{
    /*
     *  Redraw if necessary
     */

    if (invalidFlags) {
        if (invalidFlags & INVALIDATE_DRAW) {
            draw my page
        } else if (invalidFlags & INVALIDATE_CONTENT) {
            clear our content screen
            draw our contents page
        }
        invalidFlags = 0;
    }
}

All this implies we also need a method to draw, a method to draw just the contents area, and a way to find the content area of our screen.

Handling when a screen becomes visible and when it disappears

Now when we call our methods to show and hide a screen, we need to do two things.

First, we need to mark the top screen as needing to be redrawn.

Second, we need to let the screen that is appearing that it is appearing, and the screen that is disappearing that it is disappearing. This is because the appearing screen may want to do some setup (such as getting the current time or temperature), and because the screen that is disappearing may want to save its results.

So let’s extend our pushPage and popPage methods to handle all of this.

First, let’s add two more methods that can be called when the page may appear and when the page may disappear:

...
        void            invalidateContents()
                            {
                                invalidFlags |= INVALIDATE_CONTENT;
                            }

        virtual void    viewWillAppear();
        virtual void    viewWillDisappear();
    protected:
        void            processPageEvents();
...

The implementation of these methods do nothing by default; they’re there so if we create a page we can be notified when our page appears and when it disappears.

Now let’s update our push and pop methods:

void AdaUIPage::pushPage(AdaUIPage *page)
{
    if (top) top->viewWillDisappear();
    if (page) page->viewWillAppear();

    page->next = top;
    top = page;

    top->invalidFlags = 0xFF;    // Force everything to redraw
}

void AdaUIPage::popPage()
{
    if (next) {
        viewWillDisappear();
        next->viewWillAppear();

        top = next;
        top->invalidFlags = 0xFF; // Force everything to redraw
    }
}

Drawing our screen

Notice our original design. We can have one of two screens: one with a list of buttons along the left–

Basic Inverted L

–and one without–

No Inverted L

Since all of our screens are laid out like this, it’d be nice if we handled all of the drawing in this class, so that our children class can focus on just drawing the title area and the content area.

That is what our AdaUIPage::draw() method does.

I won’t reproduce all of the code here; it’s rather long. But it does make extensive use of our AdaUI class to draw the title bar and the side bar.

To get the name of the titles we need to initialize our page with some information–such as the title of our page, the name of the back button (if we have one), the list of button names, and a list of the location of the buttons that the user can tap on. (We’ll use that list below.)

Our constructor for our class–which is then overridden by our screens–looks like:

/*  AdaUIPage::AdaUIPage
 *
 *      Construction
 */

AdaUIPage::AdaUIPage(const AdaPage *p)
{
    page = p;
    invalidFlags = 0xFF;        // Set all flags
}

The page variable is a new field we add to our class, and of course we mark the page as invalid so it will be redrawn.

And the contents of our page setup structure looks like:

struct AdaPage
{
    const char *title;          // title of page (or NULL if drawing by hand)
    const char *back;           // back string (or NULL if none)
    const char **list;          // List of five left buttons or NULL
    AdaUIRect  *hitSpots;       // Hit detection spots or NULL
    uint8_t    hitCount;        // # of hit detection spots
};

Everything in the AdaPage object is stored in PROGMEM. And a typical page may declare a global that sets these values, so our page code knows what to draw for us.

Our page drawing then looks like this–in pseudo-code to keep it brief:

  1. Erase the screen contents to black.
  2. Draw the title of our page using the title if given, drawTitle() if not.
  3. Draw our back button if we have one
  4. Do we have any left buttons?
    • True: Draw the inverted L with our left buttons.
    • False: Draw a blank title bar if not.
  5. Call drawContents() so our class knows to draw its contents

Handling taps

Our Adafruit TFT Touch Shield contains a nice capacitive touch panel, which can be accessed using the Adafruit FT6206 Library. We use this library to see if the user has tapped on the screen, determine where on the screen the user has tapped, and call a method depending on what was tapped. By putting all this tapping code in our main class, our screens only need to do something like this to handle tap events:

void MyClass::handleEvent(uint8_t ix)
{
    switch (ix) {
        case MyFirstButton:
            doFirstButtonEvent();
            break;
        case MySecondButton:
            doSecondButtonEvent();
            break;
....
    }
}

Now the nice thing about the AdaPage structure we passed in is that we now can know if we have a back button, what side buttons we have, and the location of the buttons we’re drawing on the screen.

Note: Normally if we had more program memory we’d define a view class which can then determine the location of itself, and draw itself.

But for us, we need to deal with static text titles, dynamic button titles, and all sorts of other stuff–and 32K of program memory combined with 2K of RAM is just not enough space. So we “blur” the line between views and control code–by separating out the tapping of views from the displaying of views. This may make life a little harder on us–if the buttons our content code does not align with the rectangles we pass to our constructor, things can get sort of confusing. But it does save us space on a small form factor device.

Our tap code needs to track if the user is currently touching it; we’re only interested when the user taps and not when he drags. (If we don’t, we’ll send events over and over again multiple times just because the user didn’t lift his finger fast enough.) We do this by using a lastDown variable; if it is set, we’re currently touching the screen.

So we add code to our processPageEvents method to test to see if the touch screen has been tapped. In pseudo code our processPageEvents code does the following:

  1. Is the touch screen being touched, and lastDown is false?
    • True:
      1. Set lastDown to true.
        1. Get where we were tapped.
        2. Was the back button tapped?
          • True: Call popPage() and exit.
        3. Did we tap on one of the left buttons?
          • True: Call handleEvent() with the index of the left button and exit.
        4. Did we tap on one of the other buttons?
          • True: Call handleEvent() with the index of the button and exit.
        5. If we get here nothing was tapped on. Call handleTap() to give the code above a chance to handle being tapped on.
  2. If the screen is not being touched, set lastDown to false.

Summary

So in the above code we’ve created a base class which handles most of our common event handling and screen drawing stuff. This means a lot less work as we create our individual screens on our Thermostat.

And note that we were able to do all of this–creating common code for handling our screens–by having a design language that made things consistent.

That’s because consistency makes things easier to implement: we now have reduced our problem of a big, empty screen by defining exactly how our screens should work–and doing so in a consistent way that allows us to reuse the same code and reuse the same basic drawing algorithms over and over and over again.

And when we have a list of consistent screens, we then become discoverable: the user only has to learn one set of actions and learn one consistent visual language to use the thermostat.

Of course from this point, the rest of our thermostat is simply an exercise in using our two primary tools: AdaUI which draws our visual language, and AdaUIPage which handles the common behavior of our pages, to create our user interface and hook them up to our model.

For next time.

Meanwhile, all of the code above is contained at GitHub and the finished product can be downloaded and installed on your own Adafruit Metro/TFT touch screen.

Published by

William Woody

I'm a software developer who has been writing code for over 30 years in everything from mobile to embedded to client/server. Now I tinker with stuff and occasionally help out someone with their startup.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s