Building Up a Media Center Application with TWUIK
Part of the “Coding MIDlet with TWUIK” Tutorial Series
TUTORIAL PART 2
The Photo Gallery
Introduction
As a recap, in the previous part of this tutorial, we had seen the basics of TWUIK and how to use a few of its built-in widgets such as the ImageBox and the FishEyeMenu. We had also seen the power of the motion FX engines for adding some dynamics into our little main menu screen. We went further advanced with defining some relationship between the animations by handling their corresponding event notifications. Finally, we defined a structured framework for coding the application and we saw it first in action when we implemented the Main Menu screen.
For this second part, we will look at the implementation of the second screen, the Photo Gallery. The new feature of TWUIK that you will learn in this part will be about how to use the transformation FX engines for resizing/rotating the photo images and the transition FX engines for the slide show feature.
As for using the built-in widgets, this time we will see how to use the PhotoPreviewList for showing the gallery of the photos in thumbnail size (and one of them with a larger preview size). We will also look at how to efficiently load the photos that are passed to this gallery widget. The photos will be loaded from the image files that are found by searching through the file system of the device, assuming the support for JSR-75, of course. The image files we will support will be of PNG or JPEG format.
We will also see how we can create our own widget in TWUIK, and this is exemplified by the implementation of the IndicatorBar widget (we have renamed it to this, instead of ProgressBar which we mentioned so previously in part 1). From this you will learn how to define an animation for the widget, control how it is rendered, as well as how to notify occurring events to the listener. We will later begin the implementation discussion from this material first. So let us begin.
Screen Design
Now we are going to look at the implementation for our second screen in the application, the Photo Gallery. In this screen we allow the user to browse the gallery as a horizontal list of thumbnails, with the selected one showing a larger preview version, and to view the selected photo in a full-screen mode (either landscape or portrait depending on the photo size orientation). The user may also opt for slideshow of all the photos in the gallery that are then displayed one by one using some random transition effects. From this screen, the user can also go back to the main menu or to exit the application. So, let us look at the UI design in further details first:

