/* * Copyright © 2013 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.favouriteartists; import java.util.Vector; import javax.microedition.lcdui.*; import com.nokia.example.favouriteartists.tool.Log; import com.nokia.mid.ui.frameanimator.*; import com.nokia.mid.ui.gestures.*; /** * Favourite artists view. Shows current favourite artists in a grid. * <p> * This class illustrates the use of Gestures API and Frame Animator API. * Points of interests are: the constructor {@link #FavouriteArtistsView(CommandHandler, Display, ImageProvider)}, * Gestures API callback {@link #gestureAction(Object, GestureInteractiveZone, GestureEvent)} and * Frame Animator API callback {@link #animate(FrameAnimator, int, int, short, short, short, boolean)} * <p> * Gestures used in this example: * {@link GestureInteractiveZone#GESTURE_TAP} * {@link GestureInteractiveZone#GESTURE_LONG_PRESS} * {@link GestureInteractiveZone#GESTURE_DRAG} * {@link GestureInteractiveZone#GESTURE_DROP} * {@link GestureInteractiveZone#GESTURE_FLICK} * <p> * From Frame Animator API, {@link FrameAnimator#kineticScroll(int, int, int, float)} is used * with {@link FrameAnimator#FRAME_ANIMATOR_FREE_ANGLE} to achieve two-dimensional kinetic scrolling * of the grid (used for flick gesture). * <p> * The grid can be scrolled with drag and flick gestures. A single item occupies * most of the screen area. After a scroll movement the view focuses on the nearest * item, see {@link FavouriteArtistsView.FocusMoveThread}. Also, a single tap while scrolling will focus the item. * When item is focused, it can be selected with either single tap or long press, see * {link {@link #handleSelectEvent(GestureEvent)}. * Item selection will open up rating view {@link RatingView}. */ public class FavouriteArtistsView extends Canvas implements GestureListener, FrameAnimatorListener{ // Constants /** Horizontal margin between item top/bottom and screen top/bottom */ private static final int ITEM_H_MARGIN = 20; /** Vertical margin between item top/bottom and screen top/bottom */ private static final int ITEM_V_MARGIN = 20; // Member data /** Display. */ private Display display; /** Command handler. */ private CommandHandler commandHandler; /** For retrieving images. */ ImageProvider imageProvider; /** Short tap action. */ private short tapActionId; /** Long press action */ private short longPressActionId; /** Visible area's top left x-coordinate (i.e. where in the grid are we) */ private int xOffset; /** Visible area's top left y-coordinate (i.e. where in the grid are we) */ private int yOffset; /** Width of the grid, i.e. the scrollable area. */ private int gridWidth; /** Height of the grid, i.e. the scrollable area. */ private int gridHeight; /** Number of columns in the grid, calculated from item count. */ private int columns; /** Number of rows in the grid, calculated from item count. */ private int rows; /** Width of a single item, calculated from Canvas width. */ private int itemWidth; /** Height of a single item, calculated from Canvas width. */ private int itemHeight; /** The selected item, only valid during select action handling. */ private GridItem selectedItem; /** Grid items. */ private Vector items; /** The gesture interactive zone registered to this Canvas. */ private GestureInteractiveZone zone; /** FrameAnimator instance for animating list scrolling. */ private FrameAnimator animator; /** Flag for defining whether scrolling is active (gestures are handled differently while scrolling). */ private boolean scrollingActive; /** Pending select gesture event that is stored for the duration of the select delay. */ private int pendingGestureEvent; /** Select delay. */ private SelectDelay selectDelay; /** Thread used for the select delay. Reference kept to be able to cancel. */ private Thread delayThread; /** Thread used for focus move animation. Reference kept to be able to cancel.*/ Thread focusMoveThread; /** Runnable used for focus move. */ FocusMoveThread focusMoveRunnable; /** This is used to delay starting of focus move while dragging. */ Thread dragDelayThread; // Inner classes /** * Delay for showing selection focus to user before initiating action. */ private class SelectDelay implements Runnable { /** * @see java.lang.Runnable#run() */ public void run() { if (Log.TEST) Log.note("[SelectDelay#run]-->"); try { Thread.sleep(100); } catch (InterruptedException e) { if (Log.TEST) Log.note("[SelectDelay#run] interrupted"); return; } if(pendingGestureEvent > 0){ if (Log.TEST) Log.note("[SelectDelay#run] handling pending gesture"); switch (pendingGestureEvent) { case GestureInteractiveZone.GESTURE_TAP:{ handleTap(); break; } case GestureInteractiveZone.GESTURE_LONG_PRESS:{ handleLongPress(); break; } default: if (Log.TEST) Log.note("[SelectDelay#run] wrong type!"); break; } selectedItem.setSelected(false); selectedItem = null; pendingGestureEvent = 0; delayThread = null; } } } /** * Drag delay thread. This is used to give the user a moment to drag some more before * starting the automatic focus move. */ private class DragDelayThread implements Runnable{ public void run() { if (Log.TEST) Log.note("[DragDelayThread#run]-->"); try { Thread.sleep(500); centerOnClosestItem(true); } catch (InterruptedException e) { if (Log.TEST) Log.note("[DragDelayThread#run] interrupted"); return; } } } /** * Used to animate focus centering to an item after flick/drag. */ private class FocusMoveThread implements Runnable{ // Constants /** Amount of pixels to move per one animation step */ private static final int PIXELS_PER_STEP = 4; // Member data /** X-coordinate target when focusing to item */ private int xTarget; /** Y-coordinate target when focusing to item */ private int yTarget; /** How many pixels to move the x-coordinate per one animation frame */ private int xStep; /** How many pixels to move the y-coordinate per one animation frame */ private int yStep; /** Number of steps to animate in x-direction */ private int stepCountX; /** Number of steps to animate in y-direction */ private int stepCountY; /** X-coordinate distance to target. */ private int xDistance; /** Y-coordinate distance to target. */ private int yDistance; /** Step counter. */ private int step = 1; /** Defines whether to continue the animation loop. */ private boolean keepRunning; // Methods /** * Constructor. * * @param xTarget The target x-coordinate. * @param yTarget The target y-coordinate. * @param xStep The x-coordinate step amount in pixels. * @param yStep The y-coordinate step amount in pixels. */ public FocusMoveThread(int xTarget, int yTarget/*, int xStep, int yStep*/){ if (Log.TEST) Log.note("[FocusMoveThread#run]-->"); this.xTarget = xTarget; this.yTarget = yTarget; xDistance = Math.abs(xTarget - xOffset); yDistance = Math.abs(yTarget - yOffset); // Calculate step counts stepCountX = xDistance / PIXELS_PER_STEP; stepCountY = yDistance / PIXELS_PER_STEP; if(stepCountX < 1){ stepCountX = 1; } if(stepCountY < 1){ stepCountY = 1; } // Calculate the size of x/y steps xStep = xTarget < xOffset ? -PIXELS_PER_STEP : PIXELS_PER_STEP; yStep = yTarget < yOffset ? -PIXELS_PER_STEP : PIXELS_PER_STEP; if (Log.TEST) Log.note("[FocusMoveThread#run]" + " xTarget: " + xTarget + " yTarget: " + yTarget + " xDistance: " + xDistance + " yDistance: " + yDistance + " stepCountX: " + stepCountX + " stepCountY: " + stepCountY + " xStep: " + xStep + " yStep: " + yStep); } /** * Stops the animation. */ public void quit(){ keepRunning = false; } /** * @see java.lang.Runnable#run() */ public void run() { keepRunning = true; while(keepRunning){ if (Log.TEST) Log.note("[FocusMoveThread#run]" + " step: " + step); scrollingActive = true; try { Thread.sleep(10); } catch (InterruptedException e) { if (Log.TEST) Log.note("[FocusMoveThread#run] interrupted"); scrollingActive = false; return; } if( stepCountX > stepCountY ){ // update x coord xOffset += xStep; if(step % (stepCountX / stepCountY) == 0 && yOffset != yTarget){ // update y coord as well yOffset += yStep; } } else if (stepCountY > stepCountX){ // update y coord yOffset += yStep; if(step % (stepCountY / stepCountX) == 0 && xOffset != xTarget){ // update x coord as well xOffset += xStep; } } else{ xOffset += xStep; yOffset += yStep; } // Check that x-coordinate does not go past target if(xStep > 0){ if(xOffset > xTarget){ xOffset = xTarget; } } else { if(xOffset < xTarget){ xOffset = xTarget; } } // Check that y-coordinate does not go past target if(yStep > 0){ if(yOffset > yTarget){ yOffset = yTarget; } } else { if(yOffset < yTarget){ yOffset = yTarget; } } // Make sure that we're on target if this is the last step if(step >= (stepCountX > stepCountY ? stepCountX : stepCountY)){ xOffset = xTarget; yOffset = yTarget; } // Stop the loop when were on target if( xOffset == xTarget && yOffset == yTarget){ keepRunning = false; } repaint(); step++; } scrollingActive = false; } } // Methods /** * Constructor. * * @param commandHandler For command handling. * @param display For color retrieval. * @param imageProvider For image retrieval. * @throws FavouriteArtistsException */ public FavouriteArtistsView(CommandHandler commandHandler, Display display, ImageProvider imageProvider) throws FavouriteArtistsException { if (Log.TEST) Log.note("[FavouriteArtistsView#FavouriteArtistsView]-->"); this.commandHandler = commandHandler; this.display = display; this.imageProvider = imageProvider; selectDelay = new SelectDelay(); // First create a GestureInteractiveZone. The GestureInteractiveZone class is used to define an // area of the screen that reacts to a set of specified gestures. // The parameter GESTURE_ALL means that we want events for all gestures. zone = new GestureInteractiveZone(GestureInteractiveZone.GESTURE_DRAG | GestureInteractiveZone.GESTURE_DROP | GestureInteractiveZone.GESTURE_FLICK | GestureInteractiveZone.GESTURE_LONG_PRESS | GestureInteractiveZone.GESTURE_LONG_PRESS_REPEATED | GestureInteractiveZone.GESTURE_TAP ); // Next the GestureInteractiveZone is registered to registration manager. Note that multiple zones // can be registered for a single container, reference to the affected zone is passed in the event callback. if(GestureRegistrationManager.register(this, zone) != true){ // Throw an exception if register fails throw new FavouriteArtistsException("GestureRegistrationManager.register() failed!"); } // Add a listener for gesture events for this container. A container can be either a Canvas or a CustomItem. // In this example, only one gesture zone that covers the whole canvas is used. GestureRegistrationManager.setListener(this, this); // Create a FrameAnimator instance. In this class, the frame animator is used to animate grid scrolling // for flick gestures. animator = new FrameAnimator(); // Use default values for maxFps an maxPps (zero parameter means that default is used). final short maxFps = 0; final short maxPps = 0; // Register the FrameAnimator. Animation uses the initial x & y coordinates as a starting point // i.e. the animate() callback will give coordinates in relation to this point. // In this example only the delta values are used from the callback so the initial values don't make much difference. if(animator.register(0, 0, maxFps, maxPps, this) != true){ // Throw an exception if register fails throw new FavouriteArtistsException("FrameAnimator.register() failed!"); } // Define actions that are initiated for tap and long press gestures tapActionId = Actions.SHOW_RATING; longPressActionId = Actions.SHOW_RATING; // Add commands addCommand(new ActionCommand(Actions.EXIT_MIDLET, "Exit", Command.EXIT, 0)); addCommand(new ActionCommand(Actions.SHOW_ADD_FAVOURITE, "Add", Command.SCREEN, 1)); addCommand(new ActionCommand(Actions.ARRANGE_FAVOURITES, "Arrange", Command.SCREEN, 1)); // Delegate command handling to separate class. setCommandListener(commandHandler); items = new Vector(); updateItems(); } /** * Calculates grid size and updates grid item coordinates. */ private void updateItems(){ if (Log.TEST) Log.note("[FavouriteArtistsView#updateItems]-->"); // Reset offsets because previous ones might not be valid anymore (e.g. out of grid area) xOffset = 0; yOffset = 0; // First determine a suitable number of rows and columns based on item count columns = 1; while(rows * columns < items.size()){ if(rows < columns){ rows++; } else { columns++; } } // Calculate item size based on Canvas size, one item is intended to be shown on screen at a time itemWidth = getWidth() - 2 * ITEM_H_MARGIN; itemHeight = getHeight() - 2 * ITEM_V_MARGIN; // Calculate grid size, this sets the limits to the area that can be scrolled gridWidth = columns * (itemWidth + ITEM_H_MARGIN * 2); gridHeight = rows * (itemHeight + ITEM_V_MARGIN * 2); // Set coordinates for each item. // These coordinates are in relation to the grid, actual drawing coordinates are // derived from these and current x/y offset of visible area int itemX = ITEM_H_MARGIN; int itemY = ITEM_V_MARGIN; for (int i = 0; i < items.size(); i++) { GridItem item = (GridItem) items.elementAt(i); item.setRect(itemX, itemY, itemWidth, itemHeight); if(i > 0 && (i + 1) % columns == 0){ itemY += itemHeight + ITEM_V_MARGIN * 2; itemX = ITEM_H_MARGIN; } else { itemX += itemWidth + ITEM_H_MARGIN * 2; } } } /** * Updates the view with new favourite data. * @param favouriteDatas New data array. * @param repaint If true, then a repaint will be requested. */ public void updateView(FavouriteData[] favouriteDatas, boolean repaint){ if (Log.TEST) Log.note("[FavouriteArtistsView#updateView]-->"); if(favouriteDatas == null){ return; } // Clear the old items first items.removeAllElements(); // Create grid items from data and add them for(int i = 0; i < favouriteDatas.length; i++){ FavouriteData favData = favouriteDatas[i]; GridItem favItem = new GridItem(display, imageProvider); favItem.setIcon(imageProvider.getImage(favData.getImageFilename())); favItem.setFavData(favData); items.addElement(favItem); } updateItems(); if(repaint){ repaint(); } } protected void paint(Graphics g) { if (Log.TEST) Log.note("[FavouriteArtistsView#paint]-->"); // Draw background g.setColor(display.getColor(Display.COLOR_BACKGROUND)); g.fillRect(0, 0, getWidth(), getHeight()); boolean xOverBounds = false; boolean yOverBounds = false; if(xOffset < 0){ xOffset = 0; xOverBounds = true; }else if(xOffset + getWidth() > gridWidth){ xOffset = gridWidth - getWidth(); xOverBounds = true; } if(yOffset < 0){ yOffset = 0; yOverBounds = true; } else if(yOffset + getHeight() > gridHeight){ yOffset = gridHeight - getHeight(); yOverBounds = true; } // Stop scrolling if both x and y are over bounds // NOTE: if x or y alone would merit a scrolling stop, then scrolling // near grid borders would have bad UX (e.g. scroll would stop because of tiny // y direction change when user wants to scroll in horizontally). // This affects flicking only. if (scrollingActive && xOverBounds && yOverBounds) { stopScrolling(true); } // Check which items are visible and draw only those. for (int i = 0; i < items.size(); i++) { GridItem item = (GridItem) items.elementAt(i); if(item.isVisible(xOffset, yOffset, getWidth(), getHeight())){ item.paint(g, xOffset, yOffset); } } } /** * Center on the item that's closest to the given coordinates. * * @param animate Determines whether the focus movement will be animated. */ private void centerOnClosestItem(int x, int y, boolean animate){ if (Log.TEST) Log.note("[FavouriteArtistsView#centerOnClosestItem]--> x: " + x + " y: " + y); // Cancel any existing focus move thread if(focusMoveThread != null){ focusMoveRunnable.quit(); focusMoveThread = null; } // Find the closest item GridItem closestItem = null; int closestItemDistance = Integer.MAX_VALUE; for (int i = 0; i < items.size(); i++) { GridItem item = (GridItem) items.elementAt(i); int itemCenterX = item.getCenterX(); int itemCenterY = item.getCenterY(); int itemDistance = Math.abs(itemCenterX - x) + Math.abs(itemCenterY - y); if (Log.TEST) Log.note("[FavouriteArtistsView#centerOnClosestItem] item center x: " + itemCenterX + " item center y: " + itemCenterY + " item distance: " + itemDistance); if(itemDistance < closestItemDistance){ closestItemDistance = itemDistance; closestItem = item; } } centerOnItem(closestItem, animate); } /** * Centers on the given item. * * @param item The item to center on. * @param animate Determines whether the focus movement will be animated. */ private void centerOnItem(GridItem item, boolean animate){ if (Log.TEST) Log.note("[FavouriteArtistsView#centerOnItem]-->"); if(item == null){ return; } int xTarget = item.getX() - ITEM_H_MARGIN; int yTarget = item.getY() - ITEM_V_MARGIN; if(animate){ // Animate the movement scrollingActive = true; focusMoveRunnable = new FocusMoveThread(xTarget, yTarget); focusMoveThread = new Thread(focusMoveRunnable); focusMoveThread.start(); } else { // Just set the new coordinates xOffset = xTarget; yOffset = yTarget; } } /** * Center on the item that's closest to the center of the screen. * * @param animate Determines whether focus movement will be animated. */ private void centerOnClosestItem(boolean animate){ if (Log.TEST) Log.note("[FavouriteArtistsView#centerOnClosestItem]"); int screenCenterX = xOffset + getWidth()/2; int screenCenterY = yOffset + getHeight()/2; centerOnClosestItem(screenCenterX, screenCenterY, animate); } /** * @see com.nokia.mid.ui.frameanimator.FrameAnimatorListener#animate(com.nokia.mid.ui.frameanimator.FrameAnimator, int, int, short, short, short, boolean) */ public void animate(FrameAnimator animator, int x, int y, short delta, short deltaX, short deltaY, boolean lastFrame) { if (Log.TEST) Log.note("[FavouriteArtistsView#animate]" + " x: " + x + " y: " + y + " delta: " + delta + "deltaX: " + deltaX + " deltaY: " + deltaY); //NOTE: this affects only flicks, drag is not animated with FrameAnimator // We want to drag the grid, so movement goes to opposite direction xOffset -= deltaX; yOffset -= deltaY; // Scrolling is no longer active if this is the last frame scrollingActive = !lastFrame; repaint(); if(lastFrame == true){ // Start focus move, centerOnClosestItem(true); } } /** * Stops scrolling. * * @param center Whether to start centering. */ private void stopScrolling(boolean center) { scrollingActive = false; animator.stop(); if(center == true){ centerOnClosestItem(true); } } /** * @see com.nokia.mid.ui.gestures.GestureListener#gestureAction(java.lang.Object, com.nokia.mid.ui.gestures.GestureInteractiveZone, com.nokia.mid.ui.gestures.GestureEvent) */ public void gestureAction(Object container, GestureInteractiveZone zone, GestureEvent event) { if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction]-->"); // Block gesture handling if already handling one, // this may happen due to delayed handling of events (delay is needed for showing focus on selected item) if(pendingGestureEvent > 0){ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] pending gesture -> return"); return; } // Stop any drag delay or focus move threads before handling the gesture if(dragDelayThread != null){ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] interrupting drag delay thread"); dragDelayThread.interrupt(); dragDelayThread = null; } if(focusMoveThread != null){ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] quitting focus move thread"); focusMoveRunnable.quit(); focusMoveRunnable = null; focusMoveThread = null; } switch (event.getType()) { case GestureInteractiveZone.GESTURE_DRAG:{ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] drag x: " + event.getDragDistanceX() + " y: " + event.getDragDistanceY()); // In this example the drag gesture is directly used for altering the // visible area coordinates. Note that only the delta values are used. The reason for this // is that gesture event coordinates are screen coordinates and xOffset/yOffset are grid coordinates. // NOTE: Drag gestures are received in very rapid succession, the "whole" // drag (e.g. what the user would perceive as a complete drag gesture) usually ends when a // GESTURE_DROP event is received. stopScrolling(false); // We want to drag the grid, so movement goes to opposite direction xOffset -= event.getDragDistanceX(); yOffset -= event.getDragDistanceY(); repaint(); break; } case GestureInteractiveZone.GESTURE_TAP:{ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] tap"); // Tap gesture is used for item selection. if (scrollingActive) { stopScrolling(false); // First center on the item if scrolling was active. // Note that here we derive the grid coordinates from the event // coordinates and current offset. centerOnClosestItem(xOffset + event.getStartX(), yOffset + event.getStartY(), true); } else if (tapActionId != Actions.INVALID_ACTION_ID) { // Handle gesture only if there's an action set for it handleSelectEvent(event); } break; } case GestureInteractiveZone.GESTURE_DROP: { if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] drop"); // Drop indicates the end of a "whole" drag (see above). stopScrolling(false); // Start the drag delay thread, this delay allows the user some time to continue // dragging before initiating automatic focus movement. dragDelayThread = new Thread(new DragDelayThread()); dragDelayThread.start(); break; } case GestureInteractiveZone.GESTURE_FLICK:{ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] flick"); // This gives the angle in radians. float angle = event.getFlickDirection(); if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] flick angle: " + angle); // Because we're using free angle we want the whole flick speed instead of just x or y. int startSpeed = event.getFlickSpeed(); int direction = FrameAnimator.FRAME_ANIMATOR_FREE_ANGLE; // This affects the deceleration of the scroll. int friction = FrameAnimator.FRAME_ANIMATOR_FRICTION_LOW; scrollingActive = true; // Start the scroll, animate() callbacks will follow. animator.kineticScroll(startSpeed, direction, friction, angle); break; } case GestureInteractiveZone.GESTURE_LONG_PRESS:{ if (Log.TEST) Log.note("[FavouriteArtistsView#gestureAction] long press"); // Long press handling has mostly the same implementation as tap. if (scrollingActive) { stopScrolling(false); centerOnClosestItem(xOffset + event.getStartX(), yOffset + event.getStartY(), true); } else if (longPressActionId != Actions.INVALID_ACTION_ID){ // Handle gesture only if there's an action set for it handleSelectEvent(event); } break; } default: break; } } /** * Getter for selected item. Since the UI is touch based there is no selected item * all the time. In this example the selected item is only valid for the duration of action handling * * @return The selected item. */ public FavouriteData getSelectedItem() { if (Log.TEST) Log.note("[FavouriteArtistsView#getSelectedItem]-->"); return selectedItem.getFavData(); } /** * Common handler function for selection events. * * @param event Gesture event. */ private void handleSelectEvent(GestureEvent event){ if (Log.TEST) Log.note("[FavouriteArtistsView#handleSelectEvent]-->"); // Find the selected item if any. GridItem selected = getItemAt(event.getStartX(), event.getStartY()); if (selected != null) { if (Log.TEST) Log.note("[FavouriteArtistsView#handleSelectEvent] got selected item"); // Draw highlight for the selected item selectedItem = selected; selected.setSelected(true); centerOnItem(selectedItem, false); repaint(); // Delay the event handling, so that user gets a chance to see the highlighted item. // Gesture is stored so that we can handle it later on. pendingGestureEvent = event.getType(); delayThread = new Thread(selectDelay); delayThread.start(); } } /** * Handler for tap gesture. */ private void handleTap() { if (Log.TEST) Log.note("[FavouriteArtistsView#handleTap]-->"); // Delegate handling to command handler commandHandler.handleAction(tapActionId, null, this); } /** * Handler for long press gesture. */ private void handleLongPress() { if (Log.TEST) Log.note("[FavouriteArtistsView#handleLongPress]-->"); //Delegate handling to command handler commandHandler.handleAction(longPressActionId, null, this); } /** * Finds the item with the given coordinates. * * @param x X-coordinate. * @param y Y-coordinate. * @return Item at the given coordinates or null if no item found. */ private GridItem getItemAt(int x, int y) { GridItem item = null; int translatedX = xOffset + x; int translatedY = yOffset + y; for (int i = 0; i < items.size(); i++) { item = (GridItem)items.elementAt(i); if(item.isInItem(translatedX, translatedY)){ return item; } } return null; } }