ViewMaster.java

/*
 * Copyright © 2011 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners. 
 * See LICENSE.TXT for license information.
 */ 

package com.nokia.example.wordpress.views;

import com.nokia.example.wordpress.helpers.ImageLoader;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;

/**
 * Class for handling application views. Handles view switching, drawing
 * and animations. Displaying is done using GameCanvas.
 */
public class ViewMaster extends GameCanvas {

    public static final int VIEW_NOVIEW = -1;
    public static final int VIEW_SPLASH = 0;
    public static final int VIEW_LOADER = 1;
    public static final int VIEW_POSTS = 2;
    public static final int VIEW_SINGLEPOST = 3;
    public static final int VIEW_COMMENTSFORPOST = 4;
    public static final int VIEW_SINGLECOMMENT = 5;
    public static final int VIEW_COMMENTS = 6;
    public static final int VIEW_NEWPOST = 7;
    public static final int VIEW_NEWCOMMENT = 8;
    public static final int VIEW_LOGIN = 9;
    private static ViewMaster self = null;
    private BaseView splashView;
    private BaseView loaderView;
    private BaseView postsView;
    private BaseView singlePostView;
    private BaseView commentsForPostView;
    private BaseView singleCommentView;
    private BaseView commentsView;
    private BaseView newPostView;
    private BaseView newCommentView;
    private BaseView loginView;
    public BaseView activeView = null;
    private Graphics g;
    private boolean initialized = false;
    private int width;
    private int height;
    private Image backgroundImage;
    public Hashtable gravatarCache = new Hashtable();
    public Image defaultGravatarIcon;
    private Vector viewStack = new Vector();
    private Timer timer;
    private boolean animationInProgress = false;
    private float animationProgress = 0f;
    private float animationSpeed = 0.1f;
    private int animationDirection = 1;
    /**
     * The front buffer, the active view is drawn here and then flushed
     * to the main display.
     */
    private Image frontImage;
    private Graphics frontGraphics;
    /**
     * The back buffer, the view that is going to be deactivated is drawn here.
     * This and the frontImage are used for view transition animation.
     */
    private Image backImage;
    private Graphics backGraphics;

    public ViewMaster() {
        super(false);
        this.setFullScreenMode(true);
        timer = new Timer();
    }

    public static ViewMaster getInstance() {
        if (self == null) {
            self = new ViewMaster();
        }
        return self;
    }

    protected void showNotify() {
        if (initialized) {
            return;
        }

        // At this point the final dimensions are available.
        width = getWidth();
        height = getHeight();
        g = getGraphics();

        try {
            backgroundImage = ImageLoader.getInstance().loadImage("/background.png");
            defaultGravatarIcon = ImageLoader.getInstance().loadImage("/avatar.png");
        } catch (IOException e) {
        }

        // Create front and backbuffers. 
        frontImage = Image.createImage(width, height);
        frontGraphics = frontImage.getGraphics();
        backImage = Image.createImage(width, height);
        backGraphics = backImage.getGraphics();

        // Create all views.
        splashView = new SplashView(g, 0, 0, width, height);
        loaderView = new LoaderView(g, 0, 0, width, height);
        postsView = new PostsView(g, 0, 0, width, height);
        singlePostView = new SinglePostView(g, 0, 0, width, height);
        commentsForPostView = new CommentsForPostView(g, 0, 0, width, height);
        singleCommentView = new SingleCommentView(g, 0, 0, width, height);
        commentsView = new CommentsView(g, 0, 0, width, height);
        newPostView = new NewPostView(g, 0, 0, width, height);
        newCommentView = new NewCommentView(g, 0, 0, width, height);
        loginView = new LoginView(g, 0, 0, width, height);
        initialized = true;
        // The initial view is the splash view.
        setView(VIEW_SPLASH);
        
        // Set up a timer for doing animations.
        TimerTask timerTask = new TimerTask() {

            public void run() {
                if (animationInProgress) {
                    // View transition animation.
                    animate();
                } else {
                    if (activeView != null) {
                        // Active view animation.
                        activeView.animate();
                    }
                }
            }
        };
        timer.schedule(timerTask, 0, 50);
    }