1. PhotoPreviewList
- For displaying the gallery as a list of photo thumbnails.
- User uses the left/right key on the device to browse the list; the currently selected one will have a larger size of it displayed as a preview version along with the file name description.
- When the user presses the fire key on the selected photo, the full-screen size of it will be displayed, either in landscape or portrait mode, depending on the size orientation of the photo
2. IndicatorBar
- Our own custom widget.
- Will have two modes: block or flashing ball. For this screen, we use the block mode for showing an indicator of the position of currently selected photo among the rest in the gallery.
3. FishEyeMenu
- Recall that all MMCScreen will have an embedded FishEyeMenu widget automatically. For this screen, we will have the following buttons: previous, next, view photo, play slide show, back to main menu, and exit application.
For this screen, we are going to see how the built-in PhotoPreviewList widget can be used to its maximum potentiality. We will use various tools from TWUIK: the ImageUtil utility class for loading JPEG images as well as the TFFXGeometry transformation FX engine for rescaling/rotating the photo images where necessary. In essence, we are going to implement the loading of the photo images in a smooth and efficient multi-threaded manner. All this can be done as the PhotoPreviewList widget has been designed such that it imposes no restrictions on how and what images are loaded.
When constructing a PhotoPreviewList widget, we need to specify the thumbnail bounding size and the preview bounding size (the full viewing mode size depends on the widget size). Because of these fixed sizes, we may need to resize the original photo images. For this purpose, we will use the TWUIK’s powerful TFFXGeometry engines. No matter how powerful the transformation engine is (not to forget it also depends on the device processing capability!), however, there will still certainly be some delay before the transformed image can be completely constructed. To “improve” this, we will show a resized (but very pixelated) version of the image first while we are performing the scaling transformation on the original image. Such pixelated or “blurred” version can be acquired by enlarging the image from the thumbnail version, which can be done with much faster speed, as opposed to enlarging the image from the original version.
The code for the implementation of the Photo Gallery screen class is quite lengthy and therefore we will not show the whole listing of it at once (except for the IndicatorBar custom widget), unlike what we did for the Main Menu screen. Instead we will categorize the discussion into each individual feature/task. First, in the next section we will look at how to create the IndicatorBar widget, our own custom widget.
Creating Your Own Widget: IndicatorBar
All widgets in TWUIK extend from the Component class, which is an abstract entity that represents a visible component as a layer on an AnimationCanvas that it is to be contained within. Creating a new component requires subclassing from this Component class, and then overriding the following abstract methods accordingly:
| protected void animationRestarted (boolean bNeverAnimatedYet) |
| An internal event callback method which will get called every time the animation of this Component has been started/resumed. The bNeverStartedYet parameter will be passed a true value if the animation has just been started, or false if resumed. |
| protected void drawCurrentFrame (Graphics g) |
| An internal event callback method which will get called every time the current frame of this Component needs to be drawn onto the provided graphics context (which usually either represents the canvas or the container). Subclasses are expected to draw the current frame starting at position {0,0} and covers a dimension of {iWidth,iHeight}, although this restriction is not strictly enforced by the TWUIK rendering system. Subclasses should not change the visual state (i.e., advance the frame) of the Component at this method, but rather at the animate callback method instead. Therefore, programmers who want to create their own Components are expected to be able to separate the drawing code from the animation code. Unable to do this would not fail the creation of the new Components, but would particularly defy the purpose of the double-buffering feature when enabled. |
| protected boolean animate() |
| An internal callback method which will get called every time the Component's frame needs to be advanced. Subclasses should place the code that changes the visual state of the Component at this method. Drawing code should be based on the current visual state and separated by being placed at the drawCurrentFrame callback method. Where there is no visual state change required by the time this method is called, a false value should be returned, or otherwise true. This is to inform the TWUIK rendering system as to whether it needs to redraw the current frame of this Component or not (particularly in the case of double-buffered components). |
Furthermore, subclasses of Component are given access to the following protected fields:
| Field Name | Data Type | Purpose/Description |
| iWidth | int | The width of this Component. |
| iHeight | int | The height of this Component. |
| theAttachedCanvas | AnimationCanvas | The AnimationCanvas where this Component or its Panel container is contained within (null if none). |
| theAttachedPanel | Panel | The Panel where this Component or its Panel container is contained within (null if none) |
| bVisible | boolean | The visibility (either shown or hidden). |
| bDoubleBuffered | boolean | Whether this Component is double-buffered or not. |
| cel | ComponentEventListener | The ComponentEventListener object registered as the listener of the component events that can be fired by this Component (or null if none). |
Despite given access to the above fields, subclasses must not modify them directly. Instead, the corresponding accessor methods should be called. For example, the setSize method can be used to modify iWidth and/or iHeight while the setEventListener method can be used to modify cel. The reason for making these (i.e., actually internal) fields accessible by subclasses are purely for optimization purpose since these fields are very frequently accessed throughout a TWUIK-based application.
As we have discussed much earlier in tutorial part 1, a Component may generate component events. Instances of component events are reported to the registered ComponentEventListener object. Component events are those that are fired by and specific to that generating Component and the events are differentiated using their event ID that is specific to each Component type.
Now that we know the basics of subclassing the Component class, we are able to get started with coding the implementation of our own custom widget, IndicatorBar. The design for this indicator widget is that its state is represented by a current value that lies between a given low value and a given high value (inclusive). The value can be any integer number. The current value is meant to be reset progressively by the client code. The constructed IndicatorBar can be of one of two modes: basic block-based or animated flashing-ball. The block-based mode will be used for our Photo Gallery screen as we will see very soon. The flashing-ball mode will be reserved for the Audio Player and Video Player screens later on (which we will discuss in the next tutorial parts). Nevertheless, we will just go ahead with the implementation of these two modes here.
Block-based mode
Flashing-ball mode
The code for implementing this widget is rather straightforward in fact, but from this you will learn about the TINY Extra’s DrawingUtil utility class for rendering purpose, how to make your custom widget component animates (i.e., based on the concept of separation of code between drawing and animating) as well as how to notify events that are fired by the widget. So, let us have a look at the whole 200-line code listing first here, and then part-by-part explanation will follow.
Listing 2.1. Code for IndicatorBar
import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; import com.tricastmedia.twuik.*; import com.tricastmedia.twuik.extras.*;
public class IndicatorBar extends Component{
/*Public Constants*/
public static final int MODE_BLOCK = 0;
public static final int MODE_FLASHINGBALL = 1;
public static final int EVENTID_FLASHING = 0; //iParam: 0 for off, 1 for on
/*Private Constants*/
private static final int BLOCK_INDICATORSIZE_MIN = 18;
private static final int DEFAULT_FLASHINGTIMEDELAY = 150;
/*Secondary Properties*/
private int iMode;
private int iHighValue;
private int iLowValue;
private int iFlashingTimeDelay;
/*Working Variables*/
private boolean bFrameJustChanged;
private int iCurValue;
private int iIndicatorSize;
private int iIndicatorPos;
private int iMarginLeft;
private int iMarginRight;
private long lLastFlashingAnimTime;
private boolean bFlashingBallState;
/*Images*/
private Image blockInsideImg;
private Image blockOutlineLeftImg;
private Image blockOutlineMiddleImg;
private Image blockOutlineRightImg;
private Image flashingBallBallFramesImg;
private Image flashingBallOutlineLeftImg;
private Image flashingBallOutlineMiddleImg;
private Image flashingBallOutlineRightImg;
/********************************
CONSTRUCTORS
********************************/
public IndicatorBar (int iAnimTickCount, int iX, int iY,
int iWidth, int iHeight){
super (iAnimTickCount,false,iX,iY,iWidth,iHeight);
/*Initialize properties and working variables*/
iMode = MODE_BLOCK; //default to block mode
iHighValue = iLowValue = 0;
iFlashingTimeDelay = DEFAULT_FLASHINGTIMEDELAY;
iCurValue = 0;
iMarginLeft = iMarginRight = 0;
reinit();
/*Load the tile images*/
blockInsideImg =
ImageUtil.loadImageFromResource ("/block_inside.png");
blockOutlineLeftImg =
ImageUtil.loadImageFromResource ("/block_outline_left.png");
blockOutlineMiddleImg =
ImageUtil.loadImageFromResource ("/block_outline_middle.png");
blockOutlineRightImg =
ImageUtil.loadImageFromResource ("/block_outline_right.png");
flashingBallBallFramesImg =
ImageUtil.loadImageFromResource ("/flashingball_ballframes.png");
flashingBallOutlineLeftImg =
ImageUtil.loadImageFromResource ("/flashingball_outline_left.png");
flashingBallOutlineMiddleImg =
ImageUtil.loadImageFromResource ("/flashingball_outline_middle.png");
flashingBallOutlineRightImg =
ImageUtil.loadImageFromResource ("/flashingball_outline_right.png");
}
/********************************
HELPER METHODS
********************************/
private void reinit(){
switch (iMode){
case MODE_BLOCK: iMarginLeft = 8; iMarginRight = 8; break;
case MODE_FLASHINGBALL: iMarginLeft = 6; iMarginRight = 6; break;
};
final int iTotalAreaWidth = iWidth-iMarginLeft-iMarginRight;
if (iMode == MODE_BLOCK)
iIndicatorSize = Math.max (BLOCK_INDICATORSIZE_MIN,
iTotalAreaWidth / (iHighValue-iLowValue+1));
else
iIndicatorSize = 1;
final int iScrollableAreaWidth = iTotalAreaWidth - iIndicatorSize;
iIndicatorPos = (iHighValue-iLowValue == 0) ? iMarginLeft
: (iMarginLeft
+ ((iCurValue-iLowValue) * iScrollableAreaWidth
/ (iHighValue-iLowValue)));
bFrameJustChanged = true;
}
/********************************
INTERNAL EVENT CALLBACK HANDLERS
********************************/
protected void drawCurrentFrame (Graphics g){
switch (iMode){
case MODE_BLOCK:
/*Draw the outline borders*/
g.drawImage (blockOutlineLeftImg,0,iHeight/2,
Graphics.LEFT | Graphics.VCENTER);
g.drawImage (blockOutlineRightImg,iWidth-1,iHeight/2,
Graphics.RIGHT | Graphics.VCENTER);
DrawingUtil.drawTiledRectangle (g,blockOutlineLeftImg.getWidth(),
Math.max (0,(iHeight-blockOutlineMiddleImg.getHeight())/2),
iWidth-blockOutlineLeftImg.getWidth()-blockOutlineRightImg.getWidth(),
blockOutlineMiddleImg.getHeight(),
blockOutlineMiddleImg);
/*Draw the block indicator*/
DrawingUtil.drawTiledRectangle (g,iIndicatorPos,
Math.max (0,(iHeight-blockInsideImg.getHeight())/2),
iIndicatorSize,
blockInsideImg.getHeight(),
blockInsideImg);
break;
case MODE_FLASHINGBALL:
/*Draw the outline*/
g.drawImage (flashingBallOutlineLeftImg,0,iHeight/2,
Graphics.LEFT | Graphics.VCENTER);
g.drawImage (flashingBallOutlineRightImg,iWidth-1,iHeight/2,
Graphics.RIGHT | Graphics.VCENTER);
DrawingUtil.drawTiledRectangle (g,flashingBallOutlineLeftImg.getWidth(),
Math.max (0,(iHeight-flashingBallOutlineMiddleImg.getHeight())/2),
iWidth-flashingBallOutlineLeftImg.getWidth()
-flashingBallOutlineRightImg.getWidth(),
flashingBallOutlineMiddleImg.getHeight(),
flashingBallOutlineMiddleImg);
/*Draw the ball*/
if (!bFlashingBallState){ //for smaller ball
g.drawRegion (flashingBallBallFramesImg,
0,0,
flashingBallBallFramesImg.getWidth()/2,
flashingBallBallFramesImg.getHeight(),
Sprite.TRANS_NONE,
iIndicatorPos,iHeight/2,
Graphics.HCENTER | Graphics.VCENTER);
} else{ //for larger ball
g.drawRegion (flashingBallBallFramesImg,
flashingBallBallFramesImg.getWidth()/2,0,
flashingBallBallFramesImg.getWidth()/2,
flashingBallBallFramesImg.getHeight(),
Sprite.TRANS_NONE,
iIndicatorPos,iHeight/2,
Graphics.HCENTER | Graphics.VCENTER);
};
break;
};
}
protected void animationRestarted (boolean bNeverAnimatedYet){
if (bNeverAnimatedYet)
bFrameJustChanged = true; //so that a frame is drawn for the first time
lLastFlashingAnimTime = System.currentTimeMillis();
}
protected boolean animate(){
if (iMode == MODE_FLASHINGBALL){
long lCurTime = System.currentTimeMillis();
if (lCurTime-lLastFlashingAnimTime >= iFlashingTimeDelay){
bFlashingBallState = !bFlashingBallState;
lLastFlashingAnimTime = lCurTime;
bFrameJustChanged = true;
if (cel != null)
cel.componentEventFired (this,EVENTID_FLASHING,
null,bFlashingBallState ? 1 : 0);
};
}; if (bFrameJustChanged){
bFrameJustChanged = false;
return true;
} else return false;
}
/********************************
PUBLIC APIs
********************************/
public void setSize (int iWidth, int iHeight){ //just to override
super.setSize (iWidth,iHeight);
reinit();
}
public void configures (int iMode, int iLowValue, int iHighValue){
/*Checks parameters validity*/
if (iMode != MODE_BLOCK
&& iMode != MODE_FLASHINGBALL) throw new IllegalArgumentException();
if (iLowValue > iHighValue) throw new IllegalArgumentException();
/*Reconfigures*/
this.iMode = iMode;
this.iLowValue = iLowValue;
this.iHighValue = iHighValue;
/*Reflects the configuration changes*/
reinit();
setCurrentValue (iCurValue);
}
public int getHighValue(){ return iHighValue; }
public int getLowValue(){ return iLowValue; }
public void setCurrentValue (int iCurValue){
if (iCurValue < iLowValue) iCurValue = iLowValue;
if (iCurValue > iHighValue) iCurValue = iHighValue;
this.iCurValue = iCurValue;
final int iTotalAreaWidth = iWidth-iMarginLeft-iMarginRight;
final int iScrollableAreaWidth = iTotalAreaWidth - iIndicatorSize;
iIndicatorPos = (iHighValue-iLowValue == 0) ? iMarginLeft
: (iMarginLeft
+ ((iCurValue-iLowValue) * iScrollableAreaWidth
/ (iHighValue-iLowValue)));
bFrameJustChanged = true;
}
public int getCurrentValue(){ return iCurValue; }
}
There you go with the complete code listing for the IndicatorBar widget implementation. Not bad, huh?! The code basically comprises of internal state variables, initializations, and helper methods, as well as overriding methods that implement the three internal event callback methods that we discussed earlier, namely animationRestarted, drawCurrentFrame, and animate. The widget also provides the following public API methods: configures, getHighValue, getLowValue, setCurrentValue, and getCurrentValue.
For either mode of the IndicatorBar, we draw the resulting frame based on tile images so that it is resizable (in terms of width). For example, we have the left and right part images of the bar, and then use a middle part tile image to fill up the resizable gap between the left and the right parts. TWUIK provides a convenient drawing utility method for this purpose, called drawTiledRectangle which is a method of the TINY Extra’s DrawingUtil class. The API signature of this method is as follows (the tiling is done both horizontally and vertically using the specified tile image such that the specified rectangular area is all filled eventually):
public static void drawTiledRectangle (Graphics g,
int iX, int iY,
int iWidth, int iHeight,
Image tileImg)
Also note how we have separated the drawing code from the animation code, as in the drawCurrentFrame method and the animate method. First of all, note that the block-based mode does not need to animate, so the drawing code just needs to render the frame based on the last changed current value. On the other hand, for the flashing-ball mode, the animation code will check, based on the designated interval time, to periodically switch between using the smaller or the larger ball image, interleavingly. It uses the boolean variable bFlashingBallState to indicate this switch between the designated interval time. The drawing code then simply needs to accordingly render the frame based on this current visual state change if any. And for either case, when the current frame has been just changed, we set the boolean variable bFrameJustChanged to true so that the next time the animate method is called, it will check this and return the appropriate value accordingly, as can be found in the listing done as such:
protected boolean animate(){
...
if (bFrameJustChanged){
bFrameJustChanged = false;
return true;
} else return false;
}
To demonstrate event notification mechanism, we makes the flashing-ball mode of the IndicatorBar to fire event whenever it switches between the smaller and the larger ball image. Firing a component event implies notifying the event to the registered component event listener object (if any). This is exemplified by the following code in the listing:
if (cel != null)
cel.componentEventFired (this,EVENTID_FLASHING,
null,bFlashingBallState ? 1 : 0);
When notifying an event by calling the componentEventFired callback, there are two parameters that can be passed: one of type Object and another of type int. In the example above, we do not need to pass any Object parameter, and thus it is passed as null. On the other hand, we use the int parameter to indicate whether the flashing state is currently on (larger ball) or off (smaller ball).
Adding a New Screen
Now that we have all the widgets and tools ready, we can then introduce the new screen into existing our multi media application. Recall from our discussion in tutorial part 1 for the coding framework that the MMCMIDlet is the screen controller and that all screens extend from the abtract class MMCScreen. When we introduced the Main Menu screen, we basically implemented a method called gotoMainMenu in MMCMIDlet, and implement the MainMenu class as a subclass of MMCScreen. Therefore, to introduce the Photo Gallery screen, we just need to do similar things. First, we implement the screen switching method called gotoPhotoGallery (which will be called when the Photo Gallery menu is fired in the MainMenu) as follows:
void gotoPhotoGallery(){
/*Creates a new PhotoGallery screen, and
initializes screen-related variables*/
photoGallery = new PhotoGallery (this);
curScreen = photoGallery;
curCanvas = photoGallery.canvas;
iPrevScreenID = iCurScreenID;
iCurScreenID = SCREENID_PHOTOGALLERY;
/*Attach the application-level components onto the canvas*/
addMenu (curCanvas);
addIndicatorBar (curCanvas);
/*Make the PhotoGallery screen canvas as the current one*/
curCanvas.setEventListener (this);
TWUIKDisplay.setCurrent (curCanvas);
photoGallery.activated();
/*Ensure that any temporary objects on the heap particularly
from previous screen are garbage-collected*/
System.gc();
}
Note how similar the structure is with the implementation of gotoMainMenu that we have seen in tutorial part 1. Basically this method constructs and sets up the Photo Gallery screen, and then attaches the required application-level components (i.e., for this screen, in addition to FishEyeMenu, we also have IndicatorBar), and then finally activates and displays the screen.
Also note the calls to addMenu and addIndicatorBar for attaching the application-level. We need to modify the code in these two methods that does specific initializations depending on which screen it is to be attached to. Recall that in tutorial part 1, we check if the screen is the Main Menu, then we add the buttons like Photo Gallery, Audio Player, Video Player, Settings, and Exit. We have to do same thing here. As we have mentioned in the screen design discussion, for the Photo Gallery screen, we have the following buttons: Previous, Next, View Photo, Play Slide Show, Back to Main Menu, and Exit. Thus, we modify the addMenu method as follows:
void addMenu (AnimationCanvas canvas){
...
/*Specific initializations depending on which screen*/
switch (iCurScreenID){
case SCREENID_MAINMENU:
...
case SCREENID_PHOTOGALLERY:
menu.setFEPanelBackground (null, 0x000000);
menu.removeAllButton();
menu.addButton (
btnFactory.getNeonButton (ButtonFactory.ID_PREV));
menu.addButton (
btnFactory.getNeonButton (ButtonFactory.ID_NEXT));
menu.addButton (
btnFactory.getNeonButton (ButtonFactory.ID_VIEWPHOTO));
menu.addButton (
btnFactory.getNeonButton (ButtonFactory.ID_SLIDE));
menu.addButton (
btnFactory.getNeonButton (ButtonFactory.ID_TOMENU));
menu.addButton (
btnFactory.getNeonButton (ButtonFactory.ID_EXIT));
switch (iPrevScreenID){
default: menu.setCurSel (0); break;
};
break;
//...similarly for other screens
};
...
}
Similarly, for the IndicatorBar, we also need to modify the code that does specific initializations depending on which screen it is to be attached to. Basically, we need to reconfigures the IndicatorBar as to choosing the appropriate mode (either block-based or flashing-ball) for the corresponding screen. For the Photo Gallery screen, we use the block-based mode, so we code:
void addIndicatorBar (AnimationCanvas canvas){
...
/*Specific initializations depending on which screen*/
switch (iCurScreenID){
case SCREENID_PHOTOGALLERY:
indicatorBar.configures (IndicatorBar.MODE_BLOCK,0,0);
break;
//...similarly for other screens
};
...
}
There we go with the modification of the existing MMCMIDlet code that we need to do before we can get started with the implementation of the Photo Gallery screen. Now to implement the screen, as we did for the Main Menu screen, we need to create a new class called PhotoGallery that extends the MMCScreen. Our discussion for the implementation of the Photo Gallery screen that follows in next sections will revolve around the codes that are placed inside this PhotoGallery class.
The Thumbnail Gallery
TWUIK provides a built-in widget called PhotoPreviewList. This is a perfect widget for our photo gallery purpose. It shows a list of photo thumbnails where the user can browse through by navigating the current selection, and can then view it in full-mode if he/she wishes to. Furthermore, when any thumbnail is selected, a larger version of it will be displayed as a preview first. We will make use of this feature as it is. So, without further ado, let us look at how to use this widget in details. Following is the API signature for constructing a PhotoPreviewList object:
public PhotoPreviewList (int iAnimTickCount,
int iX, int iY,
int iWidth, int iHeight,
int iMode,
int iListSize,
int iThumbBoundingSize,
int iPreviewBoundingSize,
TwuikFont detailsFont,
Callback callback)
The PhotoPreviewList widget offers a certain degree of customization when it is constructed (based on the parameters we pass for the aforementioned constructor). The list can be either in vertical (MODE_VERTICAL) or horizontal mode (MODE_HORIZONTAL). One limitation would be that the list size (i.e., number of photos in the gallery) has to be predefined when the widget is constructed and is not changeable thereafter. But this will not be of much concern practically, especially in our case here, where the photo gallery has a fixed list of photos in it.
As for customizing the appearance, we are free to set any value for the thumbnail bounding size and the preview bounding size, as well as for the font used for drawing the details description text. The bounding size is the frame size for the thumbnail/preview image, regardless of whether it is in landscape/portrait/square orientation. So say, the bounding size for the thumbnail is 20 and the image is of landscape orientation, then the size of the thumbnail will be 20x15 (because the aspect ratio used by PhotoPreviewList is 4:3, or 1.3333 – this can be known from its floating-point public field named PHOTO_SIZERATIO). Likewise, if the image is of portrait orientation and the bounding size for the preview is 90, then the size of the preview will be 67x90.
Now that we know the basics of customizing the PhotoPreviewList widget when it is constructed, let us look at the code that creates and sets it up below:
class PhotoGallery extends MMCScreen
implements PhotoPreviewList.Callback, ComponentEventListener{ /*Private Constants*/
private static final int BOUNDINGSIZE_THUMB = 20;
private static final int BOUNDINGSIZE_PREVIEW = 90;
... /*Components*/
private PhotoPreviewList ppl;
... /*Working variables*/
private static LinkedList fileNamesNURLs;
... public PhotoGallery (MMCMIDlet midlet){
...
/*Initialize components*/
ppl = new PhotoPreviewList (1,0,0,
canvas.getWidth(),
canvas.getHeight(),
PhotoPreviewList.MODE_HORIZONTAL,
fileNamesNURLs.size(),
BOUNDINGSIZE_THUMB,
BOUNDINGSIZE_PREVIEW,
MMCMIDlet.fontSysPropSmallBold,
this);
ppl.setEventListener (this);
...
}
...
}
Note that we specify fileNamesNURLs.size() as the list size. fileNamesNURLs is a LinkedList variable that we have previously initialized (statically when the class is loaded) to be a list of file name and url/path pairs. Basically, we initialize the gallery by searching for PNG and JPEG image files in the file system of the device (assuming that it supports JSR-75, obviously). To do this we search for files of name *.PNG or *.JPG or *.JPEG in a set of directories of certain path names in any root that can be found in the file system of the device. We will abstract this search into a media file search so that we can also later use it for the Audio Player and Video Player screens. For this, we implement a generic static method of the following signature in MMCMIDlet:
static LinkedList searchForMediaFiles (String[] searchPathStrs,
String[] targetFileExtStrs)
We will assume the implementation of the above method and will not show the code here therefore. Basically this method will search the file system of the device for the specified media files (based on extension name) in all the specified paths, and then returns the result as a LinkedList of String arrays (with first element being the file name and the second element being the full path name). With this, we can then initialize our gallery with code such as:
static{
fileNamesNURLs = MMCMIDlet.searchForMediaFiles (
new String[]{ "",
"Picture/",
"Pictures/",
"Images/",
"Nokia/Images/",
},
new String[]{ "PNG",
"JPG",
"JPEG",
});
};
And then when the screen is activated, we reconfigure the IndicatorBar accordingly by implementing the following MMCScreen callback method as such (i.e., depending on the number of photos found in the gallery):
public void activated(){
midlet.indicatorBar.configures (IndicatorBar.MODE_BLOCK,
1,Math.max (1,fileNamesNURLs.size()));
}
Back to the earlier initialization code, note that we specify the PhotoGallery object itself as the callback object for the constructor to the PhotoPreviewList widget. In essence, the PhotoPreviewList widget does not load any images by its own. It is simply a list of thumbnail “placeholders”, and when each is required, it will just delegate the callback object to return the loaded image accordingly, if any. The Callback data type is an interface, and is defined as an inner interface in PhotoPreviewList as follows:
public static interface Callback{
public Image PPL_getThumbImage (PhotoPreviewList ppl, int iIndex);
public void PPL_unloadThumbImage (PhotoPreviewList ppl, int iIndex);
public Image PPL_getPreviewImage (PhotoPreviewList ppl, int iIndex);
public void PPL_unloadPreviewImage (PhotoPreviewList ppl, int iIndex);
public Image PPL_getViewingImage (PhotoPreviewList ppl, int iIndex);
public void PPL_unloadViewingImage (PhotoPreviewList ppl, int iIndex);
public String PPL_getDetailsText (PhotoPreviewList ppl, int iIndex);
};
The callback object does not even have to return the image immediately at the time the corresponding callback method is called if it does not want to. It can just return a null value first, and then when the required image is ready, the callback object can then update this by calling one of the following update-notification API methods provided by PhotoPreviewList:
public final void updateThumbImage (int iIndex, Image newThumbImg)
public final void updatePreviewImage (Image newCurPreviewImg,
int iImgIndex)
public final void updateViewingImage (Image newCurViewingImg,
int iImgIndex)
In fact, we will do it this way for our Photo Gallery, i.e., to postpone the returning of the required thumbnail/preview/full-mode images and then use the update-notification APIs above when they are actually ready. This is because we will use a thread that will process the requests in a queue, which we will see in details in next section.
Note that we have decided to support both PNG and JPEG image formats. For loading PNG images, MIDP’s LCDUI has a built-in support for it. For loading JPEG images, it does not however. TWUIK, fortunately, provides a built-in support for loading JPEG images, either from resource file, from file system, or from input stream. These features also exist for PNG images as TWUIK may have its own optimized PNG decoders. All these are provided by the TINY Extra’s ImageUtil utility class. Following are the API signatures for the utility methods that it provides:
public static Image loadImageFromResource (String resourcePathStr)
public static Image loadImageFromFile (String filePathStr)
throws IOException
public static Image loadPNGImageFromInputStream (InputStream is)
public static Image loadJPEGImageFromInputStream (InputStream is)
For all the methods mentioned above, they return the loaded image as an object of LCDUI’s Image, so you can always still use any of the image drawing/manipulation functions provided by LCDUI. And likewise, for all the methods, when the requested source of image cannot be used to successfully load the resulting image, a null value will be returned. Note that for either loading from resource or file system, the methods have a generic name (as opposed to loadXXXImage...). ImageUtil will implicitly look at the extension of the specified filename to decide on which format the image file is of. So for a file with name *.PNG, ImageUtil will assume that it is of PNG format. On the other hand, for a file with name *.JPG or *.JPEG, ImageUtil will assume that it is of JPEG format. No support exists yet for other file formats, and when there are, similar API patterns will follow.
Now that we know how to create, set up, and customize a PhotoPreviewList widget, we need to know how to actually provide a stimulus to it, i.e., to control it based on user input events. PhotoPreviewList provides the following stimulus APIs:
public final void navigatePrev()
public final void navigateNext()
public final void viewUnview()
navigatePrev and navigateNext simply instructs the PhotoPreviewList to navigate the selection in the list to the previous one (i.e., left in horizontal mode or up in vertical mode) or the next one (i.e., right in horizontal mode or down in vertical mode), respectively. viewUnview basically instructs the PhotoPreviewList to either switch to the full-mode view of the current selected photo or back to normal view.
Other APIs provided by PhotoPreviewList include the following:
public final int getSelectedIndex()
public final boolean isInViewingMode()
public final void showHideSelectionFocus (boolean bShow)
getSelectedIndex returns the index of the selected photo in the list (starts from 0) while isInViewingMode returns a boolean as to whether the selected photo is currently being viewed in full-mode or not. showHideSelectionFocus will either show or hide the selection focus in the list (which is shown as another frame border color).
Based on all these APIs, we then implement the code that intercept the LEFT/RIGHT, UP/DOWN, and FIRE key events in PhotoGallery to call these methods accordingly, as follows:
public void keyPressed (int iKeyCode){
...
switch (iKeyCode){
default:
switch (canvas.getGameActionNoAlt (iKeyCode)){
case Canvas.UP: //switch focus to the PhotoPreviewList
if (bMenuCanBeInteracted){
ppl.showHideSelectionFocus (true);
bMenuCanBeInteracted = false;
};
break;
case Canvas.DOWN: //switch focus to the FishEyeMenu
if (!ppl.isInViewingMode() && !bMenuCanBeInteracted){
ppl.showHideSelectionFocus (false);
bMenuCanBeInteracted = true;
};
break;
case Canvas.LEFT: //select previous photo
if (!bMenuCanBeInteracted) ppl.navigatePrev();
break;
case Canvas.RIGHT: //select next photo
if (!bMenuCanBeInteracted) ppl.navigateNext();
break;
case Canvas.FIRE: //switch to full-mode viewing or back
if (ppl.isInViewingMode()) ppl.viewUnview();
else if (!bMenuCanBeInteracted) ppl.viewUnview();
break;
};
break;
};
}
PhotoPreviewList also can fire one component event, namely EVENTID_SELECTIONCHANGED. This event is fired every time the selection in the list has been changed (i.e., as a result of call to navigatePrev or navigateNext). We need to register PhotoGallery as the event listener for the PhotoPreviewList widget (which we have done when we constructed the widget object earlier) and then intercept this event, and then update our IndicatorBar accordingly. So we implement it as follows:
public void componentEventFired (Component c, int iEventID,
Object paramObj, int iParam){
if (c == ppl){
switch (iEventID){
case PhotoPreviewList.EVENTID_SELECTIONCHANGED:
midlet.indicatorBar.setCurrentValue (iParam+1);
break;
};
};
}
Loading and Rescaling/Rotating the Photo Images
Since the PhotoPreviewList widget has been designed such that it does not impose any implementation technique for loading the required images (i.e. by making use of callbacks which we have seen earlier), we will add some animation sugar into it! For both the preview and full-mode images, we will first show a blurred version before it is then replaced with the clear version. There will be two benefits of doing this: 1) it gives quite some eye catching animation effects; and 2) since rescaling an image for a clear version would take some time in general, it would be better that we can show at least something quickly first to the user as to psychologically provide a fast-response-time feedback upon the user action.
First of all, do not be misled by the term “blurred” that we use here – it is nothing like the Gaussian nor the Motion blur effects. Instead, it simply implies a highly pixelated version of the image as a result of enlarging it from a much smaller version (i.e., for the preview one, it’s enlarged from the thumbnail version, while for the full-mode one, it’s enlarged from the preview one).
We are going to maintain a pool of images for the thumbnails as a list of the size of the number of photos in the gallery. Each index in the pool list corresponds to the index of the photo in the PhotoPreviewList. And for each entry, it will be either null or the corresponding thumbnail Image object. The Callback of PhotoPreviewList will be notified either to load or unload the specified thumbnail image. When asked to unload, we just remove it from our pool by setting the entry at that index to null. And when asked to load, we first check the corresponding entry in our pool, if there is an Image object, then we just need to return it, otherwise, we line up a task for loading it into our task queue (for which we will discuss next).
For the purpose of loading images based on request by PhotoPreviewList, we will start a thread which acts as a task executor that keeps checking the task queue periodically for any pending task. This is to say that we maintain a queue of delayable tasks that are executed one by one in First-In-First-Out (FIFO) manner. Therefore, each loading request (regardless of whether for thumbnail, or preview, or full-mode) will be en-queued as a delayable task which is to be eventually executed by the task executor thread. Thus, with every loading request, we will not slow down the animation of the PhotoPreviewList. We do this in the background with the help of the task executor thread, instead. With this design, we thus implement the following code in the PhotoGallery class to define the data structure and to starts the task executor thread:
/*Private Constants*/
private static final int THREADEXECUTOR_INTERVALDELAY = 30;
... /*Inner Classes*/
private static class DelayableTask{ //the task data structure
private static final int TASKKIND_LOADINGTHUMB = 0;
private static final int TASKKIND_LOADINGPREVIEW_BLURRED = 1;
private static final int TASKKIND_LOADINGPREVIEW_NORMAL = 2;
private static final int TASKKIND_LOADINGVIEWING_BLURRED = 3;
private static final int TASKKIND_LOADINGVIEWING_NORMAL = 4;
private int iTaskKind;
private int iImgIndex;
}; ... /*Working Variables*/
...
private LinkedList taskQueueList; //the FIFO task queue
private Thread taskExecutorThread; //the task executor thread
private LinkedList thumbImgSoFarList; //the thumbnails pool
private TFFXGeometry tffxGeom; //a reusable transform engine object
private Image lastPreviewImg; //to maintain the last loaded preview public PhotoGallery (MMCMIDlet midlet){
/*Initialize task-relateds*/
taskQueueList = new LinkedList();
taskExecutorThread = new Thread(){
public void run(){
while (taskExecutorThread != null){ //the loop
if (taskQueueList.size() > 0){ //if pending task, pick it
DelayableTask nextTask =
(DelayableTask) taskQueueList.elementAt (0);
try{
executeTask (nextTask);
} catch (Exception ex){ ex.printStackTrace(); };
};
try{ //the interval
Thread.sleep (THREADEXECUTOR_INTERVALDELAY);
} catch (InterruptedException ex){ /*ignored*/ };
};
}
};
taskExecutorThread.setPriority (Thread.MIN_PRIORITY);
taskExecutorThread.start(); ... /*Initialize working variables*/
...
thumbImgSoFarList = new LinkedList();
tffxGeom = new TFFXGeometry();
}
And then we implement the callback methods accordingly, as follows (along with some helper methods):
/*For checking whether the specified task (with same image index and
same task kind) has already been en-queued or not, with the index in
the queue being the return value if so, or otherwise -1*/
private int isTaskInQueue (int iImgIndex, int iTaskKind){
for (int i=0; i < taskQueueList.size(); i++){
DelayableTask task = (DelayableTask) taskQueueList.elementAt (i);
if (task.iImgIndex == iImgIndex && task.iTaskKind == iTaskKind){
return i;
};
};
return -1;
}
/*For removing the specified task (with same image index and same task
kind) from the task queue if it has been en-queued*/
private void removeTaskInQueue (int iImgIndex, int iTaskKind){
for (int i=0; i < taskQueueList.size(); i++){
DelayableTask task = (DelayableTask) taskQueueList.elementAt (i);
if (task.iImgIndex == iImgIndex && task.iTaskKind == iTaskKind){
taskQueueList.removeElementAt (i);
return;
};
};
}
public Image PPL_getThumbImage (PhotoPreviewList ppl, int iIndex){
/*Get from the pool list if exists*/
Image availableImg = null;
if (iIndex < thumbImgSoFarList.size()){
availableImg = (Image) thumbImgSoFarList.elementAt (iIndex);
} else thumbImgSoFarList.addElement (null);
/*Otherwise, en-queue a task to load it*/
if (availableImg == null
&& isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGTHUMB) == -1){
DelayableTask task = new DelayableTask();
task.iTaskKind = DelayableTask.TASKKIND_LOADINGTHUMB;
task.iImgIndex = iIndex;
taskQueueList.addElement (task);
return null; //returns null first
} else return availableImg;
}
public void PPL_unloadThumbImage (PhotoPreviewList ppl, int iIndex){
/*If there is this task being en-queued, de-queue it*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGTHUMB) != -1){
removeTaskInQueue (iIndex,DelayableTask.TASKKIND_LOADINGTHUMB);
};
/*Remove the loaded image from the pool list*/
thumbImgSoFarList.setElementAt (null,iIndex);
System.gc();
}
public Image PPL_getPreviewImage (PhotoPreviewList ppl, int iIndex){
/*En-queues a task to load the blurred version of preview*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGPREVIEW_BLURRED) == -1){
DelayableTask task = new DelayableTask();
task.iTaskKind = DelayableTask.TASKKIND_LOADINGPREVIEW_BLURRED;
task.iImgIndex = iIndex;
taskQueueList.addElement (task);
};
/*Followed by a task to load the normal version of preview*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGPREVIEW_NORMAL) == -1){
DelayableTask task = new DelayableTask();
task.iTaskKind = DelayableTask.TASKKIND_LOADINGPREVIEW_NORMAL;
task.iImgIndex = iIndex;
taskQueueList.addElement (task);
};
return null; //returns null first
}
public void PPL_unloadPreviewImage(PhotoPreviewList ppl,int iIndex){
/*If there is this task (blurred) being en-queued, de-queue it*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGPREVIEW_BLURRED) != -1){
removeTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGPREVIEW_BLURRED);
};
/*If there is this task (normal) being en-queued, de-queue it*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGPREVIEW_NORMAL) != -1){
removeTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGPREVIEW_NORMAL);
};
lastPreviewImg = null; System.gc();
}
public Image PPL_getViewingImage (PhotoPreviewList ppl, int iIndex){
/*En-queues a task to load the blurred version of full-mode*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGVIEWING_BLURRED) == -1){
DelayableTask task = new DelayableTask();
task.iTaskKind = DelayableTask.TASKKIND_LOADINGVIEWING_BLURRED;
task.iImgIndex = iIndex;
taskQueueList.addElement (task);
};
/*Followed by a task to load the normal version of full-mode*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGVIEWING_NORMAL) == -1){
DelayableTask task = new DelayableTask();
task.iTaskKind = DelayableTask.TASKKIND_LOADINGVIEWING_NORMAL;
task.iImgIndex = iIndex;
taskQueueList.addElement (task);
};
return null;
}
public void PPL_unloadViewingImage(PhotoPreviewList ppl,int iIndex){
/*If there is this task (blurred) being en-queued, de-queue it*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGVIEWING_BLURRED) != -1){
removeTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGVIEWING_BLURRED);
};
/*If there is this task (normal) being en-queued, de-queue it*/
if (isTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGVIEWING_NORMAL) != -1){
removeTaskInQueue (iIndex,
DelayableTask.TASKKIND_LOADINGVIEWING_NORMAL);
};
}
public String PPL_getDetailsText (PhotoPreviewList ppl, int iIndex){
String fileNameStr =
((String[]) fileNamesNURLs.elementAt (iIndex))[0];
if (fileNameStr == null) return "";
else return fileNameStr;
}
Note that for the callbacks to load the preview and full-mode images, we first en-queue a task to load the blurred version, and then followed by the normal/clear version, as we have designed earlier. Pretty straightforward, isn’t it?
Now we need to code the method that actually executes the tasks that are queued, which is to be executed by the task executor thread every time it wakes up and finds a pending task.
/*Some pre-calculated computation variables*/
private static final int THUMB_NONSQUARE_LOWERDIMENSION =
(int) (BOUNDINGSIZE_THUMB / PhotoPreviewList.PHOTO_SIZERATIO);
private static final int PREVIEW_NONSQUARE_LOWERDIMENSION =
(int) (BOUNDINGSIZE_PREVIEW / PhotoPreviewList.PHOTO_SIZERATIO); ... private void executeTask (DelayableTask task){
/*Declaration of temporary variables*/
Image origImg = null;
int iRequiredWidth;
int iRequiredHeight;
switch (task.iTaskKind){
case DelayableTask.TASKKIND_LOADINGTHUMB:
origImg = loadImage (task.iImgIndex);
if (origImg != null){
iRequiredWidth = origImg.getWidth() < origImg.getHeight()
? THUMB_NONSQUARE_LOWERDIMENSION
: BOUNDINGSIZE_THUMB;
iRequiredHeight = origImg.getHeight() < origImg.getWidth()
? THUMB_NONSQUARE_LOWERDIMENSION
: BOUNDINGSIZE_THUMB;
/*Generate the resized version*/
tffxGeom.reinit (origImg);
tffxGeom.configure (((float) iRequiredWidth)/origImg.getWidth(),
((float) iRequiredHeight)/origImg.getHeight(),
0.0f,ImageUtil.INPOL_LINEAR);
tffxGeom.doTransform();
Image readyImg = tffxGeom.getTransfImage (false);
/*Free the original image as no longer used*/
origImg = null; System.gc();
/*Reflect the update on the PhotoPreviewList*/
ppl.updateThumbImage (task.iImgIndex,readyImg);
/*Maintain the ready thumb in the list*/
thumbImgSoFarList.setElementAt (readyImg,task.iImgIndex); };
break;
case DelayableTask.TASKKIND_LOADINGPREVIEW_BLURRED:
origImg = (Image) thumbImgSoFarList.elementAt (task.iImgIndex);
if (origImg != null){
iRequiredWidth = origImg.getWidth() < origImg.getHeight()
? PREVIEW_NONSQUARE_LOWERDIMENSION
: BOUNDINGSIZE_PREVIEW;
iRequiredHeight = origImg.getHeight() < origImg.getWidth()
? PREVIEW_NONSQUARE_LOWERDIMENSION
: BOUNDINGSIZE_PREVIEW;
/*Generate the resized version*/
tffxGeom.reinit (origImg);
tffxGeom.configure (((float) iRequiredWidth)/origImg.getWidth(),
((float) iRequiredHeight)/origImg.getHeight(),
0.0f,ImageUtil.INPOL_NEAREST);
tffxGeom.doTransform();
Image readyImg = tffxGeom.getTransfImage (false);
/*Free the original image as no longer used*/
origImg = null; System.gc();
/*Reflect the update on the PhotoPreviewList*/
ppl.updatePreviewImage (readyImg,task.iImgIndex);
};
break;
case DelayableTask.TASKKIND_LOADINGPREVIEW_NORMAL:
origImg = loadImage (task.iImgIndex);
if (origImg != null){
iRequiredWidth = origImg.getWidth() < origImg.getHeight()
? PREVIEW_NONSQUARE_LOWERDIMENSION
: BOUNDINGSIZE_PREVIEW;
iRequiredHeight = origImg.getHeight() < origImg.getWidth()
? PREVIEW_NONSQUARE_LOWERDIMENSION
: BOUNDINGSIZE_PREVIEW;
/*Generate the resized version*/
tffxGeom.reinit (origImg);
tffxGeom.configure (((float) iRequiredWidth)/origImg.getWidth(),
((float) iRequiredHeight)/origImg.getHeight(),
0.0f,ImageUtil.INPOL_LINEAR);
tffxGeom.doTransform();
Image readyImg = tffxGeom.getTransfImage (false);
/*Free the original image as no longer used*/
origImg = null; System.gc();
/*Reflect the update on the PhotoPreviewList*/
ppl.updatePreviewImage (readyImg,task.iImgIndex);
lastPreviewImg = readyImg;
};
break;
case DelayableTask.TASKKIND_LOADINGVIEWING_BLURRED:
origImg = lastPreviewImg;
if (origImg != null){
iRequiredWidth = ppl.getWidth();
iRequiredHeight = ppl.getHeight();
/*Generate the pixelated (and rotated, if any) version*/
tffxGeom.reinit (origImg);
if (origImg.getWidth() < origImg.getHeight()){ //portrait
tffxGeom.configure (((float) iRequiredWidth)/origImg.getWidth(),
((float) iRequiredHeight)/origImg.getHeight(),
0.0f,ImageUtil.INPOL_NEAREST);
} else {
tffxGeom.configure (((float) iRequiredWidth)/origImg.getHeight(),
((float) iRequiredHeight)/origImg.getWidth(),
90.0f,ImageUtil.INPOL_NEAREST);
};
tffxGeom.doTransform();
Image readyImg = tffxGeom.getTransfImage (false);
/*Free the original image as no longer used*/
origImg = null; System.gc();
/*Reflect the update on the PhotoPreviewList*/
ppl.updateViewingImage (readyImg,task.iImgIndex);
};
break;
case DelayableTask.TASKKIND_LOADINGVIEWING_NORMAL:
origImg = loadImage (task.iImgIndex);
if (origImg != null){
iRequiredWidth = ppl.getWidth();
iRequiredHeight = ppl.getHeight();
/*Generate the resized (and rotated, if any) version*/
tffxGeom.reinit (origImg);
if (origImg.getWidth() < origImg.getHeight()){ //portrait
tffxGeom.configure (((float) iRequiredWidth)/origImg.getWidth(),
((float) iRequiredHeight)/origImg.getHeight(),
0.0f,ImageUtil.INPOL_LINEAR);
} else{
tffxGeom.configure (((float) iRequiredWidth)/origImg.getHeight(),
((float) iRequiredHeight)/origImg.getWidth(),
90.0f,ImageUtil.INPOL_LINEAR);
};
tffxGeom.doTransform();
Image readyImg = tffxGeom.getTransfImage (false);
/*Free the original image as no longer used*/
origImg = null; System.gc();
/*Reflect the update on the PhotoPreviewList*/
ppl.updateViewingImage (readyImg,task.iImgIndex); };
break;
};
if (taskQueueList.size() > 0){ //condition check in case it’s been
//removed while was being executed
taskQueueList.removeElementAt (0); //de-queue
};
}
We also need to implement the helper method that loads the photo image given its index number, as follows:
private Image loadImage (int iIndex){
if (iIndex < 0 || iIndex > fileNamesNURLs.size()-1) return null;
try{
String fileURLStr =
((String[]) fileNamesNURLs.elementAt (iIndex))[1];
return (ImageUtil.loadImageFromFile (fileURLStr));
} catch (Exception ex){
ex.printStackTrace();
return null;
}
}
Note in the code for executeTask for how we use TFFXGeometry for resizing (and rotating when necessary) the photo images. TFFXGeometry is TWUIK’s transformation FX engine for rescaling and/or rotating an image, with the rescaling algorithm can be one of two types: nearest-neighbor interpolation method () or bi-linear interpolation method (ImageUtil.INPOL_LINEAR). In the code above, when we are resizing to the normal/clear version, we use the bi-linear interpolation method for the best quality output, while on the other hand, when we are resizing to the pixelated version, we use the nearest-neighbor interpolation method since we don’t care much about its quality.
Generally, using TFFXGeometry involves the following basic steps:
- Constructs a new TFFXGeometry object if not yet.
- Re-initializes it with the desired source (before-transform) Image object by calling the reinit method.
- Configures the parameters appropriately as desired by calling the configure method. The API signature for this method is:
public void configure (float scaleX, float scaleY,
scaleX is the horizontal scaling factor while scaleY is the vertical scaling factor (must be > 0). rotateDegree is the degree of rotation with the zero degree being defined at the 3 o’clock and increases in clock-wise manner. The last parameter is the algorithm (i.e., the interpolation method) for rescaling, which can be either one of following two values: ImageUtil.INPOL_NEAREST or ImageUtil.INPOL_LINEAR.
float rotateDegree, int iInterpoMethod) - Performs the transformation by calling the doTransform method.
- Finally, retrieves the output/resulting image after the transformation by calling the getTransfImage method passing either a true or false boolean value. If true, then if the output image is not ready yet, previous transformation output image (if any) will be returned instead. If false, null will be returned if the output image is not ready yet.
Back to the code for the executeTask method, note that after the completion of any task, we calls one of the PhotoPreviewList‘s update-notification methods that we have discussed earlier accordingly. For example, for the TASKKIND_LOADINGTHUMB task, after its completion, we make a call to updateThumbImage passing in the ready thumbnail image with its index. Likewise, for the TASKKIND_LOADINGPREVIEW_BLURRED task, after its completion, we make a call to updateThumbImage passing in the ready preview (blurred) image with its index. This is all necessary because remember that when those images are requested via the PhotoPreviewList’s callbacks, we did not return them immediately. If we do not do this, no image will be displayed in the PhotoPreviewList widget at all even after we have executed all the loading tasks. This is how we connect our thread-based loading mechanism to the PhotoPreviewList widget.
Making a Slide Show
Now all is almost done for the Photo Gallery screen. Everything is now up, we can browse the gallery and we can also view the selected photo in full-mode if we want to. One last feature that we need to add is the ability to play a slide show for all the photos in the gallery, using some transition effects in between them, ideally.
The best way to make this happen is by using the built-in ImageBox widget that we display to occupy the full-screen. We have used this widget before in tutorial part 1, but not yet up to its full features. One particularly useful feature of ImageBox is that it provides a built-in slide show support. You can pass in a list of images you have, and then configure the transition FX engine to use, and then call a method to start the slide show, and that’s all!
However, we do not want to do it this way, unfortunately. We can’t just pass in a list of all the images to the ImageBox. That would mean we need to load all the images in our gallery, which would soon use up all the available memory in the device and then we get an OutOfMemory error.
A smarter way would instead load only two images at any time. We perform the transition FX from the first to the second image. And then we unload the first one, and load the next image. The previously second image now become the first image and the newly loaded one becomes the second one. We then perform the transition FX, and so on. With this design, we then implement the slide show code as follow:
/*Components*/
...
private ImageBox ibSlideShow;
/*Working Variables*/
...
private TFFXGeometry tffxGeom2;
private boolean bSlideShowStarted;
private Thread slideShowThread; public PhotoGallery (MMCMIDlet midlet){
...
/*Initialize components*/
...
bSlideShow = new ImageBox (1,false,
0,0,
canvas.getWidth(),canvas.getHeight(),
null,null,null,0);
ibSlideShow.setBgColor (0xFFFFFF);
/*Initialize working variables*/
...
tffxGeom2 = new TFFXGeometry();
} ... private void startSlideShow(){
/*Resets the control variables*/
bSlideShowStarted = true;
bMenuCanBeInteracted = false;
/*Prepares the ImageBox*/
ibSlideShow.stopAnyTransition();
ibSlideShow.setImages (null);
ibSlideShow.setVisibleMode (true);
slideShowThread = new Thread(){
public void run(){
try{
/*Initialize temporary variables*/
Image[] tmpImgs = new Image[2];
boolean bReuseSecond = false;
int iCurFirstImgIndex = ppl.getSelectedIndex();
Image curFirstImg = null;
Image curSecondImg = null;
int iCurTSFXKindIndex = 0;
TransitionFX tsfx = null;
/*Loop through the list of photos, show each for some period,
and then performs a transition FX to the next one, and then
repeat the same thing indefinitely until requested to stop*/
while (bSlideShowStarted){
/*Load the required current first and second images*/
if (!bReuseSecond){
curFirstImg = loadImage (iCurFirstImgIndex);
bReuseSecond = true;
} else{
curFirstImg = curSecondImg;
System.gc();
};
curSecondImg =
loadImage ((iCurFirstImgIndex+1)%fileNamesNURLs.size());
/*Resize (and rotate if necessary) the first image if size
not equal to screen size*/
if (curFirstImg.getWidth() != canvas.getWidth()
|| curFirstImg.getHeight() != canvas.getHeight()){
tffxGeom2.reinit (curFirstImg);
if (curFirstImg.getWidth()
< curFirstImg.getHeight()){ //portrait
tffxGeom2.configure (((float) canvas.getWidth())
/ curFirstImg.getWidth(),
((float) canvas.getHeight())
/ curFirstImg.getHeight(),
0.0f,ImageUtil.INPOL_NEAREST);
} else{
tffxGeom2.configure (((float) canvas.getWidth())
/ curFirstImg.getHeight(),
((float) canvas.getHeight())
/ curFirstImg.getWidth(),
90.0f,ImageUtil.INPOL_NEAREST);
};
tffxGeom2.doTransform();
curFirstImg = tffxGeom2.getTransfImage (false);
}; /*Resize (and rotate if necessary) the second image if
size not equal to screen size*/
if (curSecondImg.getWidth() != canvas.getWidth()
|| curSecondImg.getHeight() != canvas.getHeight()){
tffxGeom2.reinit (curSecondImg);
if (curSecondImg.getWidth()
< curSecondImg.getHeight()){ //portrait
tffxGeom2.configure (((float) canvas.getWidth())
/ curSecondImg.getWidth(),
((float) canvas.getHeight())
/ curSecondImg.getHeight(),
0.0f,ImageUtil.INPOL_NEAREST);
} else{
tffxGeom2.configure (((float) canvas.getWidth())
/ curSecondImg.getHeight(),
((float) canvas.getHeight())
/ curSecondImg.getWidth(),
90.0f,ImageUtil.INPOL_NEAREST);
};
tffxGeom2.doTransform();
curSecondImg = tffxGeom2.getTransfImage (false);
};
/*Prepare the images for the ImageBox*/
tmpImgs[0] = curFirstImg;
tmpImgs[1] = curSecondImg;
ibSlideShow.setImages (tmpImgs);
/*Prepare and start the transition FX for the ImageBox*/
switch (iCurTSFXKindIndex){
case 0: tsfx = new TSFXSliding (1,true,10,-1); break;
case 1: tsfx = new TSFXFadingInOut (1,10); break;
case 2: tsfx = new TSFXDissolve (1,20,15); break;
case 3: tsfx = new TSFXWiping (1,true,10,-1); break;
case 4: tsfx = new TSFXRandomFall (1,20); break;
case 5: tsfx = new TSFXFadeThrough (1,10,0xFFFFFF);break;
case 6: tsfx = new TSFXBreakOut (2,20); break;
};
ibSlideShow.configureTransitionFXs (tsfx,null,1000);
ibSlideShow.goTo (1);
/*Wait until next image is shown or slide show stopped*/
while (bSlideShowStarted
&& (ibSlideShow.getCurrentImageIndex() != 1
|| ibSlideShow.isCurrentlyPerformingTSFX())){
Thread.yield();
};
/*Advance to next two images (with next transition FX)*/
iCurFirstImgIndex =
(iCurFirstImgIndex + 1) % fileNamesNURLs.size();
iCurTSFXKindIndex = (iCurTSFXKindIndex + 1) % 7;
};
} finally {
slideShowThread = null;
};
}
};
/*Starts the thread with maximum priority*/
slideShowThread.setPriority (Thread.MAX_PRIORITY);
slideShowThread.start(); }
Note from the code above that there are several API methods for ImageBox that we have never used previously. These are:
public final void setImages (Image[] imgs)
public final void stopAnyTransition()
public final void configureTransitionFXs (TransitionFX forwardTSFX,
TransitionFX backwardTSFX,
long lTransitionDelay)
public final void goTo (int iImgIndex)
public final int getCurrentImageIndex()
public final boolean isCurrentlyPerformingTSFX()
All the API methods above should be self-explanatory in their names. Of necessary mention would be the configureTransitionFXs and goto methods. The former one sets up the ImageBox for which TransitionFX engine to use for forward transition and for backward transition as well as for how long to display an image before performing the required transition effects. The latter method performs the transition from the current image to the image of the specified index in the given list.
Now, note that for the transition effects purpose, we try out all the built-in transition FX engines in TWUIK one-by-one in sequence. As we can see, using a transition FX engine for ImageBox is as easy as constructing the corresponding TSFX object, and then passes it to the ImageBox. Nevertheless, following are the constructor signatures of all the built-in transition FX engines:
TSFXSliding (int iTransitionTickCount, boolean bHorizontal,
int iStepCount, int iDir) TSFXFadingInOut (int iTransitionTickCount, int iStepCount) TSFXDissolve (int iTransitionTickCount, int iBlockSize,
int iDissolveStepCount) TSFXWiping (int iTransitionTickCount, boolean bHorizontal,
int iStepCount, int iDir) TSFXRandomFall (int iTransitionTickCount, int iBlockSize) TSFXFadeThrough (int iTransitionTickCount, int iStepCount,
int iThroughColor)
We are almost there! Now we just need to implement the code that accepts the user input to start/stop the slide show. Our design is that the slide show is started when the user fires the Play Slide Show button of the FishEyeMenu. And to switch back from the slide show screen, the user pressed the FIRE key. Thus we have the following implementation:
public void menuButtonFired (int iButtonID){
switch (iButtonID){
...
case ButtonFactory.ID_SLIDE:
//only when not started yet and previous thread has finished
if (!bSlideShowStarted && slideShowThread == null){
startSlideShow();
};
break;
...
};
}
public void keyPressed (int iKeyCode){
if (bSlideShowStarted){
if (canvas.getGameActionNoAlt (iKeyCode) == Canvas.FIRE){
bSlideShowStarted = false;
ibSlideShow.setVisibleMode (false);
bMenuCanBeInteracted = true;
};
return;
};
...
}
Finally, our Photo Gallery screen is all up working and provides all the features that we have designed it to. Look at the Flash demo below showing the screen with all its features.
There you go with the details of the implementation of the Photo Gallery screen. You should have learnt quite a lot about TWUIK basics from this second part of the tutorial. Next we will see the implementation of the Audio Player screen, so see you soon!
For enquiry about TWUIK, please email us at (sales@tricastmedia.com)
