Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Global Developer

Hands-On Google Web Toolkit


Designing the User Interface

The sample application's UI consists of two widgets (UI elements) and a main controller. The first widget, AlbumWidget, lets users select an image service, input their username, and select a photo album; see Figures 1 and 2. The second widget, ImageFlowWidget, displays the photos and provides navigation through the album. The main program (GWTFlow.java) controls the two widgets and maintains application state. All of the widgets are dynamically added and removed from the placeholder <div> element we put in the host HTML page.

AlbumWidget is constructed using a number of simpler GWT-provided widgets, such as Button, Image, Label, RadioButton, and TextBox. These are all arranged on VerticalPanels and HorizontalPanels. (For documentation on these and more, see the references at the end of this article.)

The advantage of using the widgets and panels provided by the toolkit is that they work the same way on all browsers. A prime example is the scrollable selector for choosing the photo in Figure 2. This would be difficult to implement in traditional HTML; but in GWT, it's a simple Grid table contained within a ScrollPanel. That's not to say all cross-browser problems go away—we experienced a few quirks with Internet Explorer and Safari—but they are significantly fewer when compared to traditional Ajax development.

[Click image to view at full size]

Figure 1: Users can select a photo service and username.

[Click image to view at full size]

Figure 2: List of albums is retrieved from the server and displayed in a scrollable grid.

ImageFlowWidget extends AbsolutePanel, which allows for absolute positioning and overlapping of widgets. Image widgets are placed in this panel and scaled to simulate perspective. The main image is always the largest and is positioned in the center of the window. Other pictures are stacked underneath and sized smaller and smaller the further they are from the center.

When browsers display absolute positioned elements, they overlap elements that were added later on top of earlier ones. So we need to add images from the outside in (using AbsolutePanel.add()), with the main image last. Image positions, however, are calculated from the inside out to prevent smaller images from getting completely hidden. Listing Three presents the algorithm for handling this. Starting from the center image, it first calculates the size and position, recurses to the neighboring image, calls AbsolutePanel.add(), and finally returns to the caller.


private void calcImagePosition(int imageIdx, int[][] whlt, int offsetWidth,
     int offsetHeight, boolean addToPanel, boolean animateAdd)
 {
   if (imageIdx < 0 || imageIdx >= images.length) {
     return; // Stopping recursion once we reach the first or last image
   }
   // Calculating image's width/height based on its distance from center.
   int distFromCenter = Math.abs(selIdx - imageIdx);
   int maxWidth = (int)((offsetWidth*.6)/(distFromCenter+1));
   int maxHeight = (int)((offsetHeight*.6)/(distFromCenter+1));
   int[] wh = scaleImage(dtos[imageIdx].width, dtos [imageIdx].height,
       maxWidth, maxHeight);
   int width = wh[0];
   int height = wh[1];

   // Calculating image's top position so it's always centered on viewport
   int top = (offsetHeight / 2) - (height / 2);

   // Calculating image's left position so 45% of its width pokes out
   int left;
   if (imageIdx < selIdx) {
     int xDiff = (int)(width*.45);
     left = whlt[imageIdx+1][LEFT] - xDiff;
   } else if (imageIdx > selIdx) {
     int xDiff = (int)(width*.55);
     left = whlt[imageIdx-1][LEFT] + whlt[imageIdx-1][WIDTH] - xDiff;
   } else { // centering middle image
     left = (getOffsetWidth() / 2) - (width / 2);
   }

   whlt[imageIdx] = new int[] { width, height, left, top };

   // Recursive calls to calc positions for images on the left and/or right
   if (imageIdx <= selIdx) {
     calcImagePosition(imageIdx-1, whlt, offsetWidth, offsetHeight,
         addToPanel, animateAdd);
   }
   if (imageIdx >= selIdx) {
     calcImagePosition(imageIdx+1, whlt, offsetWidth, offsetHeight,
         addToPanel, animateAdd);
   }

   // Adding images to the panel.  This happens on the initial load and on
   // window resize events.  If the user is just clicking between images,
   // they are already on the panel so we don't need to add them.
   if (addToPanel) {
     images[imageIdx].setWidth(width + "px");
     images[imageIdx].setHeight(height + "px");
     // The first time we load, we'll use the Scriptaculous grow effect
     // for any images that are in the viewport.
     if (animateAdd && inViewport(left, images[imageIdx].getWidth())) {
       images[imageIdx].setVisible(false);
       images[imageIdx].addLoadListener(new LoadListener() {
         public void onLoad(Widget sender) {
           Effect.grow(sender, new EffectOption[] {
               new EffectOption("duration", LOAD_DURATION)});
         }
         public void onError(Widget sender) {
           showErrorMessage();
         }
       });
     }
     // Last added are on top, so we add from the outside in.
     this.add(images[imageIdx], left, top);
   }
 }
