GUI design - Contact List, Part 2
Use of CustomItem
This entry is continuation of Designing Address Book - Contact List, Part 1
First task is to come up with user interface design. As the template of what I expect to have I previously mentioned I will use component layout from Sony Ericsson W910. Traditionally once user is inside main menu, set of 3 x 4 icons, and select Contacts the screen view will presents list of all contacts plus on top of it there is "New contact" option (on real device there is also "Myself" to create personal business card, but for the moment we can ignore that).
![]() |
![]() |
![]() |
![]() |
I will put "New contact" in starting screen accompanied by "List All" option so the user can either choose if he wants to add new contact or first want to see existing contacts.
Traditional design with Form class
![]() |
![]() |
| Screen shot of emulator from Sony Ericsson web site. | Design with use of Form class only. |
With my first attempt I went for traditional design through Form class and use of StringItems that naturally came to mind. This provided me with general skeleton from which I was hopping to expand. Unfortunately there is no way to add image to StringItem and attempts to combine it with ImageItem did not bring expected fruits. Each of these components is added to new row and I did not find a way to absolutely position them.
import java.util.Vector; import javax.microedition.lcdui.Alert; import javax.microedition.lcdui.AlertType; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Form; import javax.microedition.lcdui.Item; import javax.microedition.lcdui.ItemCommandListener; import javax.microedition.lcdui.StringItem; import organiser.forms.subforms.AddContactName; import organiser.forms.subforms.AddContactNumber; import organiser.forms.beans.Contact; import organiser.forms.beans.Phone; import organiser.forms.logic.RecordStoreOperations; public class NewContact extends Form implements CommandListener { private Contact contact; private Command backCommand; private Command selectCommand = new Command("Select", Command.ITEM, 1); public NewContact() { super("New Contact"); contact = new Contact(); //Name entry StringItem nameSI = new NameStringItem(); append(nameSI); //Number entry StringItem numberSI = new NumberStringItem(contact); append(numberSI); addCommands(); } public NewContact(Contact con) { super("New Contact"); this.contact = con; //Name entry StringItem nameSI; if (contact.getName() == null) { nameSI = new NameStringItem(); } else { nameSI = new NameStringItem(contact.getName()); } int width = getWidth(); int height = nameSI.getFont().getHeight(); nameSI.setPreferredSize(width - 4, height + 2); nameSI.setDefaultCommand(selectCommand); nameSI.setItemCommandListener(new ItemCommandListener() { public void commandAction(Command c, Item i) { AddContactName acn = new AddContactName(contact); Display.getDisplay(organiser.ElectronicPersonalOrganiser.instance).setCurrent(acn); } }); append(nameSI); //Display numbers if any Vector phoneDetails = contact.getPhoneNumbers(); if (phoneDetails.size() > 0) { for (int i = 0; i < phoneDetails.size(); i++) { Phone p = (Phone) phoneDetails.elementAt(i); if (p.getPhoneType().equals("Home")) { final StringItem homeSI = new StringItem(null, (p.getPhoneType() + ":\n" + p.getPhoneNum())); homeSI.setPreferredSize(width - 4, height + 2); append(homeSI); } if (p.getPhoneType().equals("Mobile")) { final StringItem mobileSI = new StringItem(null, (p.getPhoneType() + ":\n" + p.getPhoneNum())); mobileSI.setPreferredSize(width - 4, height + 2); append(mobileSI); } if (p.getPhoneType().equals("Office")) { final StringItem officeSI = new StringItem(null, (p.getPhoneType() + ":\n" + p.getPhoneNum())); officeSI.setPreferredSize(width - 4, height + 2); append(officeSI); } if (p.getPhoneType().equals("Other")) { final StringItem otherSI = new StringItem(null, (p.getPhoneType() + ":\n" + p.getPhoneNum())); otherSI.setPreferredSize(width - 4, height + 2); append(otherSI); } } } //Show add new number if vector size less then 4 if (phoneDetails.size() < 4) { StringItem numberSI = new NumberStringItem(contact); append(numberSI); } addCommands(); } class NumberStringItem extends StringItem { public NumberStringItem(final Contact contact) { super(null, "New Number:"); int width = getWidth(); int height = getFont().getHeight(); setPreferredSize(width - 4, height + 2); setDefaultCommand(selectCommand); setItemCommandListener(new ItemCommandListener() { public void commandAction(Command c, Item i) { AddContactNumber acn = new AddContactNumber(contact); Display.getDisplay(organiser.ElectronicPersonalOrganiser.instance).setCurrent(acn); } }); } } class NameStringItem extends StringItem { public NameStringItem() { super(null, "Name:", Item.PLAIN); initNameStringItem(); } public NameStringItem(String str) { super(null, ("Name:\n" + str), Item.PLAIN); initNameStringItem(); } public void initNameStringItem() { setDefaultCommand(selectCommand); setItemCommandListener(new ItemCommandListener() { public void commandAction(Command c, Item i) { AddContactName acn = new AddContactName(new Contact()); Display.getDisplay(organiser.ElectronicPersonalOrganiser.instance).setCurrent(acn); } }); } } private void addCommands() { backCommand = new Command("Back", Command.BACK, 2); addCommand(backCommand); setCommandListener(this); Command saveCommand = new Command("Save", Command.SCREEN, 1); addCommand(saveCommand); setCommandListener(this); } public void commandAction(Command c, Displayable d) { if (c == backCommand) { goBack(); } else { if (contact.getName() != null) { //Save contact RecordStoreOperations rso = new RecordStoreOperations(); rso.saveContact(contact); goBack(); } else { Alert alert = new Alert("Error", "New contact must have name.", null, AlertType.ERROR); alert.setTimeout(Alert.FOREVER); Display.getDisplay(organiser.ElectronicPersonalOrganiser.instance).setCurrent(alert); } } } private void goBack() { MainMenu mMenu = new MainMenu(); Display.getDisplay(organiser.ElectronicPersonalOrganiser.instance).setCurrent(mMenu); } }
Form class design with CustoItem
By studying subclasess available to Form class I found out I have no idea what CustomItem is for. API explanation was enough to get me investigate:
A CustomItem is customizable by subclassing to introduce new visual and interactive elements into Forms. Subclasses are responsible for their visual appearance including sizing and rendering and choice of colors, fonts and graphics. Subclasses are responsible for the user interaction mode by responding to events generated by keys, pointer actions, and traversal actions. Finally, subclasses are responsible for calling Item.notifyStateChanged() to trigger notification of listeners that the CustomItem's value has changed.
![]() |
![]() |
| Design with use of Form class only. | Design with CustomItem as one of the components of the Form class. |
Here you can see how I used CustomItem to my needs (first attempt that been latter improved)
import java.util.Vector; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.CustomItem; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Font; import organiser.beans.Contact; import organiser.beans.NewContactLabels; import organiser.tools.ImageLoader; /** * Created by IntelliJ IDEA. * User: Peter Miklosko * URL: http://www.peterscorner.co.uk * Date: 18-Mar-2009 * Time: 11:50:30 */ public class CustomNewContact extends CustomItem { private Display display; private Contact contact; private Font fontTitle, fontContent; private int selectedItem, maxItems, hContainer; private Vector contactLabels; private final int X_POS = 2; private final int ANCHOR = Graphics.TOP | Graphics.LEFT; private boolean paintDone = false; public CustomNewContact(Display d, Contact c) { super(null); display = d; contact = c; setVariables(); //Paint the menu repaint(); } public void setVariables() { //Initiating fonts fontTitle = Font.getFont(Font.FONT_STATIC_TEXT); fontContent = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_ITALIC, Font.SIZE_SMALL); //Height of one container represent expected of one label hContainer = fontTitle.getHeight() + fontContent.getHeight() + 3; //First selection selectedItem = 0; //Get NewContactLabels to display contactLabels = contact.contactToVector(); maxItems = contactLabels.size(); } public int getMinContentWidth() { return display.getCurrent().getWidth(); } public int getMinContentHeight() { return display.getCurrent().getHeight(); } public int getPrefContentWidth(int width) { return getMinContentWidth(); } public int getPrefContentHeight(int height) { return getMinContentHeight(); } public void paint(Graphics g, int w, int h) { //Position where to start paint from top int y = 2; ImageLoader il = new ImageLoader(); //Clear screen of previus content g.setColor(display.getColor(Display.COLOR_BACKGROUND)); g.fillRect(0, 0, getMinContentWidth(), getMinContentHeight()); NewContactLabels labels; for (int i = 0; i < contactLabels.size(); i++) { if (selectedItem == i) { g.setColor(display.getColor(Display.COLOR_HIGHLIGHTED_BACKGROUND)); g.fillRect(0, y - 1, getMinContentWidth(), hContainer); g.setColor(display.getColor(Display.COLOR_HIGHLIGHTED_FOREGROUND)); } else { g.setColor(display.getColor(Display.COLOR_FOREGROUND)); } labels = (NewContactLabels) contactLabels.elementAt(i); g.setFont(fontTitle); g.drawString(labels.getLabelName(), X_POS, y, ANCHOR); y += fontTitle.getHeight() + 1; if (labels.getLabelName().equals("Home") || labels.getLabelName().equals("Mobile") || labels.getLabelName().equals("Office") || labels.getLabelName().equals("Other")) { g.setFont(fontContent); g.drawImage(il.loadImage(labels.getLabelName().toLowerCase() + ".png"), 2, y, ANCHOR); g.drawString(labels.getLabelValue(), 18, y, ANCHOR); y += fontContent.getHeight() + 2; } else{ g.setFont(fontContent); g.drawString(labels.getLabelValue(), X_POS, y, ANCHOR); y += fontContent.getHeight() + 2; } } if (!paintDone) { paintDone = true; } } /** * Called by the system when traversal has entered the item or has occured within the item * * @param dir the direction of traversal, one of Canvas.UP, Canvas.DOWN, Canvas.LEFT, Canvas.RIGHT, or NONE. * @param viewportWidth the width of the container's viewport * @param viewportHeight the height of the container's viewport * @param visRect_inout passes the visible rectangle into the method, and returns the updated traversal rectangle from the method * @return always true as internal traversal had occurred */ protected boolean traverse(int dir, int viewportWidth, int viewportHeight, int[] visRect_inout) { if (paintDone) { if (dir == Canvas.UP) { setSelectedItem(-1); repaint(); this.notifyStateChanged(); } if (dir == Canvas.DOWN) { setSelectedItem(1); repaint(); this.notifyStateChanged(); } } return true; } private void setSelectedItem(int i) { selectedItem += i; if (selectedItem < 0 || selectedItem == maxItems) { if (selectedItem < 0) { selectedItem = maxItems - 1; } if (selectedItem == maxItems) { selectedItem = 0; } } } public String getSelectedItem() { NewContactLabels labels = (NewContactLabels) contactLabels.elementAt(selectedItem); return labels.getLabelName(); } }
The CustomItem class has 5 abstract methods (getMinContentHeight(), getMinContentWidth(), getPrefContentHeight(int width), getPrefContentWidth(int height) and paint(Graphics g, int w, int h) ) that must be used, but not necessary fully implemented. The most important of them is of course paint() where all components are put down to canvas. My code may look little complex or rather confusing, but after closer examination you will see it is not so. However I believe that if I was willing to spend extra time on it I could make it more efficient and easier to comprehend.Screen for new contact shows number of items such as add name, add number (which will be hidden if all 4 numbers already provided) and zero or more phone numbers with their description (max 4). So at any time there will at least 2 (options "Name" and "New number") or more items displayed, where maximum number of items can only be 5 ("Name" + "New Number" + 3 numbers OR "Name" + 4 numbers). Problem is that not all items may be visible because of screen size difference between various mobile devices. (Detection of maximum items per screen is not discussed here, final source code provides answer for this.) All mentioned items are handled/stored in Contact bean. For easier data manipulation/drawing, data are transferred from Contact bean into simple Vector of Objects, where an Object is represented by two String arguments. This simple conversion let me know how many items will be displayed, if the "New number" option is required; and provide an easy way to iterate through these data for displaying correct set. For loop first checks if data to by displayed are these of current selected item. If this is true highlighted selection area will be created. Logical outcome of this detection will also affect colour that will be used to display item in current work position "i" of for loop. Next step is drawing of first String argument from the Vector. However for correct drawing of second String argument, first argument is checked if it is {Home, Mobile, Office, Other} and if found to be item representing phone number second String argument is pre-fixed with appropriate phone icon.
And here is how I add my newly coded CustomItem to the Form
customNewContact = new CustomNewContact(Display.getDisplay(organiser.ElectronicPersonalOrganiser.instance), contact); selectedMenuCommand = getItemCommand(customNewContact.getSelectedItem()); customNewContact.setDefaultCommand(selectedMenuCommand); this.setItemStateListener(this); customNewContact.setItemCommandListener(new ItemCommandListener(){ public void commandAction(Command c, Item i){ //decide where to go goTo(customNewContact.getSelectedItem()); } }); append(customNewContact);
The above is not final solution as there been few changes once I wrote CustomItem for displaying list of Contacts. Both of the CustomItems are controlled by CustomItemControler that take care of maintenance for maximum items per screen, start and end of current displayed set, iteration trough items and identification of currently selected item.Next stop, record sorting in part 3 of this mini exercise.








