Unlike desktop applications, mobile applications only display one screen, or view, at a time. Each screen should be designed for a single task with clear and focused content. Intuitive single-click navigation to move to previous and next steps is key.
We will explore two approaches in this chapter. The first is a navigation system I wrote for my mobile applications using pure ActionScript. The second approach takes advantage of the ViewNavigator in the Flex Hero framework for mobile applications. It includes a few additional features such as transitions and a global ActionBar.
Navigation
You want your audience to be able to move forward between screens without re-creating the same steps over and over. You may need to provide the logic to navigate back to previous screens.
Google discourages the use of the physical back button for anything but going back to the previous application. However, the functionality exists, and some applications, as well as Flash Builder Mobile, use it to go through the stack of views. In my example, I create a back button within the application instead.
ViewManager
I originally developed this code for a conference scheduler application.
Attendees can carry the scheduler in their pocket and organize and save their own schedules. The application’s business logic is fairly simple, but all the screens are interconnected and can be accessed from several different points. For instance, from the session view, the user can access the session’s speaker(s), and from the speaker view, the user can access one of the sessions he is speaking at.
Creating views
The ViewManager creates the different views and manages them during the life of the application.
The document class creates an instance of the ViewManager. It calls its init function and passes a reference of the timeline:
[code]
import view.ViewManager;
// member variable
private var viewManager:ViewManager;
viewManager = new ViewManager();
viewManager.init(this);
[/code]
The ViewManager class stores a reference to the timeline to add and remove views from the display list:
[code]
private var timeline:MovieClip;
public function init(timeline:MovieClip):void {
this.timeline = timeline;
}
[/code]
The ViewManager creates an instance of each view and stores them in a viewList object. The following code assumes the MenuView, SessionsView, and SessionView classes exist.
The initialization process for each view, creation of the view’s member variables and references to other objects, only needs to happen once.
Note that the views are of data type BaseView.
[code]
private var currentView:BaseView;
private viewList:Object = {};
public function init(timeline:MovieClip):void {
this.timeline = timeline;
createView(“menu”, new MenuView());
createView(“sessions”, new SessionsView());
createView(“session”, new SessionView());
}
private function createView(name:String, instance:BaseView):void {
viewList[name] = instance;
}
[/code]
The initial view display
When the application first starts, the document class loads an XML document that contains all the data regarding the conference, such as the list of sessions and speakers. While this is taking place, the ViewManager displays an introductory view without any interactivity. Let’s modify the init method to add this functionality. The setCurrent View method will be discussed in the next paragraph:
[code]
public function init(timeline:MovieClip):void {
this.timeline = timeline;
createView(“intro”, new IntroView());
createView(“menu”, new MenuView());
createView(“sessions”, new SessionsView());
createView(“session”, new SessionView());
setCurrentView({view:”intro”});
}
[/code]
The current view display
Once the data is loaded, parsed, and stored in the model part of the application, the document class calls the onDataReady method on the ViewManager:
[code]
// set up application, model and get data from external xml
viewManager.onDataReady();
[/code]
In turn, the ViewManager defines the new view by calling the setCurrentView method and passes an object with the property view to define the view to display:
[code]
public function onDataReady():void {
setCurrentView({view:”menu”});
}
[/code]
The setCurrentView method removes the previous view if there is one. It then stores the view in the currentView variable and adds it to the display list. Two methods, onHide and onShow, are called via the IView interface, discussed next. Each view uses the methods to clear or add from the display list and destroy objects.
The method also registers the view for a custom ClickEvent.NAV_EVENT with the setCur rentView method of the ViewManager as the callback function.
[code]
import view.ClickEvent;
private var currentView:BaseView;
private function setCurrentView(object:Object):void {
// remove current view
if (currentView) {
currentView.removeEventListener(ClickEvent.NAV_EVENT, goTo);
IView(currentView).onHide();
timeline.removeChild(currentView);
currentView = null;
}
// add new view
currentView = viewList[object.view];
if (object.id != undefined) {
currentView.setID(object.id);
}
currentView.addEventListener(ClickEvent.NAV_EVENT, goTo, false, 0, true);
IView(currentView).onShow();
timeline.addChild(currentView);
}
// pass event data object
private function goTo(event:ClickEvent):void {
setCurrentView(event.data);
}
[/code]
The IView interface
It is imperative that all views have the two methods, onHide and onShow, so we use an IView interface. Each view also needs a method—here it is clickAway—to navigate to the next view. In our application, this always occurs upon user interaction. We will therefore use a MouseEvent:
[code]
package view {
import flash.events.MouseEvent;
public interface IView
{
function onShow():void
function onHide():void
function clickAway(event:MouseEvent):void
}
}
[/code]
Creating a custom event
A custom event is used to pass the destination view and additional data, if needed, from the current view. For example, if the user is looking at the screen displaying all the conference sessions and clicks on a specific session, we use the event object to pass the session ID to the Session view, as illustrated in Figure 16-1:
[code]
{view:”session”, id:5}
[/code]
The custom class is as follows. Its data property is an object, so we can add additional parameters as needed:
[code]
import flash.events.Event;
import events.ClickEvent;
final public calls ClickEvent extends Event {
public static const NAV_EVENT:String = “NavEvent”;
public var data:Object;
public function ClickEvent(type:String, data:Object = null) {
super(type, true, true);
this.data = data;
}
public override function clone():Event {
return new ClickEvent(this.type, this.data);
}
}
[/code]
Individual Views
Let’s examine some of the views.
Inheritance
Some functionality is identical for all views. For this reason, they all inherit from the same super class, called BaseView. Here we declare two member variables, id and con tainer, and a function, setID. It is a simple class initially. We will develop it further in this chapter to add a button and interactivity to navigate in reverse:
[code]
package view {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
public class BaseView extends Sprite
{
protected var id:int;
protected var container:Sprite;
protected function SeID(id:int):void {
this.id = id;
}
}
}
[/code]
The following code is for the MenuView class. It adds three buttons—for sessions, speakers, and a custom schedule—and their listeners on onShow. It clears listeners and empties the display list on onHide. Figure 16-2 shows the Menu view of the AIR scheduler application for the 2010 GoogleIO conference:
[code]
package view {
import flash.events.MouseEvent;
import view.ClickEvent;
final public class MenuView extends BaseView implements IView() {
public function MenuView(){}
public function onShow():void {
var sessions:sessionsBut = new sessionsBut();
sessions.view = “sessions”;
sessions.addEventListener(MouseEvent.CLICK, onClickAway);
var speakers:speakersBut = new speakersBut();
speakers.view = “speakers”;
speakers.addEventListener(MouseEvent.CLICK, onClickAway);
var schedule:scheduleBut = new scheduleBut();
schedule.view = “schedule”;
schedule.addEventListener(MouseEvent.CLICK, onClickAway);
addChild(sessions);
addChild(speakers);
addChild(schedule);
}
public function onHide():void {
while(numChildren > 0) {
getChildAt(0).
removeEventListener(MouseEvent.CLICK, onClickAway);
removeChildAt(0);
}
}
public function onClickAway(event:MouseEvent):void {
dispatchEvent(new ClickEvent(ClickEvent.NAV_EVENT,
{view:event.currentTarget.view});
}
}
}
[/code]
The next example shows the SessionsView code. Note that the code is minimal to keep the focus on navigation. For instance, the scrolling mechanism was left out.
As mentioned before, the initialization process for each view only needs to happen once for the life of the application. Here the SessionsView class acquires the data for the sessions just once if its list variable is null.
The sessions data is stored in a Vector object called sessionList of the static class Sessions (not covered here) and is populated with objects of data type Session. It is already sorted and organized by day and time:
[code]
package view {
import events.ClickEvent;
import flash.events.MouseEvent;
import model.Sessions; // static class holds list of all Sessions
import model.Session; // class holds individual session data
final public class SessionsView extends BaseView implements IView() {
private var list:Vector.<Session>;
public function SessionsView (){}
]
public function onShow():void {
container = new Sprite();
// request list of sessions if not acquired yet
if (list == null) {
list = Sessions.sessionList;
}
// display sessions
showSessions();
}
}
[/code]
We traverse the list of sessions and display all the sessions with the hardcoded date of 19. Again, this is to keep this example simple. The conference took place over two days and would, in a full example, require a UI to choose between one of the two dates:
[code]
private showSessions():void {
var timeKeeper:String = “0”;
var bounds:int = list.length;
var ypos:int = 50;
for (var i:int = 0; i < bounds; i++) {
var session:Session = list[i];
// display a blue time indicator if it is a new time
if (session.time > timeKeeper) {
timeKeeper = session.time;
ypos += 15;
// TimeBar is a movieclip
var bar:TimeBar = new TimeBar();
bar.y = ypos;
bar.timeInfo.text = timeKeeper;
container.addChild(bar);
ypos += 60;
}
// load the individual session
// it returns its height to position the next element below it
var newPos = loadSession(session, ypos);
ypos =+ (newPos + 10);
}
addChild(container);
}
private loadSession(session:Session, ypos:int):int {
// SessionSmall is a movieclip
var mc:SessionSmall = new SessionSmall();
mc.y = ypos;
mc.id = session.id;
mc.what.autoSize = TextFieldAutoSize.LEFT;
mc.what.text = “+ ” + session.title;
mc.back.height = mc.what.height;
mc.addEventListener(MouseEvent.CLICK, clickAway, false, 0, true);
container.addChild(mc);
// return the session movie clip height
return mc.what.height;
}
[/code]
When the user chooses a session, its ID is passed along with the destination view:
[code]
public function clickAway(event:MouseEvent):void {
dispatchEvent(new ClickEvent(ClickEvent.NAV_EVENT,
{view:”session”, id:event.currentTarget.id}));
}
[/code]
In the onHide method, all the children of the Sprite container are removed as well as their listeners if they have one. Then the container itself is removed. Figure 16-3 shows the sessions broken down by day and time:
[code]
public function onHide():void {
while (container.numChildren > 0) {
var child:MovieClip = container.getChildAt(0) as MovieClip;
if (child.id != null) {
child.removeEventListener(MouseEvent.CLICK, clickAway);
}
container.removeChild(child);
}
removeChild(container);
container = null;
}
[/code]
Here is the SessionView code. The method displays all the data related to a session. This includes the session title, a description, the speakers involved, and the room, category, and rank:
[code]
package view {
import events.ClickEvent;
import flash.events.MouseEvent;
import model.Sessions;
import model.Speakers; // static class that holds Speakers data
final public class SessionView extends BaseView implements IView() {
public function SessionView(){}
public function onShow():void {
// search Sessions by id
var data:Object = Sessions.getItemByID(id);
container = new Sprite();
addChild(container);
// display title and description
// SessionMovie is a movieclip
var session:SessionMovie = new SessionMovie();
session.title.autoSize = TextFieldAutoSize.LEFT;
session.title.text = data.title;
session.body.text = data.description;
container.addChild(session);
// display list of speakers
for (var i:int; i < data.speakers.length; i++) {
var bio:Bio = new Bio();
bio.id = data.speakers[i];
// search list of speakers by id
var bioData:Object = Speakers.getItemByID(bio.id);
bio.speaker.text = bioData.first + ” ” + bioData.last;
bio.addEventListener(MouseEvent.CLICK,
clickAway, false, 0, true);
}
// display category, level, rank and room number
// Border is a movieClip
var border:Border = new Border();
// categories is a movieclip with frame labels matching category
border.categories.gotoAndStop(data.tag);
// rank is a movieclip with a text field
border.rank.text = String(data.type);
// room is a movieclip with a text field
border.room.text = String(data.room);
container.addChild(border);
}
[/code]
Clicking on one of the speakers takes the user to a new speaker destination view defined by an ID:
[code]
public function clickAway(event:MouseEvent):void {
dispatchEvent(new ClickEvent(ClickEvent.NAV_EVENT,
{view:”speaker”, id:event.currentTarget.id}));
}
}
[/code]
In the onHide method, all the children of the Sprite container are removed as well as their listeners if they have one. Then the container itself is removed:
[code]
public function onHide():void {
while(container.numChildren > 0) {
var child:container.getChildAt(0);
if (child.id != null) {
child removeEventListener(MouseEvent.CLICK, clickAway);
}
container.removeChild(child);
}
removeChild(container);
container = null;
}
[/code]
Figure 16-4 shows a subset of information for a session.