Listing Three

Animation

GWT does not currently provide a built-in animation or effects library (although developers are working on one for an upcoming release). Instead, you need to call out to a JavaScript library such as Script.aculo.us (script.aculo.us), YUI (developer.yahoo.com/yui), or Rico (openrico.org). For GWTFlow, we chose the Effect class in the open-source GWT Widget Library. Using GWT's JavaScript Native Interface (JSNI), it wraps the Script.aculo.us effects library, providing animation for moving, scaling, and fade effects.

When users navigate to a new image, GWTFlow recalculates the new images and sizes, calls Effect.move() and Effect.scale() (provided by the GWT Widget Library), and lets Script.aculo.us work its magic. Listing Four presents the method called to animate the image movement.

private void animateMove(Widget widget, int left, int top, double dur) 
{
  Effect.move(widget, new EffectOption[] { new EffectOption("mode", "absolute"),
    new EffectOption("x", left), new EffectOption("y", top), 
    new EffectOption("duration", dur)} );
}

Listing Four

Getting Data From Flickr

When users type in a username and click the Next button, we need to retrieve the list of images and albums from the server. Flickr provides access to its data via an extensive web service API (www.flickr.com/services/api). We created a proxy service to provide this data using GWT's Remote Procedure Call (RPC) functionality (Figure 3).

[Click image to view at full size]

Figure 3: ImageService acts as a proxy between the client and Flickr.

RPC services in GWT extend from the Java Servlet API, with built-in callbacks defined to make everything asynchronous. The objects returned are serializable POJOs (Plain Old Java Objects), which we use to store image and album metadata.

The actual calls to Flickr are done in ImageService.java using Flickrj, an open-source Java library that wraps Flickr's REST-based API (flickrj.sourceforge.net). Listing Five shows the code to look up users, get a list of their photo sets, and transform the list into an array of lightweight Album data transfer objects.

public AlbumDTO[] getFlickrAlbums(String username) throws IOException,
    SAXException, FlickrException 
{
  Flickr flickr = FlickrUtil.getInstance(); // Access Flickr using API key
  PeopleInterface people = flickr.getPeopleInterface();
  User user = people.findByUsername(username);
  PhotosetsInterface photoSets = flickr.getPhotosetsInterface();
  Collection sets = photoSets.getList(user.getId()).getPhotosets();
  AlbumDTO[] albums = new AlbumDTO[sets.size()];
  Iterator iter = sets.iterator();
  for (int i = 0; i < albums.length; i++) {
    Photoset set = (Photoset) iter.next();
    albums[i] = new AlbumDTO(); // Custom POJO w/Album metadata
    albums[i].id = set.getId();
    albums[i].name = set.getTitle();
    albums[i].description = set.getDescription();
    albums[i].imageCount = set.getPhotoCount();
    albums[i].smallSquareUrl = set.getPrimaryPhoto().getSmallSquareUrl();
  }
  return albums;
}
Listing Five

As in any Ajax application, client-side responsiveness is a primary concern. An RPC service lets us minimize the data sent across the wire and use the server's processing power to retrieve image information. It also allows for a cleanly defined API that can be extended to support other image providers, such as Google's Picasa Web Albums.

However, RPC services typically require a servlet engine such as Tomcat or JBoss, adding complexity to deployment and limiting the number of hosting providers. One alternative we considered was JSON (JavaScript Object Notation), a data interchange format with support for native JavaScript web service requests. JSON data feeds are supported by a variety of providers, including Flickr and some Google services (although not Picasa). In the end, we decided against JSON, favoring RPC for performance, ease of debugging, and code maintainability.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.