Mysterious extra pixels in ButtonField? - blackberry

I have created a very basic custom layout to set out a menu made up of ButtonFields.
To position them vertically, i split the screen into 6 and then put them at divides 2,3,4 and 5 as is shown in the code.
So for the emulator, the Torch, a Height of 480 would see buttons at 160,240,320 and 400.
However, when I check them, they are all 24 pixels below this, and there seems no obvious reason unless this is just a typical convention I have missed!
package Test;
import net.rim.device.api.system.Display;
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.container.VerticalFieldManager;
class MenuLayoutManager extends VerticalFieldManager{
public MenuLayoutManager()
{
super();
}
public int getPreferredWidth()
{
int preferredWidth = Display.getWidth();
return preferredWidth;
}
protected void sublayout(int inMaxWidth, int inMaxHeight)
{
int xCentre = Display.getWidth()/2;
int yGap = Display.getHeight() / 6;
int xPos = 0;
int yPos = yGap * 2;
int fieldNo = this.getFieldCount();
for(int index = 0; index<fieldNo; index++)
{
Field aField = this.getField(index);
this.layoutChild(aField, inMaxWidth, inMaxHeight);
xPos = xCentre - (aField.getWidth() / 2);
this.setPositionChild(aField, xPos, yPos);
yPos += yGap;
}
this.setExtent(inMaxWidth, inMaxHeight);
}
}
------Entry point and button creation----
package Test;
import net.rim.device.api.ui.component.ButtonField;
import net.rim.device.api.ui.container.MainScreen;
class MyScreen extends MainScreen
{
public MyScreen(){
super(MainScreen.NO_VERTICAL_SCROLL|MainScreen.NO_VERTICAL_SCROLLBAR);
this.initialize();
}
private void initialize()
{
// Set the displayed title of the screen
this.setTitle(String.valueOf("Ben's Menu"));
MenuLayoutManager mlm = new MenuLayoutManager();
ButtonField Button1 = new ButtonField("New Game");
ButtonField Button2 = new ButtonField("High Scores");
ButtonField Button3 = new ButtonField("Instructions");
ButtonField Button4 = new ButtonField("Exit");
mlm.add(Button1);
mlm.add(Button2);
mlm.add(Button3);
mlm.add(Button4);
this.add(mlm);
}
}

With a MainScreen, you don't get the full display height allocated to the screen contents. (At a minimum, IIRC, there's the title and a separator.) Try using a FullScreen instead and see what happens.

Related

Blackberry - How to center the datepicker popup for DateField

