Navigation
The Navigation Framework library structures a multi-page application. It turns a set of screens into structured navigation between pages identified by a key, maintains the navigation history, and can animate every page change with a transition.
The library builds on MWT: a page produces an MWT Widget, and the navigator mounts it on the application’s Desktop.
It answers the concerns an application would otherwise implement by hand: which page is active, what happens on a back navigation, when a page must build or release its content, and how one page gives way to the next.
Usage
Add the Navigation Framework dependency to the Application project build file:
implementation("ej.library.ui:navigation-framework:2.0.0")
<dependency org="ej.library.ui" name="navigation-framework" rev="2.0.0"/>
Then start MicroUI. From the MicroUI thread, initialize the navigator with the desktop it drives and the factory that builds the pages, and navigate to the first page:
public static void main(String[] args) {
MicroUI.start();
final Desktop desktop = new Desktop();
desktop.setStylesheet(buildStylesheet());
MicroUI.callSerially(new Runnable() {
@Override
public void run() {
Navigator.initialize(desktop, new ExamplePageFactory());
Navigator.getInstance().navigateTo(Pages.HOME);
}
});
}
The navigator shows the desktop on the first navigation.
The application does not call Desktop.requestShow() itself.
Every navigator method must be called from the MicroUI thread, hence the MicroUI.callSerially() in the example above.
A navigation requested from a page lifecycle callback or from a navigation listener is not run inline.
The navigator defers it with MicroUI.callSerially() and runs it once the navigation in progress is finished.
Concepts
Navigator
The Navigator is the controller and the entry point of the library.
It is a singleton: the application never constructs one, it configures the instance once with Navigator.initialize(Desktop, PageFactory) and obtains it with Navigator.getInstance().
Every method invoked before initialization throws IllegalStateException.
Initializing again restarts the navigator from a clean state (empty history, no listener, default transition).
The navigator drives the Desktop given at initialization and mounts each page’s content on it.
The application keeps ownership of that desktop: it sets the stylesheet, the render policy and the input handling on it.
Pages and Keys
A page is a factory for its content, not the content itself: the navigator calls getContent() each time the page is about to be shown, so a fresh widget is produced on every navigation to it.
The content can be any MWT Widget, such as one of those provided by the Widget library:
public class HomePage extends Page {
@Override
protected Widget getContent() {
Button button = new Button("Settings");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
Navigator.getInstance().navigateTo(Pages.SETTINGS);
}
});
return button;
}
}
A page is identified in the public API by a non-negative int key.
The application declares its keys as constants:
public class Pages {
public static final int HOME = 0;
public static final int SETTINGS = 1;
public static final int DETAILS = 2;
}
Identity is per history entry, not per key. The same key may appear more than once in the history, and each forward navigation builds a new page.
Page Factory
The application implements one PageFactory, which maps a key to a page:
public class ExamplePageFactory implements PageFactory {
@Override
public Page create(int key, @Nullable Object argument) throws IllegalArgumentException {
switch (key) {
case Pages.HOME:
return new HomePage();
case Pages.SETTINGS:
return new SettingsPage();
default:
throw new IllegalArgumentException("Unknown key.");
}
}
}
The factory returns a fresh page on each call and never caches one.
It is called only by the operations that build a page, navigateTo() and replaceWith().
A back navigation re-shows the page instance the history holds, so it does not consult the factory.
The navigator is not passed to the factory.
A factory, or a page it builds, reaches it with Navigator.getInstance().
Navigation Operations
Four operations change the active page.
Each one has an overload taking an explicit Transition.
Operation |
Effect |
Page origin |
|---|---|---|
|
Pushes a new entry on top of the history. |
Built by the factory |
|
Pops the active entry; the entry beneath becomes active. |
History |
|
Pops back to the nearest entry below the active one with that key, removing every entry above it. |
History |
|
Replaces the active page in place, leaving the history depth unchanged. |
Built by the factory |
Passing Data to a Page
A page receives its data through its own constructor: the framework owns no parameter slot. The two operations that build a page carry an optional argument to the factory, which injects it:
// At the call site: hand the selection to the page being built.
Navigator.getInstance().navigateTo(Pages.DETAILS, selection);
// In the factory: check what was received and inject it.
case Pages.DETAILS:
if (!(argument instanceof Selection)) {
throw new IllegalArgumentException("The details page requires a selection.");
}
return new DetailsPage((Selection) argument);
The navigator keeps no reference to the argument.
It is forwarded to PageFactory.create() and nowhere else, so whatever the page needs from it, the page keeps itself.
Back navigations take no argument: they re-show the page the history holds.
Page Lifecycle
A page is notified when it becomes, or ceases to be, the active page:
onEntered(): the page has become the active page.onExited(): the page has ceased to be the active page.
Both are invoked immediately after the navigation operation, before the transition animation starts, and only on navigation operations. Use them to start and stop what must live only while the page is displayed, such as a timer or a sensor subscription.
Navigation Listeners
A NavigationListener observes page changes from outside the pages, for example to update a breadcrumb, record an analytics trace, or refresh a shared header:
Navigator.getInstance().addNavigationListener(new NavigationListener() {
@Override
public void onNavigation(@Nullable Page exitedPage, Page enteredPage) {
// exitedPage is null on the first navigation.
}
});
The callback fires once per navigation, in lockstep with the page lifecycle callbacks. The listeners notified for a navigation are those registered when its notification pass starts.
History Queries
The navigator exposes the current state of the history:
getActivePage()andgetActiveKey(): the page on top of the history, and its key.getHistory()andgetHistoryKeys(): a copy of the history, oldest first, the active page last.
Transitions
Every page change is animated by a Transition.
A transition draws the incoming page over the outgoing one.
The outgoing page does not move: it is the frame already on the display, and a transition adds the incoming page over it.
The library ships three transitions:
Transition.IMMEDIATE: the incoming content replaces the outgoing one in a single frame. This is the navigator’s initial default.FadeTransition: draws the incoming content over the outgoing one at a growing opacity.SlideTransition: slides the incoming content in from one edge of the content area, in theSlideTransition.RIGHT_TO_LEFT,LEFT_TO_RIGHT,BOTTOM_TO_TOPorTOP_TO_BOTTOMdirection. On a back navigation, the direction is mirrored by default, so going back undoes visually what going forward did. A constructor flag turns the mirroring off.
Both animated transitions are configurable objects, not constants.
Their duration defaults to Transition.DEFAULT_DURATION_MILLIS (300 ms).
Their easing is an ej.motion.Function and defaults to a quadratic ease-out.
Both are set at construction.
A transition applies either to every navigation, as the default, or to a single one:
Navigator navigator = Navigator.getInstance();
// Applies to every navigation made through an overload without a transition.
navigator.setTransition(new FadeTransition());
// Applies to this navigation only.
navigator.navigateTo(Pages.DETAILS, new SlideTransition(SlideTransition.RIGHT_TO_LEFT, 500));
Pointer events are ignored while a transition runs. A navigation requested while a transition is running snaps that transition to its end before proceeding, so a fast sequence of navigations never leaves a page half-animated.
Requirements
The transitions draw over the pixels the previous frame left on the display. The VEE Port must use a buffer refresh strategy that restores the previous frame into the back buffer before each drawing: Single, Predraw, Legacy, or the direct buffer policy.
An animated transition captures the incoming page into an image the size of the page content area, and draws its frames from that image.
The first animated transition allocates the image, and the following ones reuse it.
The images heap must have room for that image: size the ej.microui.memory.imagesheap.size option accordingly.
Navigator.releaseTransitionBuffers() frees the image, for an application that needs the memory elsewhere.
The next animated transition allocates it again.
Transition.IMMEDIATE never allocates it.
Because the incoming page is drawn from a snapshot, a page whose content animates itself is frozen for the duration of the transition.
Disabling the Snapshots
The ej.navigation.transition.snapshots BON constant controls the snapshot image.
The library defines it as true.
To remove the image, define the constant as false in a *.constants.list file of the application:
ej.navigation.transition.snapshots=false
Starting from Architecture 8.3.0, the same constant can also be defined as an Application Option, which takes precedence over the classpath:
microej.constant.ej.navigation.transition.snapshots=false
When the constant is false:
The transitions allocate no image, so the images heap does not need room for one page-sized image.
The frames draw the incoming page widget live.
TransitionContext.drawIncoming()ignores its opacity argument, andTransitionContext.getIncomingImage()returnsnull.SlideTransitionstill animates.FadeTransitionneeds the image to blend, so it becomes an instant page change.
Custom Transition
An application defines its own effect by implementing Transition.
A transition is a painter, not an animator.
It declares its duration with getDuration(), prepares itself in start(TransitionContext), and draws one frame per render(GraphicsContext, TransitionContext, float) call.
The navigator owns the clock: it computes the progress, from 0 inclusive to 1 exclusive, stops at the duration, and then shows the incoming page live.
A transition holds no timer and signals no completion.
A transition whose duration is 0 is completed without a call to start() or render().
The TransitionContext the navigator supplies exposes:
getContentWidth()andgetContentHeight(): the size of the content area.getNavigationKind(): the navigation being animated,TransitionContext.FORWARD,BACKorREPLACE.drawIncoming(GraphicsContext, int, int, int): draws the incoming page with its top-left corner at the given coordinates and at the given opacity.getIncomingImage(): the snapshot of the incoming page, ornullwhen the snapshots are disabled. The image belongs to the navigator: never close it and never draw into it.
The graphics context given to render() is translated and clipped to the content area, and already holds the pixels of the outgoing page.
A frame draws the incoming page over them.
A region a frame leaves untouched keeps showing the outgoing page.
public class WipeTransition implements Transition {
@Override
public int getDuration() {
return 250;
}
@Override
public void start(TransitionContext context) {
// Nothing to prepare: the incoming page is already captured.
}
@Override
public void render(GraphicsContext g, TransitionContext context, float progress) {
// Uncover the incoming page from the left. The outgoing page stays visible on the right.
int width = (int) (progress * context.getContentWidth());
g.intersectClip(0, 0, width, context.getContentHeight());
context.drawIncoming(g, 0, 0, GraphicsContext.OPAQUE);
}
}
Examples
The Navigation Framework example application demonstrates the whole library: a splash screen replaced by the home page, one page per shipped transition, a custom transition, and a form passing its result to the next page as a navigation argument.
