![]() |
ScreenManager v1.0.0
A lightweight C++ library for managing application screens, navigation, transitions and display timing.
|
Please see main README.md for a functional overview.
ScreenManager is GFX agnostic. It has no knowledge of the display, graphics library, widgets, or rendering mechanism used by an application. A Screen is responsible for its own rendering if required. ScreenManager's only involvement in display rendering is determining when the current screen's
draw()method is called, based on the configured refresh rate. This allows the same ScreenManager to be used with any display or graphics library or with screens that do not render anything themselves.
>The registered Routers provide a powerful way to manage transitions between screens or create history trees & wizards.
The ScreenManager:
requestScreen() and optional Router(s).IManagedScreen at the appropriate times.A Screen must be registered with the ScreenManager before it can be used (managed).
By default the first screen registerd becomes the initial screen. This can be overridden with setInitialScreenId().
All app screens must be implement/derived from the IManagedScreen interface and managed screens must have a default constructor.
At registration each IManagerScreen is identified with a ScreenId. This is just a uint8_t (255 screens is enough?) and it is up to the App to ensure these are unique.
myScreenManager.registerScreen() will return false if a duplicate ScreenId is used.
Suggested ScreenId implementation:
I also use a typedef: using ScreenId = uint16_t;
The life of an external screen is managed by the app. It must out live the ScreenManager. These are normally the 'core' screens of your app.
If a Screen is registered before myScreenManger.begin() is called its begin() method will be called from myScreenManger.begin(), otherwise the screen's begin() will be called when it is registered.
For the rest of the screen's lifecycle, please see Screen Lifecycle below.
The life of a managed screen is managed by the ScreenManager (that was a surprise, wasn't it?). These are normally resource intensive screens or seldom used ones.
A managed screen's begin() is called every time the screen becomes the current screen (before start()).
For the rest of the screen's lifecycle, please see Screen Lifecycle below.
Managed screens should only hold state that is relevant while they are active - this is also recommended for external screens. Screen may occassionally be the'View' of MVC but it is not the 'Model'.
See Screen Access to Your App below.
The IManagedScreen interface specifies the lifecycle methods:
begin()**: As described above, will be called at the appropriate time for either an external or managed Screen.start()**: Called when the Screen become current. Used to setup widgets etc and to read current state.draw()**: When the current Screen, the draw() method is called repeatedly from ScreenManager::update() at the configured refresh rate, allowing the screen to update its visible state independently of the application's main loop.end()**: A screen may prevent a transition by returning false from end(). Forced transitions still call the current screen's end() method, but ignore the return value.Before any transition takes place, registered Screen Routers are given the opportunity to examine the requested transition. A router may allow the transition to proceed unchanged, redirect it to a different screen, or prevent it altogether, enabling application-specific navigation policies without coupling individual screens together.
When a Screen transition occurs, ScreenManager coordinates the lifecycle of both screens. See Routers below.
Most screens will need access to app level resources such as state and inputs so will need an application-level mechanism for locating the application instance. For external screens, this is easy as it can be passed in the constructor but for managed screens a different approach is required.
Recommendation is to pass a single App object that contains references to all other resources.
Example using extern:
File: app.h
File: app.cpp
Then in an app screen:
Alternatively create a global function:
And in the app screen:
I think I prefer the latter and highly recommend creating a BaseAppScreen to encapsulate all the common behaviour.
*** Routers are entirely optional. ***
If your app only requires navigation from one screen to another, then calling myScreenManager.requestScreen(nextScreenId) will do just that - no routers required. By default this will only fail if the requested screen has not been registered or current screen returns false from end() (to force a transition use myScreenManager.requestScreen({TransitionIntentType::Force, nextScreenId});)
However, it is often necessary to validate the transition from one screen to another and/or to redirect. This can be done within a screen itself but by centralising app logic into one or more routers, it becomes DRYer and more maintainable. Screens do not need to be aware of each other.
Routers belong to the app. ScreenManager doesn't care what they do internally, it only acts on the return value from resolveScreen().
Routers are evaluated in registration order. A router returning 0 indicates that it has no opinion and evaluation continues. The first router to return a non-zero
ScreenIdresolves the transition and no subsequent routers are called.
A router's sole resolveScreen() method will receive the current ScreenId and a TransitionIntent structure and may or may not return an opinion.
When a router returns 0 it means "I have no opinion on this transition", when it returns a non-zero value it means "Use this as the next screen ID and don't look at other routers"
This means a router can prevent subsequent routers being called by explicitly returning the requested next ScreenId from the TransitionIntent structure (ie it has an opinion).
Returning 0 will not overwrite the requested ScreenId from the TransitionIntent structure and will allow other Routers to have an opinion.
To remain on the current screen, the router would return the current ScreenId.
The simplest intent is just to just pass a ScreenId to requestScreen() but the ScreenTransitionType can be used without a ScreenId to delegate resposiblity for resolving which screen will be the next to Router(s).
myScreenManger.update() is called).ScreenId.ScreenId. Similar to Next but allows a router to determine the intent.setInitialScreenId(). A Router can have an opinion.Next (ie Routers can have an opinion). Current screen's end() will be called but the result ignored. Use with caution.Individual Routers can maintain a history tree or a 'wizard' flow to use with Auto, Next and Back intents for their own area of interest. If a particular Router has no interest, it has no opinion.