The ViewMaster
class handles the MIDlet UI views.
It extends the GameCanvas
class and performs all
drawing operations in a separate thread to keep the animations smooth.
The class also handles the logic for switching between views.
The class implements the singleton pattern, and the getInstance
method provides access to the singleton object.
public static ViewMaster getInstance() { if (instance == null) { instance = new ViewMaster(); instance.initialize(); } return instance; }
After the singleton instance is created, the MIDlet
initializes all the views using the initialize
method.
private void initialize() { this.setCommandListener(this); try { defaultThumbnailIcon = ImageLoader.getInstance().loadImage( "/thumbnail.png", null); } catch (IOException e) { } try { Image i = ImageLoader.getInstance().loadImage("/loader_content.png", null); defaultLoaderSprite = new Sprite(i, i.getWidth() / 24, i.getHeight()); } catch (IOException e) { } attractionsView = new AttractionsView(); detailsView = new DetailsView(); mapView = new MapView(); guidesView = new GuidesView(); aboutView = new AboutView(); helpView = new HelpView(); try { Class.forName("com.nokia.mid.payment.IAPClientPaymentListener"); Class c = Class.forName("com.nokia.example.attractions.views." + "BuyGuidesView"); buyGuidesView = (BaseView) (c.newInstance()); } catch (Exception e) { buyGuidesView = null; } }
The showNotify
method starts
the drawing thread.
protected final void showNotify() { hidden = false; Main.getInstance().refreshLocationFinder(); // S60 doesn't call sizeChanged on start-up so it has to be called here. sizeChanged(getWidth(), getHeight()); draw(); final Graphics g = getGraphics(); new Thread() { public void run() { while (!hidden) { if (refreshScreen && canPaint) { refreshScreen = false; if (activeView != null) { activeView.draw(g); flushGraphics(); } } drawLock.sleep(50); } } }.start(); new Thread() { public void run() { try { sleep(500); draw(); } catch (InterruptedException e) { } } }.start(); }
The platform calls the paint
method
when the GameCanvas
is shown and ready to be painted
on. When the paint
method is called, the MIDlet sets
the canPaint
flag in the ViewMaster
.
public final void paint(Graphics g) { canPaint = true; forceDraw(); }
The draw
method sets a flag indicating
that the screen must be refreshed.
public final void draw() { refreshScreen = true; }
The forceDraw
method sets a flag
indicating that the screen must be refreshed and wakes up the drawing
thread.
public final void forceDraw() { draw(); drawLock.wakeup(); }
The setView
method adds a view
to the top of the view stack, activates the view, and configures the
softkey buttons for the view.
private void setView(final BaseView view) { Main.getInstance().callSerially(new Runnable() { public void run() { if (activeView != null) { if (activeView == view) { // Trying to activate a view that's already active. return; } else { activeView.deactivate(); } } activeView = view; synchronized (viewStack) { // Remove the view from viewstack. viewStack.removeElement(activeView); // Push the view into viewstack. viewStack.addElement(activeView); } activeView.activate(); forceDraw(); } }); }
The setPreviousView
method activates
the previous view from the view stack.
public final void setPreviousView() { BaseView view = null; synchronized (viewStack) { if (viewStack.size() >= 2) { // First remove and forget the current view. viewStack.removeElement(viewStack.lastElement()); // Get the 2nd to last, which we want to activate. view = (BaseView) viewStack.lastElement(); } } // Activate the view. if (view != null) { setView(view); } }
All UI views extend the BaseView
class.
When a view is activated, the ViewMaster
calls the activate
method. In this method, views refresh the data
that is drawn, activate any touch event listeners, and so on.
public void activate() { active = true; logEvent("activated"); }
When a view is hidden, the ViewMaster
calls the deactivate
method. In this method, views
stop their animations, deactivate touch event listeners, and so on.
public void deactivate() { viewMaster.removeCommand(exitCmd); viewMaster.removeCommand(backCmd); viewMaster.removeCommand(mapCmd); viewMaster.removeCommand(guidesCmd); viewMaster.removeCommand(buyGuidesCmd); viewMaster.removeCommand(aboutCmd); viewMaster.removeCommand(helpCmd); active = false; }
The draw method draws the view to the screen. Every subclass overrides this method with their own dedicated implementation.
public void draw(final Graphics g) { }
The List
class is a custom UI component for the
views. It provides a cursor for focus and a scroll bar for visualizing
the number of items in the list. The MIDlet only draws those items
that intersect with the displayable area. The MIDlet draws the list
items using an implementation of the List.Drawer
interface.
The selection event is returned using the Listener
interface.
The static getList
method creates
a new List
component. If the device supports the
Frame Animator API and Gesture API, the method returns a GestureList
object; if not, the method returns a List
object
instead.
public static List getList(Drawer drawer, Listener listener) { List list = null; try { Class.forName("com.nokia.mid.ui.frameanimator.FrameAnimator"); Class.forName("com.nokia.mid.ui.gestures." + "GestureRegistrationManager"); Class c = Class.forName("com.nokia.example.attractions.views.list." + "GestureList"); list = (List) c.newInstance(); } catch (Exception e) { list = Main.isS60Phone() ? new S60List() : new List(); } list.drawer = drawer; list.listener = listener; return list; }
The GestureList
class extends the custom List
class with support for touch events and kinetic scrolling.
When the list is enabled, the class registers the frame animator and the zone for receiving gestures.
public final void enable() { if (enabled) { return; } enabled = true; super.enable(); // Use default values for maxFps an maxPps // (zero param means that default is used). final short maxFps = 0; final short maxPps = 0; if (animator.register((short) 0, (short) 0, maxFps, maxPps, this) != true) { throw new RuntimeException("FrameAnimator.register() failed!"); } if (GestureRegistrationManager.register(view, zone) != true) { throw new RuntimeException( "GestureRegistrationManager.register() failed!"); } GestureRegistrationManager.setListener(view, this); }
When the list is disabled, the class unregisters the gesture zone and the frame animator.
public final void disable() { if (!enabled) { return; } enabled = false; GestureRegistrationManager.unregister(view, zone); super.disable(); animator.unregister(); }
The DetailsView
shows the details of an attraction.
The view can contain a lot of text, and drawing text can be a slow
operation. To achieve smooth scrolling animations, the MIDlet draws
the text to a buffer. The MIDlet updates the buffer only if the text
changes or the screen dimensions change.
The drawBuffer
method draws the details of an attraction to the buffer.
protected final void drawBuffer(Graphics g) { int w = getContentWidth(); int x0 = 0; int y0 = 0; g.setFont(Visual.SMALL_BOLD_FONT); g.setColor(Visual.LIST_SECONDARY_COLOR); g.drawString(attraction.getStreet(), x0, y0, Graphics.TOP | Graphics.LEFT); if (attraction.getDistance() != null) { g.drawString(attraction.getDistance(), w, y0, Graphics.TOP | Graphics.RIGHT); } y0 += Visual.SMALL_BOLD_FONT.getHeight() + 4; g.setFont(Visual.SMALL_FONT); g.setColor(Visual.LIST_PRIMARY_COLOR); for (int i = 0; i < lines.size(); i++) { g.drawString((String) lines.elementAt(i), x0, y0, Graphics.TOP | Graphics.LEFT); y0 += Visual.SMALL_FONT.getHeight(); } }
The BuyGuidesView
shows a list of purchasable
guides. The MIDlet loads a list of guide IDs from the back-end server,
and Nokia Store provides the titles, descriptions, and prices for
the guides according to the IDs.
The IAPClientPaymentManager
is initialized in the class constructor.
BuyGuidesView() throws IAPClientPaymentException { // ... manager = IAPClientPaymentManager.getIAPClientPaymentManager(); IAPClientPaymentManager.setIAPClientPaymentListener(this); }
The loadAccountAndGuides
method
calls the IAPClientPaymentManager.getUserAndDeviceId
method to retrieve the user's account information.
private void loadAccountAndGuides() { if (loadingAccount) { return; } loadingAccount = true; // For restoring guides user's account is needed int status = manager.getUserAndDeviceId( IAPClientPaymentManager.DEFAULT_AUTHENTICATION); if (status != IAPClientPaymentManager.SUCCESS) { showAlert("Authorization failure " + status, "Authorization failure", "Authorization process failed. " + getPaymentError(status)); viewMaster.setPreviousView(); loadingAccount = false; } }
The userAndDeviceDataReceived
callback method returns the account information.
public final void userAndDeviceDataReceived(int status, IAPClientUserAndDeviceData ud) { if (status == OK) { account = ud.getAccount(); loadGuides(); } else { showAlert("Authorization listener failure " + status, "Authorization failure", "Authorization process failed. " + getPaymentError(status)); viewMaster.setPreviousView(); } loadingAccount = false; }
The loadGuides
method starts
a network operation to load the guide IDs from the back-end server.
The back-end server needs the user's account to mark already purchased
guides restorable. After the IDs are loaded, the method calls the loadProductData
method.
private void loadGuides() { if (loadingGuides) { return; } loadingGuides = true; new NewGuidesOperation(new NewGuidesOperation.Listener() { public void guidesReceived(Vector newGuides) { if (newGuides == null) { showAlert("Network failure", "Network failure", "Connecting network failed."); viewMaster.setPreviousView(); loadingGuides = false; } else { waitingProductData = new Hashtable(); for (int i = 0, length = newGuides.size(); i < length; i++) { Guide guide = (Guide) newGuides.elementAt(i); if (!data.getGuides().contains(guide)) { waitingProductData.put(guide.getId(), guide); guide.setUrl(GUIDE_URL_PREFIX + guide.getId()); guide.setAccount(account); } } loadProductData(); } } }, NEW_GUIDES_URL, account).start(); }
The loadProductData
method loads
the titles, descriptions, and prices for the guides from Nokia Store.
private void loadProductData() { if (waitingProductData.isEmpty()) { waitingProductData = null; if (guides == null) { guides = new Vector(); loadingGuides = false; if (isActive()) { viewMaster.draw(); } } return; } String[] productIds = new String[waitingProductData.size()]; Enumeration e = waitingProductData.keys(); for (int i = 0; e.hasMoreElements(); i++) { productIds[i] = ((String) e.nextElement()); } int status = manager.getProductData(productIds); if (status != IAPClientPaymentManager.SUCCESS) { waitingProductData = null; showAlert("Metadata failure " + status, "Connection failure", "Loading new guides failed. " + getPaymentError(status)); viewMaster.setPreviousView(); loadingGuides = false; } }
The productDataListReceived
method
is called after the product data is loaded.
public final void productDataListReceived(int status, IAPClientProductData[] productDataList) { if (status == OK) { guides = new Vector(); for (int i = 0, size = productDataList.length; i < size; i++) { IAPClientProductData productData = productDataList[i]; if (productData.getProductId() != null) { Guide guide = (Guide) waitingProductData.remove(productData. getProductId()); String title = productData.getTitle(); if (title == null) { title = productData.getShortDescription(); } if (title == null) { title = "unknown"; } String price = productData.getPrice(); if (price == null) { price = "unknown"; } if (guide != null) { guide.setCity(title); guide.setPrice(price); guides.addElement(guide); } } } if (isActive()) { showGuides(); viewMaster.draw(); } } else { showAlert("Metadata listener failure " + status, "Connection failure", "Loading new guides failed. " + getPaymentError(status)); viewMaster.setPreviousView(); } waitingProductData = null; loadingGuides = false; }
The MIDlet calls the selectGuide
method when the user selects a guide from the list. The method starts
the purchase process.
private void selectGuide(int index) { if (purchasing != null) { return; } purchasing = (Guide) guides.elementAt(index); if (purchasing.isRestorable()) { // Restore guide from the backend guidePurchased(""); } else { // Purchase guide using Nokia Store int status = manager.purchaseProduct(purchasing.getId(), IAPClientPaymentManager.FORCED_AUTOMATIC_RESTORATION); if (status != IAPClientPaymentManager.SUCCESS) { purchasing = null; showAlert("Purchase failure " + status, "Purchase failure", "Purchase process failed. " + getPaymentError(status)); } } }
After the purchase is completed, the purchaseCompleted
method is called.
public final void purchaseCompleted(int status, String purchaseTicket) { switch (status) { case OK: guidePurchased(purchaseTicket); break; case RESTORABLE: guidePurchased(purchaseTicket == null ? "" : purchaseTicket); break; default: purchasing = null; showAlert("Purchase listener failure " + status, "Purchase failure", "Purchase process failed. " + getPaymentError(status)); break; } }
The guidePurchased
method adds
a purchase ticket to the guide, moves the guide from the new guides
list to the list of guides in the DataModel
, and
opens a view that shows the attractions in the guide.
private void guidePurchased(String purchaseTicket) { if (purchasing == null) { return; } purchasing.setPurchaseTicket(purchaseTicket); guides.removeElement(purchasing); data.getGuides().addElement(purchasing); data.setCurrentGuideIndex(data.getGuides().indexOf(purchasing)); data.saveGuides(); purchasing = null; viewMaster.showAttractionsView(); }