    /**
     * Sets a view to be displayed. Transitions the previous view away.
     * @param viewId
     */
    public void setView(int viewId) {
        System.out.println("setView " + viewId);
        if (animationInProgress) {
            // Do nothing when already transitioning.
            return;
        }
        if (activeView != null) {
            if (activeView.id() == viewId) {
                // Trying to activate a view that's already active.
                return;
            } else {
                // Draw the previous view into the back buffer.
                activeView.setGraphics(backGraphics);
                synchronized (backGraphics) {
                    // Synchronizeddraw due to the animation timer thread.
                    activeView.draw(0, 0, width, height);
                }
                activeView.deactivate();
            }
        }

        switch (viewId) {
            case VIEW_SPLASH:
                activeView = splashView;
                break;
            case VIEW_LOADER:
                activeView = loaderView;
                break;
            case VIEW_POSTS:
                activeView = postsView;
                break;
            case VIEW_SINGLEPOST:
                activeView = singlePostView;
                break;
            case VIEW_COMMENTSFORPOST:
                activeView = commentsForPostView;
                break;
            case VIEW_SINGLECOMMENT:
                activeView = singleCommentView;
                break;
            case VIEW_COMMENTS:
                activeView = commentsView;
                break;
            case VIEW_NEWPOST:
                activeView = newPostView;
                break;
            case VIEW_NEWCOMMENT:
                activeView = newCommentView;
                break;
            case VIEW_LOGIN:
                activeView = loginView;
                break;
        }
        // Push the view id into viewstack.
        viewStack.addElement(new Integer(activeView.id()));
        activeView.setGraphics(frontGraphics);
        activeView.activate();
        // Skip transition animation for some views for better user experience.
        if (activeView.id() == VIEW_SPLASH || activeView.id() == VIEW_LOGIN || activeView.id() == VIEW_LOADER) {
            draw();
        } else {
            startAnimation();
        }
    }

    /**
     * Activate the previous view from the stack.
     * At least two views need to be there.
     */
    public void setPreviousView() {
        if (viewStack.size() >= 2) {
            // First remove and forget the current view id.
            viewStack.removeElement(viewStack.lastElement());
            // Remove the 2nd to last, which we want to activate.
            int viewId = ((Integer) viewStack.lastElement()).intValue();
            viewStack.removeElement(viewStack.lastElement());

            if (viewId == VIEW_NEWPOST || viewId == VIEW_NEWCOMMENT) {
                // Backing into one of these is not a good idea, so skip
                // to an even older view.
                viewId = ((Integer) viewStack.lastElement()).intValue();
                viewStack.removeElement(viewStack.lastElement());
            }

            // Activate and push the view id back.
            setView(viewId);
        }
    }

    /**
     * Retrieves the id of the previously active view.
     * @return
     */
    public int getPreviousViewId() {
        if (viewStack.size() < 2) {
            return VIEW_NOVIEW;
        } else {
            // Access the second to last element.
            Integer viewId = (Integer) viewStack.elementAt(viewStack.size()-2);
            return viewId.intValue();
        }
    }

    /**
     * Draws the active view.
     */
    public void draw() {
        if (!initialized || activeView == null || animationInProgress) {
            return;
        }
        synchronized (frontGraphics) {
            // Synchronized draw due to the animation timer thread.
            activeView.draw(0, 0, width, height);
        }
        g.drawImage(frontImage, 0, 0, g.TOP | g.LEFT);
        flushGraphics();
    }

    /**
     * Updates a part of screen from the front buffer.
     * @param x
     * @param y
     * @param width
     * @param height
     */
    public void updateScreen(int x, int y, int width, int height) {
        g.drawImage(frontImage, 0, 0, g.TOP | g.LEFT);
        flushGraphics(x, y, width, height);
    }