I have DateField in center of my screen
And when I touch it I see:
But I want to see:
My question is:
How can I center the datepicker popup for DateField?
Blackberry OS 6
Update:
public class MyMainScreen extends MainScreen
{
protected DateField dateField_ = null;
protected VerticalFieldManager container_ = null;
public MyMainScreen()
{
super();
container_ = new VerticalFieldManager(VerticalFieldManager.VERTICAL_SCROLL
| VerticalFieldManager.VERTICAL_SCROLLBAR | VerticalFieldManager.USE_ALL_HEIGHT)
{
protected void sublayout(int width, int height)
{
super.sublayout(width, height);
layoutChild(dateField_ , 100, 40);
setPositionChild(dateField_ , (width - 100) / 2, 50);
setVirtualExtent(width, 50);
}
};
add(container_);
dateField_ = new DateField("", new Date(), new SimpleDateFormat("HH:mm"), Field.FIELD_HCENTER |
DrawStyle.HCENTER | Field.FOCUSABLE);
container_.add(dateField_);
add(container_);
}
}
Ok, so there's a few problems:
the DateField won't center the field just because you pass Field.FIELD_HCENTER | DrawStyle.HCENTER into the constructor. I don't think DateField even recognizes those flags.
A VerticalFieldManager is a special Manager that's designed to lay out its children vertically from top-to-bottom in the order you add() them. If you're not going to lay out the fields that way, then I don't know that you should extend VerticalFieldManager. Just extend Manager directly, if you want to implement sublayout(). Also, calling super.sublayout() in your implementation might cause issues.
You are hardcoding the height/width of your DateField. I'm not sure it allows that.
Your code didn't actually compile for me, as the DateField constructor takes a long date, not a Date. That doesn't have anything to do with centering though.
So, I'd recommend something like this:
super(MainScreen.VERTICAL_SCROLL | MainScreen.VERTICAL_SCROLLBAR);
dateField_ = new DateField("", (new Date()).getTime(), new SimpleDateFormat("HH:mm"), Field.FOCUSABLE);
container_ = new Manager(Manager.VERTICAL_SCROLL | Manager.VERTICAL_SCROLLBAR) {
public int getPreferredHeight() {
return Display.getHeight();
}
public int getPreferredWidth() {
return Display.getWidth();
}
protected void sublayout(int width, int height) {
int h = Math.min(height, Display.getHeight());
setExtent(width, h);
// only needed if there's actually more content than fits in the visible area
//setVirtualExtent(width, h + ?);
int dfWidth = dateField_.getPreferredWidth();
int dfHeight = dateField_.getPreferredHeight();
layoutChild(dateField_, dfWidth, dfHeight);
setPositionChild(dateField_, (width - dfWidth) / 2, (h - dfHeight) / 2);
}
};
container_.add(dateField_);
add(container_);
P.S. I wasn't sure if you wanted scrolling or not. With only a DateField, there's no reason to scroll. But, I know your real Screen probably has more content, so you may need to setVirtualExtent(), which I've commented out.
This solution works for me also:
package com.my.package;
import java.util.Calendar;
import java.util.Date;
import net.rim.device.api.i18n.SimpleDateFormat;
import net.rim.device.api.ui.DrawStyle;
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.Manager;
import net.rim.device.api.ui.TouchEvent;
import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.DateField;
import net.rim.device.api.ui.container.VerticalFieldManager;
import net.rim.device.api.ui.picker.DateTimePicker;
public class MyDateManager extends VerticalFieldManager {
protected DateField dateField_ = null;
protected DateTimePicker dateTimePicker_ = null;
protected SimpleDateFormat dateFormat_ = null;
public MyDateManager() {
super(Manager.FIELD_HCENTER | Manager.FIELD_VCENTER);
dateFormat_ = new SimpleDateFormat("HH:mm");
dateField_ = new DateField("", new Date().getTime(), dateFormat_,
Field.FIELD_HCENTER | DrawStyle.HCENTER | Field.FOCUSABLE) {
protected boolean touchEvent(TouchEvent paramTouchEvent) {
if (paramTouchEvent.getEvent() == TouchEvent.CLICK) {
dateField_.setEnabled(false);
showDatePicker();
}
return super.touchEvent(paramTouchEvent);
}
protected boolean trackwheelClick(int paramInt1, int paramInt2) {
dateField_.setEnabled(false);
showDatePicker();
return super.trackwheelClick(paramInt1, paramInt2);
}
};
this.add(dateField_);
}
protected void sublayout(int width, int height) {
layoutChild(dateField_, width, height);
setPositionChild(dateField_, (width - dateField_.getWidth()) / 2,
(height - dateField_.getHeight()) / 2);
int h = dateField_.getHeight();
if (h <= 0)
h = height;
setExtent(width, h);
}
protected void showDatePicker() {
if (dateTimePicker_ == null) {
Calendar cal = Calendar.getInstance();
// HACK
String dateFormat = dateFormat_.toPattern().indexOf("H", 0) == -1 ? dateFormat_
.toPattern() : null;
String timeFormat = dateFormat_.toPattern().indexOf("H", 0) != -1 ? dateFormat_
.toPattern() : null;
dateTimePicker_ = DateTimePicker.createInstance(cal, dateFormat,
timeFormat);
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
if (dateTimePicker_.doModal()) {
dateField_.setDate(dateTimePicker_.getDateTime()
.getTime());
}
dateField_.setEnabled(true);
dateField_.setFocus();
dateTimePicker_ = null;
}
});
}
}
}

labelField.getWidth() returns 0? Why does this happen?

