/*
* 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.gestures.*;
import com.nokia.mid.ui.frameanimator.*;
/**
* Simple list control that uses the Gestures- and Frame Animator APIs for scrolling
* and handling selecting list items. Each list item has an image and a short
* string.
* <p>
* This class illustrates the use of Gestures API and Frame Animator API.
* Points of interests are: the constructor {@link #CustomList(CustomListOwner, short, short)},
* 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_FLICK}
* <p>
* From Frame Animator API, {@link FrameAnimator#kineticScroll(int, int, int, float)} is used
* with {@link FrameAnimator#FRAME_ANIMATOR_VERTICAL} to achieve kinetic scrolling of the list
* (used for flick gesture).
* <p>
* Items can be selected by either short tap or long press. An action can be mapped for both gestures.
* When an item is selected the selection is highlighted briefly to give the user some visual feedback
* of the selection (See {@link #handleSelectEvent} and {@link CustomList.SelectDelay}).
* <p>
* The {@link ListItem} objects represent individual items and are responsible for drawing themselves.
* <p>
* Scrolling is done by modifying the {@link #translateY} value based on the
* values received from the Gestures- and Frame Animator API callbacks. The list
* paint always goes through all list items from first to last, but skips
* drawing of items that aren't partially or fully visible. In theory the list
* items can be of any height (in theory because this hasn't been tested).
* <p>
* This list control does not inherit any {@link Displayable} directly. Instead, the owner
* instance is a {@link Displayable} that delegates actual painting etc. to this class.
* The reason for this is to make this class re-usable ({@link Canvas} and {@link CustomItem})
*/
public class CustomList implements GestureListener, FrameAnimatorListener{
// Member data
/** Owner of this list control. */
private CustomListOwner owner;
/** Short tap action. */
private short tapActionId;
/** Long press action */
private short longPressActionId;
/** Height */
private int height;
/** The selected item, only valid during select action handling. */
private ListItem selectedItem;
/** List items. */
private Vector items;
/** Filtered items */
private Vector originalItems;
/** Filter string */
private String filterString;
/** Y-coordinate of the top of visible area. */
private int translateY = 0;
/** 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 during delay. */
private Thread delayThread;
/** Last known list height */
private int lastKnownListHeight = -1;
/** The MIDlet's main display */
private Display display;
// 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;
}
}
}
/**
* Constructor.
*
* @param owner Parent object (e.g. Canvas or CustomItem).
* @param tapActionId Action for short tap gesture.
* @param longPressActionId Action for long press gesture.
*/
public CustomList(CustomListOwner owner, short tapActionId, short longPressActionId, Display display)
throws FavouriteArtistsException {
if (Log.TEST) Log.note("[CustomList#CustomList]-->");
this.display = display;
this.owner = owner;
this.tapActionId = tapActionId;
this.longPressActionId = longPressActionId;
this.items = new Vector();
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.
GestureInteractiveZone 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(owner, zone) != true){
throw new FavouriteArtistsException("GestureRegistrationManager.register() failed!");
}
// Add a listener for gesture events.
GestureRegistrationManager.setListener(owner, this);
// Create a FrameAnimator instance.
animator = new FrameAnimator();
// Use default values for maxFps an maxPps (zero param 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.
if(animator.register((short)0, (short)0, maxFps, maxPps, this) != true){
throw new FavouriteArtistsException("GestureRegistrationManager.register() failed!");
}
}
/**
* Height of this list.
*/
public void setHeight(int height){
if (Log.TEST) Log.note("[CustomList#setHeight]-->");
this.height = height;
}
/**
* Appends an item to the list.
*
* @param item Item to append.
*/
public void appendItem(ListItem item){
if (Log.TEST) Log.note("[CustomList#appendItem]-->");
items.addElement(item);
}
/**
* Removes an item from the list.
* @param item The item to remove
*/
public void removeItem(ListItem item){
if (Log.TEST) Log.note("[CustomList#appendItem]-->");
items.removeElement(item);
translateY = 0;
}
/**
* Clears the list.
*/
public void clearList(){
if (Log.TEST) Log.note("[CustomList#clearList]-->");
translateY = 0;
selectedItem = null;
items.removeAllElements();
originalItems = null;
filterString = null;
}
/**
* Filters the visible list to items which' text1 contains the given search string.
*
* @param filterString Filter string.
*/
public void filterList(String filterString){
if (Log.TEST) Log.note("[CustomList#filterList]-->");
if(filterString == null || filterString.length() == 0){
return;
}
if (Log.TEST) Log.note("[CustomList#filterList] filterString: " + filterString );
Vector sourceItems = null;
if(this.filterString != null && this.filterString.startsWith(filterString) == true){
if (Log.TEST) Log.note("[CustomList#filterList] using old filter");
// Filter the existing set further
this.filterString = filterString;
sourceItems = items;
} else {
if (Log.TEST) Log.note("[CustomList#filterList] creating new filter");
// Remove any existing filter
if(this.filterString != null){
this.filterString = null;
items = originalItems;
}
// New filter
this.filterString = filterString;
// Store the original items
originalItems = items;
sourceItems = originalItems;
// Create new vector for the filtered items.
items = new Vector();
}
// Filter matching items
for(int i = 0; i < sourceItems.size(); i++){
ListItem item = (ListItem)sourceItems.elementAt(i);
if(item.getText1().indexOf(filterString) >= 0){
items.addElement(item);
}
}
translateY = 0;
}
/**
* Removes possible filter.
*/
public void removeFilter(){
if(originalItems != null){
items = originalItems;
}
originalItems = null;
filterString = null;
}
/**
* Gets the selected item. Only valid during command handling.
*
* @return The selected item or null if nothing selected.
*/
public ListItem getSelectedItem(){
if (Log.TEST) Log.note("[CustomList#getSelectedItem]-->");
return selectedItem;
}
/**
* @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) {
// In this example the y coordinate is used directly.
translateY = y;
// Scrolling is no longer active if this is the last frame
scrollingActive = !lastFrame;
// Request repaint from parent Displayable
owner.requestRepaint();
}
/**
* Stops scrolling.
*/
private void stopScrolling() {
scrollingActive = false;
animator.stop();
}
/**
* @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) {
// 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){
return;
}
switch (event.getType()) {
case GestureInteractiveZone.GESTURE_DRAG:{
if (Log.TEST) Log.note("[CustomList#gestureAction] drag");
// In this example the drag gesture is directly used for altering the
// visible area y-coordinate. Note that only the delta value is used. The reason for this
// is that gesture event coordinates are screen coordinates and translateY is a list coordinate.
// 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. In this example we don't need to know when user stops the drag,
// so GESTURE_DROP is omitted.
if (scrollingActive) {
stopScrolling();
} else {
translateY += event.getDragDistanceY();
owner.requestRepaint();
}
break;
}
case GestureInteractiveZone.GESTURE_TAP:{
if (Log.TEST) Log.note("[CustomList#gestureAction] tap");
if (scrollingActive) {
// Stop scrolling first
stopScrolling();
} else if (tapActionId != Actions.INVALID_ACTION_ID) {
// Select is initiated only when list is not scrolling.
// Handle gesture only if there's an action set for it
handleSelectEvent(event);
}
break;
}
case GestureInteractiveZone.GESTURE_FLICK:{
if (Log.TEST) Log.note("[CustomList#gestureAction] flick");
// Start vertical flick only if the gesture is more vertical than horizontal.
float angle = Math.abs(event.getFlickDirection());
if (Log.TEST) Log.note("[CustomList#gestureAction] flick angle: " + angle);
if((angle > (Math.PI / 4) && angle < (3 * Math.PI / 4))){
if (Log.TEST) Log.note("[CustomList#gestureAction] flick accepted");
// Because we're using only vertical scrolling we use just the y speed.
int startSpeed = event.getFlickSpeedY();
int direction = FrameAnimator.FRAME_ANIMATOR_VERTICAL;
// 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, 0);
}
break;
}
case GestureInteractiveZone.GESTURE_LONG_PRESS:{
if (Log.TEST) Log.note("[CustomList#gestureAction] long press");
//Long press handling has mostly the same implementation as tap.
if (scrollingActive) {
stopScrolling();
} else if (longPressActionId != Actions.INVALID_ACTION_ID){
// Handle gesture only if there's an action set for it
handleSelectEvent(event);
}
break;
}
default:
break;
}
}
/**
* Common handler function for selection events.
*
* @param event Gesture event.
*/
private void handleSelectEvent(GestureEvent event){
if (Log.TEST) Log.note("[CustomList#handleSelectEvent]-->");
ListItem selected = getItemAt(event.getStartX(), event.getStartY());
if (selected != null) {
if (Log.TEST) Log.note("[CustomList#handleSelectEvent] got selected item");
// Draw focus for the selected item
selectedItem = selected;
selected.setSelected(true);
owner.requestRepaint();
// Delay the event handling, so that user gets a chance to see the focus
// 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("[CustomList#handleTap]-->");
animator.unregister();
}
// Delegate handling to owner
owner.handleAction(tapActionId);
}
/**
* Handler for long press gesture.
*/
private void handleLongPress() {
if (Log.TEST) Log.note("[CustomList#handleLongPress]-->");
// Delegate handling to owner
owner.handleAction(longPressActionId);
}
/**
* 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 ListItem getItemAt(int x, int y) {
y -= translateY;
ListItem item = null;
int heightSoFar = 0;
int heightNext = 0;
// Go through all items (pretend that list item heights can vary).
for (int i = 0; i < items.size(); i++) {
item = (ListItem) items.elementAt(i);
heightNext += item.getHeight();
if (y >= heightSoFar && y <= heightNext) {
return item;
}
heightSoFar += item.getHeight();
}
return null;
}
/**
* @see javax.microedition.lcdui.Canvas#paint(javax.microedition.lcdui.Graphics)
*/
protected void paint(Graphics g) {
if (Log.TEST) Log.note("[CustomList#paint] ");
if (translateY > 0) {
// Trying to scroll beyond list start.
translateY = 0;
if (scrollingActive) {
stopScrolling();
}
} else if (lastKnownListHeight != -1 && (translateY + lastKnownListHeight) < height) {
// Trying to scroll beyond list end.
translateY = -lastKnownListHeight + height;
if (scrollingActive) {
stopScrolling();
}
}
if (Log.TEST) Log.note("[CustomList#paint] translateY: " + translateY);
g.translate(0, translateY);
ListItem item = null;
int yOffset = 0;
int yOffsetNext = 0;
int y0 = -translateY;
int y1 = y0 + height;
int i = 0;
g.setColor(display.getColor(Display.COLOR_BACKGROUND));
g.fillRect(0, 0, g.getClipWidth(), g.getClipHeight()*items.size());
g.setColor(display.getColor(Display.COLOR_FOREGROUND));
for (; i < items.size(); i++) {
item = (ListItem) items.elementAt(i);
yOffsetNext += item.getHeight();
if (yOffsetNext < y0) {
// Item is not visible -> skip drawing it.
yOffset = yOffsetNext;
continue;
} else if (yOffset > y1) {
// Item would be drawn "under" the visible area -> stop drawing.
break;
}
if (Log.TEST) Log.note("[CustomList#paint] yOffset: " + yOffset);
item.paint(g, yOffset);
yOffset = yOffsetNext;
}
if (i == items.size()) {
lastKnownListHeight = yOffset;
}
}
}