    /**
     * Starts the view transition animation.
     */
    private void startAnimation() {

        // Change the direction every time.
        animationDirection = -animationDirection;

        // Prepare front buffer, protect from the animation timer.
        synchronized (frontGraphics) {
            activeView.draw(0, 0, width, height);
        }
        // Update the tab area first, as animating it looks disorienting.
        g.drawImage(frontImage, 0, 0, g.TOP | g.LEFT);
        flushGraphics(0, 0, width, activeView.tabsHeight());

        animationInProgress = true;
        animationProgress = 0f;
    }

    /**
     * Run the view transition animation step by step.
     */
    private void animate() {

        animationProgress += animationSpeed;

        if (animationProgress >= 1f) {
            // Animation finished. Draw the finished state on screen.
            animationProgress = 1f;
            animationInProgress = false;
            g.drawImage(frontImage, 0, 0, g.TOP | g.LEFT);
            flushGraphics();
            // Text editor based views would like to know when
            // they can display the text editors.
            activeView.transitionFinished();
            return;
        }

        // Calculate the position on a quadratic curve and determine
        // the placements for the front and back buffers.
        float progress = -animationProgress * (animationProgress - 2);
        int xPos = (int) (((float) width) * progress);

        int frontX = -width + xPos;
        int backX = xPos;

        if (animationDirection > 0) {
            frontX = width - xPos;
            backX = -xPos;
        }

        // Draw the buffers and display.
        g.drawImage(frontImage, frontX, 0, g.TOP | g.LEFT);
        g.drawImage(backImage, backX, 0, g.TOP | g.LEFT);
        flushGraphics(0, activeView.tabsHeight(), width, height - activeView.tabsHeight());
    }

    /**
     * Key pressed event handler.
     * @param keyCode
     */
    protected void keyPressed(int keyCode) {
        if (activeView == null) {
            return;
        }
        activeView.keyPressed(keyCode);
    }

    /**
     * Key released event handler.
     * @param keyCode
     */
    protected void keyReleased(int keyCode) {
        if (activeView == null) {
            return;
        }
        activeView.keyReleased(keyCode);
    }

    /**
     * Utility to draw the background image. This is used to draw the background
     * and list highlights.
     * @param g
     * @param x
     * @param y
     * @param width
     * @param height
     * @param highlight Set to true to have a highlighted effect.
     */
    public void drawBackground(Graphics g, int x, int y, int width, int height, boolean highlight) {
        // Check whether the image is big enough.
        if (x + width >= backgroundImage.getWidth() ||
                y + height >= backgroundImage.getHeight()) {
            g.setColor(Visual.BACKGROUND_COLOR);
            g.fillRect(x, y, width, height);
        } else {
            g.drawRegion(backgroundImage,
                    x, y,
                    width, height,
                    Sprite.TRANS_NONE,
                    x, y,
                    g.TOP | g.LEFT);
        }
        if (highlight) {
            drawDimmedBackground(g, x, y, width, height);
        }

    }

    /**
     * Dim the background when using the progress indicator on top.
     */
    public void drawDimmedBackground() {
        drawDimmedBackground(g, 0, 0, width, height);
        flushGraphics();
    }

    /**
     * Render a part of the background dimmed. Used for drawing list focus cursor.
     * @param g
     * @param x
     * @param y
     * @param width
     * @param height
     */
    private void drawDimmedBackground(Graphics g, int x, int y, int width, int height) {
        // This would be more optimized if the alpha mask were pregenerated.
        int[] pixels = new int[width * height];
        for (int i = 0; i < width * height; i++) {
            //pixels[i] = 0xaa000000;
            pixels[i] = 0x55000000;
        }

        g.drawRGB(pixels, 0, width, x, y, width, height, true);
        pixels = null;
    }
}