I'm working on my own custom manager, and I've gotten it complete so far, but it setsMargins using a percentage of the screen resolution.
Here's how I call the following class:
LabelIconCommandManager licm3 = new LabelIconCommandManager("Address blah bklahblah ", 0);
licm3.add(new ImageButtonField(b1, b2, b3, Field.FIELD_LEFT | ImageButtonField.CONSUME_CLICK));
Here's the class [I've marked in a comment where it returns 0 and where it returns 219. please tell me why this happens:
public class LabelIconCommandManager extends HorizontalFieldManager implements BCMSField
{
LabelIconCommandManager me = this;
EvenlySpacedHorizontalFieldManager buttonManager = new EvenlySpacedHorizontalFieldManager(0);
LabelField labelField;
int side = 0;
int HPADDING = 3;
int VPADDING = 4;
int screenWidth = Display.getWidth();
int labelField_width = 40;
public LabelIconCommandManager()
{
this("", 0);
}
public LabelIconCommandManager(String label, long style)
{
super(USE_ALL_WIDTH| FOCUSABLE);
this.setBorder(BorderFactory.createBitmapBorder(new XYEdges(15, 20, 15, 20),Bitmap.getBitmapResource( "border_edit.png" )));
this.setMargin(1,10,1,10);
labelField = new LabelField(label,LabelField.ELLIPSIS)
{
public void layout(int width, int height)
{
// Done because otherwise ellipses dont work with labelfields
super.layout((int)(screenWidth * 0.61), getHeight());
setExtent((int)(screenWidth * 0.61), getHeight());
labelField_width = labelField.getWidth();
DisplayDialog.alert("labelField_width = " + labelField_width); // returns 219
}
};
// Top Right Bottom Left
labelField.setMargin(VPADDING, HPADDING, VPADDING, 0);
// super because we want this horizontalfieldManager to add it
super.add(labelField);
super.add(buttonManager);
}
public void alternateConstructor(Attributes atts)
{
labelField = new LabelField(atts.getValue("label"), 0);
}
public void onFocus(int direction)
{
this.setBorder(BorderFactory.createBitmapBorder(new XYEdges(15, 20, 15, 20),Bitmap.getBitmapResource( "border_edit_select.png" )));
// uses the same color as listStyleButtonField selections
this.setBackground(BackgroundFactory.createSolidBackground(0x186DEF));
super.onFocus(direction);
}
//Invoked when a field loses the focus.
public void onUnfocus()
{
//top, right,bottom,left
this.setBorder(BorderFactory.createBitmapBorder(new XYEdges(15, 20, 15, 20),Bitmap.getBitmapResource( "border_edit.png" )));
this.setBackground(BackgroundFactory.createSolidTransparentBackground(Color.GRAY, 0));
super.onUnfocus();
invalidate();
}
// Overrride this managers add function
public void add(Field imageButton)
{
// Add a button to the evenly spaced manager
buttonManager.add(imageButton);
// Based on how many buttons there are, set the margin of where the manager holding the buttons start [offset from labelField]
if(buttonManager.getFieldCount() == 1)
{
//side = (int)(screenWidth * 0.1388);
side = screenWidth - labelField_width - 32 - 10 - 15;
DisplayDialog.alert("Screen Width = " + screenWidth);
DisplayDialog.alert("labelField_width2 = " + labelField_width); // returns 0
DisplayDialog.alert("Side = " + side);
}
else side = (int)(screenWidth * 0.05);
buttonManager.setMargin(0,0,0,side);
}
public int getLabelWidth()
{
return labelField_width;
}
}
Here's a picture just to be more clear:
Note: when I ran your code, I didn't actually see labelField_width set to 0. You initialize the value to 40 in the code you posted above. So, I do sometimes see it set to 40, or 219 (on a 360 px wide screen).
But, the problem is that I think you're trying to access the value of labelField_width too soon. The only place it's properly assigned is in the layout() method of your anonymous LabelField. Just because you declare and implement the layout() method in line with the instantiation, doesn't mean that it's called when the LabelField is created. This is actually one of the reasons I don't like anonymous classes.
Anyway, this code:
LabelIconCommandManager licm3 = new LabelIconCommandManager("Address blah bklahblah ", 0);
licm3.add(new ImageButtonField(b1, b2, b3, Field.FIELD_LEFT | ImageButtonField.CONSUME_CLICK));
Will first instantiate the LabelField (inside the LabelIconCommandManager constructor). As I said, that does not trigger the layout() method. The second line above (add()) will trigger your overridden method:
// Overrride this managers add function
public void add(Field imageButton)
{
which is where you see the bad value for labelField_width. That method gets called before layout(). That's the problem.
Since it looks like you only use that width to set the buttonManager margin, you could just wait a little longer to do that. If you wait until the LabelIconCommandManager sublayout() method is called, your LabelField will have had its layout() method called, and labelField_width assigned correctly:
protected void sublayout(int maxWidth, int maxHeight) {
// make sure to call superclass method first!
super.sublayout(maxWidth, maxHeight);
// now, we can reliably use the label width:
side = screenWidth - labelField_width - 32 - 10 - 15;
buttonManager.setMargin(0,0,0,side);
}
That method goes in the LabelIconCommandManager class. And then, you can remove the other place you call buttonManager.setMargin().
Some brief summary from Nate post.
When you construct manager and add fields don't expect that it will be layouted correctly. Manager doesn't know the context - where it will be placed. So layout method for field will be called only when you add his manager to the screen (when layout for manager will be also called). And this is correct.
Move the calculation of your side variable to layout method.
If you really need side value before you put manager to screen. You could precalculate it by using Field.getPrefferedWidth() which returns meaningful values for standard fields (getFont().getAdvance(text) for LabelField, probably also with borders please check yourself). But be careful with this values.
Please review code below. It's manager which has label and buttons. And it puts label at the left side and buttons at the right.
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.Manager;
import net.rim.device.api.ui.component.ButtonField;
import net.rim.device.api.ui.component.LabelField;
import net.rim.device.api.ui.decor.Border;
import java.util.Vector;
public class TabFieldManager extends Manager {
public TabFieldManager(long style) {
super(style);
}
protected void sublayout(int width, int height) {
LabelField label = null;
Vector tabs = new Vector();
int tabsWidth = 0;
int tabHeight = 0;
int tabPaddingTop = 0;
int tabPaddingLeft = 0;
for (int i=0; i < getFieldCount(); i++) {
Field field = getField(i);
if (field instanceof LabelField) {
label = (LabelField) field;
} else if (field instanceof ButtonField){
tabs.addElement(field);
layoutChild(field, width, height);
int fieldwidth = field.getWidth() > 0 ? field.getWidth() : field.getPreferredWidth() ;
tabsWidth += fieldwidth + getBorderAndPaddingWidth(field);
int fieldHeight = field.getHeight() > 0 ? field.getHeight() : field.getPreferredHeight();
if (fieldHeight > tabHeight) {
tabHeight = getBorderAndPaddingHeight(field) + fieldHeight;
}
int fieldPaddingTop = field.getPaddingTop();
if (fieldPaddingTop > tabPaddingTop) {
tabPaddingTop = fieldPaddingTop;
}
int fieldPaddingLeft = field.getPaddingLeft();
if (fieldPaddingLeft > tabPaddingLeft) {
tabPaddingLeft = fieldPaddingLeft;
}
}
}
if (label != null) {
layoutChild(label, width - tabsWidth, height);
int y = tabHeight - label.getHeight() >> 1;
setPositionChild(label, tabPaddingLeft , y);
}
for (int i = 0; i < tabs.size(); i++) {
Field tabField = (Field) tabs.elementAt(i);
setPositionChild(tabField, width - tabsWidth, getBorderAndPaddingHeight(tabField));
tabsWidth -= tabField.getWidth() + getBorderAndPaddingWidth(tabField);
}
setExtent(width, tabHeight);
}
private int getBorderAndPaddingHeight( Field field ) {
int height = field.getPaddingTop() + field.getPaddingBottom();
Border border = field.getBorder();
if( border != null ) {
height += border.getTop() + border.getBottom();
}
return height;
}
private int getBorderAndPaddingWidth( Field field ){
int width = field.getPaddingLeft() + field.getPaddingRight();
Border border = field.getBorder();
if( border != null ) {
width += border.getLeft() + border.getRight();
}
return width;
}
protected int moveFocus(int amount, int status, int time) {
if ((status & Field.STATUS_MOVE_FOCUS_VERTICALLY) == Field.STATUS_MOVE_FOCUS_VERTICALLY && amount > 0) {
return amount;
} else
return super.moveFocus(amount, status, time);
}
protected int nextFocus(int amount, int axis) {
if (amount > 0 && axis == Field.AXIS_VERTICAL)
return -1;
else
return super.nextFocus(amount, axis);
}
}

Blackberry UI Hint Box

I am very much new to the blackberry UI development and want to know that is there any way you can add a hint box on bitmap? just like a title attribute in an element of HTML
You can use the following TooltipScreen and add fields to the screen in the following way
add(new ButtonField(“myButton”), “My Tooltip text”);
TooltipScreen
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.XYRect;
import net.rim.device.api.ui.container.MainScreen;
public class TooltipScreen extends MainScreen {
TooltipScreen screen = this;
boolean doRedraw = false;//prevent infinte redrawing
Vector tooltips = new Vector();//vector to hold tooltip strings
private Timer tooltipTimer = new Timer();
private TimerTask tooltipTask;
boolean alive = false;//is the tooltip alive? used to pop it after our timeout
int count = 0;//used to calculate time tooltip is displayed
//tooltip popup colours:
int backgroundColour = 0xeeeeee;
int borderColour = 0xaaaaaa;
int fontColour = 0x666666;
//the tooltip:
String tooltip;
int tooltipWidth;
int yCoord;
int xCoord;
//region parameters:
XYRect contentArea;
int contentBottom;
int contentRight;
public TooltipScreen() {
super();
//when timeout reaches 100ms*20 ie. 2seconds set alive to false and redraw screen:
tooltipTask = new TimerTask() {
public void run() {
if (alive) {
count++;
if (count == 20) {
alive = false;
invalidate();
}
}
}
};
tooltipTimer.scheduleAtFixedRate(tooltipTask, 100, 100);
}
//override add method adds an empty string to tooltip vector:
public void add(Field field) {
tooltips.addElement("");
super.add(field);
}
//custom add method for fields with tooltip: add(myField, "myTooltip");
public void add(Field field, String tooltip) {
super.add(field);
tooltips.addElement(tooltip);
}
public void setColours(int backgroundColour, int borderColour, int fontColour) {
this.backgroundColour = backgroundColour;
this.borderColour = borderColour;
this.fontColour = fontColour;
}
//reset everything when user changes focus,
//possibly needs logic to check field has actually changed (for listfields, objectchoicefields etc etc)
protected boolean navigationMovement(int dx, int dy, int status, int time) {
count = 0;
alive = true;
doRedraw = true;
return super.navigationMovement(dx, dy, status, time);
}
protected void paint(Graphics graphics) {
super.paint(graphics);
if (alive) {
Field focusField = getFieldWithFocus();
tooltip = (String) tooltips.elementAt(screen.getFieldWithFocusIndex());
//don't do anything outside the norm unless this field has a tooltip:
if (!tooltip.equals("")) {
//get the field content region, this may fall inside the field actual region/coordinates:
contentArea = focusField.getContentRect();
contentBottom = contentArea.y + contentArea.height;
contentRight = contentArea.x + contentArea.width;
//+4 to accomodate 2 pixel padding on either side:
tooltipWidth = graphics.getFont().getAdvance(tooltip) + 4;
yCoord = contentBottom - focusField.getManager().getVerticalScroll();
//check the tooltip is being drawn fully inside the screen height:
if (yCoord > (getHeight() - 30)) {
yCoord = getHeight() - 30;
}
//check the tooltip doesn't get drawn off the right side of the screen:
if (contentRight + tooltipWidth < getWidth()) {
xCoord = contentRight;
} else {
xCoord = getWidth() - tooltipWidth;
}
//draw the tooltip
graphics.setColor(backgroundColour);
graphics.fillRect(xCoord, yCoord, tooltipWidth, 30);
graphics.setColor(borderColour);
graphics.drawRect(xCoord, yCoord, tooltipWidth, 30);
graphics.setColor(fontColour);
graphics.drawText(tooltip, xCoord + 2, yCoord);
}
}
//doRedraw logic prevents infinite loop
if (doRedraw) {
//System.out.println("redrawing screen: " + System.currentTimeMillis());
screen.invalidate();
doRedraw = false;
}
}
}

Custom editfield not displaying all typed text

Below class is a textbox field. Can this be modified so that when the textbox is filled with text and user keeps type the text then scrolls ? Whats happening now is that once the textbox is filled with text any subsequent text that is typed is not being displayed.
Thanks
import net.rim.device.api.ui.Color;
import net.rim.device.api.ui.Font;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.component.EditField;
public class CustomEditField extends EditField {
// private members of the CustomEditField class
private Font defaultFont;
// used to get the default font
private String text;
// used to specify the default width of the table cells
// constructor calls the super class constructor
public CustomEditField(String label, String initialValue, int maxNumChars,
long style) {
super(label, initialValue, maxNumChars, style);
}
// overrides the default getPreferredWidth functionality to return a fixed
// width
public int getPreferredWidth() {
defaultFont = Font.getDefault();
text = "0000000000";
return defaultFont.getAdvance(text);
}
// overrides the default layout functionality to set the width of the table
// cell
protected void layout(int width, int height) {
width = getPreferredWidth();
height = super.getPreferredHeight();
super.layout(width, height);
// uses the super class' layout functionality
// after the width and the height are set
super.setExtent(width, height);
// uses the super class' setExtent functionality
// after the width and the height are set
}
public void paint(Graphics graphics){
graphics.setBackgroundColor(Color.LIGHTBLUE);
super.paint(graphics);
}
}
This will help you to get started. It is a simplified version of the ScrollableEditField that I am using. I coded it before touch BlackBerry devices became available, therefore some additional work is required here to support TouchEvents.
class ScrollableEditField extends Manager {
private final static int DEFAULT_TOP_PADDING = 1;
private final static int DEFAULT_BOTTOM_PADDING = 1;
private final static int DEFAULT_LEFT_PADDING = 1;
private final static int DEFAULT_RIGHT_PADDING = 1;
private int TOTAL_VERTICAL_PADDING = DEFAULT_TOP_PADDING + DEFAULT_BOTTOM_PADDING;
private int TOTAL_HORIZONTAL_PADDDING = DEFAULT_LEFT_PADDING + DEFAULT_RIGHT_PADDING;
private int width = -1;
private int height = -1;
private HorizontalFieldManager hfm = new HorizontalFieldManager(HORIZONTAL_SCROLL);
private EditField ef;
public ScrollableEditField(String label, String initialValue, int maxNumChars, long innerEditFieldStyle) {
super(NO_HORIZONTAL_SCROLL);
ef = new EditField(label, initialValue, maxNumChars, innerEditFieldStyle);
hfm.add(ef);
add(hfm);
}
protected void sublayout(int width, int height) {
if (this.width != -1) {
width = this.width;
}
if (this.height != -1) {
height = this.height;
} else {
height = ef.getFont().getHeight();
}
layoutChild(hfm, width-TOTAL_HORIZONTAL_PADDDING, height-TOTAL_VERTICAL_PADDING);
setPositionChild(hfm, DEFAULT_LEFT_PADDING, DEFAULT_TOP_PADDING);
setExtent(width, height);
}
public EditField getEditField() {
return ef;
}
public void setWidth(int width) {
this.width = width;
}
protected void onFocus(int direction) {
super.onFocus(direction);
ef.setCursorPosition(0);
}
protected void onUnfocus() {
hfm.setHorizontalScroll(0);
super.onUnfocus();
}
};
public class ScrollableEditFieldScreen extends MainScreen {
public ScrollableEditFieldScreen() {
super(NO_VERTICAL_SCROLL);
setTitle("ScrollableEditField");
// hfm1 and hfm2 are here just to position the ScrollableEditField in the center of the screen
HorizontalFieldManager hfm1 = new HorizontalFieldManager(USE_ALL_HEIGHT | FIELD_HCENTER);
HorizontalFieldManager hfm2 = new HorizontalFieldManager(FIELD_VCENTER);
// instantiating the scrollable edit field and adding border
ScrollableEditField sef = new ScrollableEditField("", "", 50, 0);
sef.setBorder(BorderFactory.createRoundedBorder(new XYEdges(5,5,5,5)));
sef.setWidth(sef.getFont().getAdvance('0')*10);
hfm2.add(sef);
hfm1.add(hfm2);
add(hfm1);
}
}

Vertical scrollbar with jump points - setVerticalScroll locking UI

I have a question about the BlackBerry VerticalScrollField and scrolling which seems to lock or make the UI unstable. The following code is a BlackBerry screen with worlds as content on the left (in a scroll field) and a jumpbar off to the right that allows clicking into the content.
When a jump letter is clicked the setVerticalScroll method is called, it performs the scroll but has the unfortunate side effect of rendering the UI unstable or unusable. The scroll call is done on the UI thread so its not clear what the source of the error is. The app is being tested in a 6.0 simulator.
I've included the class which can be copied into BB Eclipse for hacking/testing.
The section that kicks of the scrolling can be found towards the bottom with the following code:
UiApplication.getUiApplication().invokeLater(new Runnable(){
public void run() {
scroller.setVerticalScroll(y, true);
}});
Here's the full class:
package test;
import java.util.Vector;
import net.rim.device.api.system.ApplicationManager;
import net.rim.device.api.ui.Field;
import net.rim.device.api.ui.Font;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.TouchEvent;
import net.rim.device.api.ui.UiApplication;
import net.rim.device.api.ui.component.LabelField;
import net.rim.device.api.ui.component.Status;
import net.rim.device.api.ui.container.HorizontalFieldManager;
import net.rim.device.api.ui.container.MainScreen;
import net.rim.device.api.ui.container.VerticalFieldManager;
public class Startup extends UiApplication {
private int[] jump;
static final String[] words = new String[]{
"auto", "apple", "bear", "car", "farm", "ferret", "gold",
"green", "garden", "hedge", "happy", "igloo", "infrared",
"jelly", "kangaroo", "lemon", "lion", "marble", "moon",
"nine", "opera", "orange", "people", "puppy", "pear",
"quince", "race", "run", "sunset", "token", "willow", "zebra"
};
private final static String[] alphabet = new String[]{"A","B","C","D","E",
"F","G","H","I","J","K","L","M","N","O","P","Q","R",
"S","T","U","V","W","X","Y","Z","#"};
private VerticalFieldManager scroller;
public Startup() {
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
UiApplication.getUiApplication().pushScreen(new ScrollScreen());
}
});
}
public static void main(String[] args) {
ApplicationManager app = ApplicationManager.getApplicationManager();
while (app.inStartup()) {
try { Thread.sleep(200); } catch (Throwable e) {}
}
Startup startup = new Startup();
startup.enterEventDispatcher();
}
/**
* Screen with content in a scrollbar left and a letters on the right that
* can be used to jump into the content.
*/
class ScrollScreen extends MainScreen {
public ScrollScreen() {
super(NO_HORIZONTAL_SCROLL | NO_VERTICAL_SCROLL);
HorizontalFieldManager hfm = new HorizontalFieldManager(USE_ALL_HEIGHT | NO_VERTICAL_SCROLL | NO_HORIZONTAL_SCROLL){
protected void sublayout(int maxWidth, int maxHeight) {
Field scroll = getField(0);
Field alpha = getField(1);
layoutChild(alpha, maxWidth, maxHeight);
layoutChild(scroll, maxWidth-alpha.getWidth(), maxHeight);
setPositionChild(scroll, 0, 0);
setPositionChild(alpha, maxWidth-alpha.getWidth(), 0);
setExtent(maxWidth, maxHeight);
}
};
hfm.add(createScrollContent());
hfm.add(createAlphabetJumpBar());
add(hfm);
}
private Field createScrollContent() {
Vector vocabulary = new Vector();
for (int ii=0; ii<alphabet.length; ii++)
vocabulary.addElement(alphabet[ii]);
scroller = new VerticalFieldManager(VERTICAL_SCROLL | USE_ALL_WIDTH) {
protected void sublayout(int maxWidth, int maxHeight) {
// Record the jump offsets
int y = 0;
for (int ii=0; ii<getFieldCount(); ii++) {
Field field = getField(ii);
layoutChild(field, maxWidth, maxHeight);
setPositionChild(field, 0, y);
if (field instanceof WordField) {
WordField object = (WordField)field;;
char character = object.getWord().toLowerCase().charAt(0);
int offset = ((int)character)-(int)alphabet[0].toLowerCase().charAt(0);
if (offset < 0 || offset > jump.length)
offset = jump.length-1;
while (offset >= 0 && offset < jump.length && jump[offset] == 0) {
jump[offset] = y;
offset--;
}
}
y += field.getHeight();
}
int offset = jump.length-1;
do {
jump[offset] = y;
offset--;
} while (offset >= 0 && jump[offset] == 0);
setExtent(maxWidth, maxHeight);
setVirtualExtent(maxWidth, y+10);
}
};
jump = new int[alphabet.length];
Font largeFont = Font.getDefault().derive(Font.PLAIN, 46);
for (int ii=0; ii<words.length; ii++) {
WordField wordField = new WordField(words[ii]);
wordField.setFont(largeFont);
scroller.add(wordField);
}
return scroller;
}
private Field createAlphabetJumpBar() {
VerticalFieldManager vfm = new VerticalFieldManager() {
protected void sublayout(int maxWidth, int maxHeight) {
int y = 0;
int width = 0;
double allowedAlphaHeight = (double)maxHeight / (double)getFieldCount();
for (int ii=0; ii<getFieldCount(); ii++) {
WordField field = (WordField)getField(ii);
layoutChild(field, maxWidth, (int)allowedAlphaHeight);
setPositionChild(field, 0, y);
y += field.getHeight();
double paddedY = Math.floor(allowedAlphaHeight*(ii+1));
if (y < paddedY) y = (int)paddedY;
width = Math.max(width, field.getWidth());
}
setExtent(width, maxHeight);
}
};
for (int ii=0; ii<alphabet.length; ii++) {
vfm.add(new AlphaField(alphabet[ii]){
protected boolean touchEvent(TouchEvent message) {
if (message.getEvent() == TouchEvent.UP) {
int startOffset = (int)alphabet[0].charAt(0);
int offset = ((int)getWord().charAt(0)) - startOffset;
final int y = offset == 0 ? 0 : jump[offset - 1];
UiApplication.getUiApplication().invokeLater(new Runnable(){
public void run() {
scroller.setVerticalScroll(y, true);
}});
}
return true;
}
});
}
return vfm;
}
class WordField extends LabelField {
private final String word;
public WordField(String word) {
super(word);
this.word = word;
}
public String getWord() { return word; }
}
Font alphaFont = null;
class AlphaField extends WordField {
public AlphaField(String word) {
super(word);
}
protected void layout(int width, int height) {
if (alphaFont == null)
alphaFont = Font.getDefault().derive(Font.PLAIN, height);
setExtent(alphaFont.getAdvance(getWord()), alphaFont.getHeight());
}
protected void paint(Graphics graphics) {
graphics.setFont(alphaFont);
graphics.drawText(getWord(), 0, 0);
}
}
/**
* For debugging.
* #see net.rim.device.api.ui.Screen#keyChar(char, int, int)
*/
protected boolean keyChar(char c, int status, int time) {
if ('o' == c) { // shows the jump offsets into the scroll field
UiApplication.getUiApplication().invokeLater(new Runnable(){
public void run() {
StringBuffer buf = new StringBuffer();
for (int ii=0; ii<jump.length; ii++) {
buf.append(alphabet[ii]+"="+jump[ii]);
if (ii<jump.length-1)
buf.append(",");
}
Status.show("offsets="+buf.toString());
}});
}
return super.keyChar(c, status, time);
}
}
}
You're using UiApplication.invokeLater in a few places where you're already on the UI event thread, so those are redundant - the debug code in keyChar and the setVerticalScroll call from the touchEvent handler. The Runnable is executed synchronously when you do an invokeLater from the UI thread, with no delay specified.
Are you sure you want to set the scroll explicitly? One option would be to set the focus on the WordField you are interested in, by calling setFocus(), then the OS will do the scrolling events to move that field on screen for you.
If you really need to explicitly set the vertical scroll, your problem may be that the touch event is already causing scroll, so setting it again causes problems. You can get around this by specifying a one millisecond delay for your invokeLater(...). This means your Runnable will be added to the event queue, instead of executing synchronously. That way the scroll won't be changed in the middle of another event call-stack.
Finally tracked down the issue - if the touchEvent for the alphabet label field returns a true then it locks up the main scroll field, if however return super.touchEvent(message) is called the scrolling happens and the scroll field can still be scrolled up and down by clicking on the screen.
This may be a bug in the BlackBerry OS or just the simulator. The Field.touchEvent() documentation for 6.0 recommends returning true if the method consumes the event; however doing so (at least in the above code) causes another UI field to loose the ability to detect touch events which would cause it to scroll.

Resources