Related
This is a continuation of a question here: Trying to setup a custom DependencyObject. Clearly missing something. It's not practical to edit the original question; changes are too great. So I'm starting a fresh question.
I'm trying to setup bindings between custom DependencyObjects in my UWP app. The relevant code is below. I am seeing calls to ActualWidthPropertyChanged, but they are not triggering any call to WidthPropertyChanged. What am I missing?
class WindowsElement: DependencyObject
{
public WindowsElement()
{
}
public double Width
{
get
{
return (double)GetValue(WidthProperty);
}
set
{
SetValue(WidthProperty, value);
}
}
private static void WidthPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WindowsElement element = (WindowsElement)o;
double width = (double)e.NewValue;
CommonDebug.LogLine("WPC", element, o, width);
element.Width = width;
}
private static void ActualWidthPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WindowsElement element = (WindowsElement)o;
double width = (double)e.NewValue;
CommonDebug.LogLine("AWPC", o, e, width, element.Width);
element.ActualWidth = width;
}
public static readonly DependencyProperty WidthProperty = DependencyProperty.Register(
"Width",
typeof(double),
typeof(WindowsElement),
new PropertyMetadata((double)0, WidthPropertyChanged));
public double ActualWidth {
get
{
return (double)GetValue(ActualWidthProperty);
}
set
{
SetValue(ActualWidthProperty, value);
}
}
public static readonly DependencyProperty ActualWidthProperty =
DependencyProperty.Register(
"ActualWidth",
typeof(double),
typeof(WindowsElement),
new PropertyMetadata((double)0, ActualWidthPropertyChanged));
public static void MessWithBindings()
{
WindowsElement we1 = new WindowsElement();
WindowsElement we2 = new WindowsElement();
var b = new Binding
{
Source = we2,
Path = new PropertyPath("ActualWidth")
};
BindingOperations.SetBinding(we1, WindowsElement.WidthProperty, b);
we2.ActualWidth = 13;
CommonDebug.LogLine(we1, we1.Width, we1.ActualWidth, we2, we2.Width, we2.ActualWidth);
}
}
I am seeing calls to ActualWidthPropertyChanged, but they are not triggering any call to WidthPropertyChanged. What am I missing?
To solve this question, you would need to implement the INotifyPropertyChanged interface on the source object so that the source can report changes.
Please see the following code:
class WindowsElement : DependencyObject, INotifyPropertyChanged
{
public WindowsElement()
{
}
public double Width
{
get
{
return (double)GetValue(WidthProperty);
}
set
{
SetValue(WidthProperty, value);
}
}
private static void WidthPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WindowsElement element = (WindowsElement)o;
double width = (double)e.NewValue;
CommonDebug.LogLine("WPC", element, o, width);
//element.Width = width;
element.RaisedPropertyChanged("Width");
}
private static void ActualWidthPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WindowsElement element = (WindowsElement)o;
double width = (double)e.NewValue;
CommonDebug.LogLine("AWPC", o, e, width, element.Width);
//element.ActualWidth = width;
element.RaisedPropertyChanged("ActualWidth");
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisedPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
public static readonly DependencyProperty WidthProperty = DependencyProperty.Register(
"Width",
typeof(double),
typeof(WindowsElement),
new PropertyMetadata((double)0, WidthPropertyChanged));
public double ActualWidth
{
get
{
return (double)GetValue(ActualWidthProperty);
}
set
{
SetValue(ActualWidthProperty, value);
}
}
public static readonly DependencyProperty ActualWidthProperty = DependencyProperty.Register(
"ActualWidth",
typeof(double),
typeof(WindowsElement),
new PropertyMetadata((double)0, ActualWidthPropertyChanged));
public static void MessWithBindings()
{
WindowsElement we1 = new WindowsElement();
WindowsElement we2 = new WindowsElement();
var b = new Binding
{
Source = we2,
Path = new PropertyPath("ActualWidth")
};
BindingOperations.SetBinding(we1, WindowsElement.WidthProperty, b);
we2.ActualWidth = 13;
CommonDebug.LogLine(we1, we1.Width, we1.ActualWidth, we2, we2.Width, we2.ActualWidth);
}
}
Not sure why in UWP a one-way Binding from one dependency property to another doesn't automatically update the target property (as it does in WPF).
However, you could simply revert the direction of the Binding and make it two-way:
var b = new Binding
{
Source = we1,
Path = new PropertyPath("Width"),
Mode = BindingMode.TwoWay
};
BindingOperations.SetBinding(we2, WindowsElement.ActualWidthProperty, b);
we2.ActualWidth = 13;
I am trying to use Mouse.GetState() for my menu selection. Currently, it will only highlight if I hover over a region left and up from where the menu is. I used DrawString to display the mouses coordinates and found that the 0,0 point wasn't in the top left of my monitor or in the top left of the game window. It was somewhere about 100,100 pixels from the top left of the screen. Also, the 0,0 point moves every time I run the programme.
I looked at others people who have had the same problem but wasn't able to solve it. I tried using Mouse.WindowHandle = this.Window.Handle; in my Initialize() but it didn't nothing. I have two monitors and when I forced the game in fullscreen it would open on my second monitor so I disabled it but the problem remains.
here is a link to my code http://pastebin.com/PNaFADqp
Game1 class:
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont spriteFont;
public const int WINDOW_HEIGHT = 800;
public const int WINDOW_WIDTH = 600;
public int tree;
public TitleScreen titleScreen;
public SATDemo satDemo;
public SeparatingAxisTest separatingAxisTest;
public SATWithAABB sATWithAABB;
GameState currentState;
public static Dictionary<string, Texture2D> m_textureLibrary = new Dictionary<string, Texture2D>();
public static Dictionary<string, SpriteFont> m_fontLibrary = new Dictionary<string, SpriteFont>();
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferHeight = WINDOW_HEIGHT;
graphics.PreferredBackBufferWidth = WINDOW_WIDTH;
}
protected override void Initialize()
{
Mouse.WindowHandle = this.Window.Handle;
//enable the mousepointer
IsMouseVisible = true;
currentState = GameState.TitleScreen;
//sets the windows mouse handle to client bounds handle
base.Initialize();
}
public void RequestSATDemo()
{
currentState = GameState.RequestSATDemo;
}
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
m_textureLibrary.Add("Pixel", Content.Load<Texture2D>("White_Pixel"));
m_fontLibrary.Add("Font", Content.Load<SpriteFont>("MotorwerkOblique"));
titleScreen = new TitleScreen();
satDemo = new SATDemo();
separatingAxisTest = new SeparatingAxisTest();
sATWithAABB = new SATWithAABB();
}
public void RequestSeparatingAxisTest()
{
currentState = GameState.SeparatingAxisTest;
}
public void RequestSATWithAABB()
{
currentState = GameState.SATWithAABB;
}
protected override void Update(GameTime gameTime)
{
MouseTestState = Mouse.GetState();
switch (currentState)
{
case GameState.TitleScreen:
{
titleScreen.Update(gameTime);
break;
}
case GameState.SeparatingAxisTest:
{
separatingAxisTest.Update(gameTime);
break;
}
case GameState.SATWithAABB:
{
sATWithAABB.Update(gameTime);
break;
}
case GameState.Exit:
{
Exit();
break;
}
default:
{
titleScreen.Update(gameTime);
break;
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
spriteBatch.Begin();
spriteBatch.DrawString(m_fontLibrary["Font"], MouseTestState.ToString(), new Vector2(0, 0), Color.White);
switch (currentState)
{
case GameState.TitleScreen:
{
titleScreen.Draw(spriteBatch, spriteFont);
break;
}
case GameState.SeparatingAxisTest:
{
separatingAxisTest.Draw(gameTime, spriteBatch);
break;
}
case GameState.SATWithAABB:
{
sATWithAABB.Draw(gameTime, spriteBatch);
break;
}
case GameState.Exit:
{
Exit();
break;
}
default:
{
titleScreen.Update(gameTime);
break;
}
}
spriteBatch.End();
base.Draw(gameTime);
}
}
TitleScreen class:
public class TitleScreen : Screen
{
List<Button> buttonList = new List<Button>();
public Menu mainMenu;
public TitleScreen()
{
mainMenu = new Menu(new Vector2(200, 100), buttonList, 0);
buttonList.Add(new PushButton("Separating Axis Test"));
buttonList.Add(new PushButton("SAT With AABB"));
buttonList.Add(new PushButton("Awesome"));
buttonList.Add(new PushButton("Awesomere"));
buttonList.Add(new PushButton("Awesomere"));
}
public override void Update(GameTime gametime)
{
mainMenu.Update(gametime);
}
public void Draw(SpriteBatch sB, SpriteFont sF)
{
mainMenu.Draw(sB, sF);
}
}
PushButton class:
public class PushButton : Button
{
string m_text;
SpriteFont m_font;
Color m_static, m_onClick, m_onHover;
Texture2D m_sprite2D, m_onClick2D;
static public int Pbuttoncount;
//click processing
bool m_clickedInside = false,
m_releasedInside = false,
m_OnClicked = false,
selected = false;
Rectangle drawRectangle;
public PushButton(string Text)
{
m_text = Text;
drawRectangle = new Rectangle((int)Menu.m_position.X, (int)Menu.m_position.Y + (15 * Pbuttoncount), 200, 15);
ButtonRegion = new Rectangle((int)Position.X, (int)Position.Y, 200, 15);
Pbuttoncount++;
}
public PushButton(Rectangle ButtonRegion, SpriteFont Font, string Text, Color Static, Color OnClick, Color OnHover)
{
m_buttonRegion = ButtonRegion;
m_font = Font;
m_text = Text;
m_static = Static;
m_onClick = OnClick;
m_onHover = OnHover;
// drawRectangle = ButtonPosition(m_buttonRegion);
}
public PushButton(Rectangle ButtonRegion, Texture2D Sprite2D, Texture2D OnClick2D)
{
m_buttonRegion = ButtonRegion;
m_sprite2D = Sprite2D;
m_onClick2D = OnClick2D;
//drawRectangle = ButtonPosition(m_buttonRegion);
}
public override void Update(GameTime gameTime)
{
MouseState currentMouse = Mouse.GetState();
selected = MouseState(drawRectangle, currentMouse);
m_clickedInside = ClickInside(currentMouse, m_lastMouseState);
ReleaseInside(currentMouse, m_lastMouseState);
if (selected && m_clickedInside && m_releasedInside)
m_OnClicked = true;
else
m_OnClicked = false;
m_lastMouseState = currentMouse;
}
public override void Draw(SpriteBatch spriteBatch, SpriteFont spriteFont, int buttonCount, Vector2 Position)
{
spriteBatch.Draw(Game1.m_textureLibrary["Pixel"], new Rectangle((int)Position.X + 10, (int)(Position.Y + 15 * buttonCount), 180, 15), Color.Wheat);
if (selected)
spriteBatch.DrawString(Game1.m_fontLibrary["Font"], m_text, new Vector2(Position.X + 15, Position.Y + 15 * buttonCount), Color.Orange);
else
spriteBatch.DrawString(Game1.m_fontLibrary["Font"], m_text, new Vector2(Position.X + 15, Position.Y + 15 * buttonCount), Color.Black);
}
}
Menu class:
public class Menu
{
List<Button> m_buttonList;
float m_transparency;
public int n = 0;
public Rectangle buttonRegion, m_menuRegion, m_dimensions;
static public Vector2 m_position;
int m_WINDOW_HEIGHT = Game1.WINDOW_HEIGHT;
int m_WINDOW_WIDTH = Game1.WINDOW_WIDTH;
private Game1 m_managerClass;
public Menu(Vector2 Position, List<Button> ButtonList, float Transparency)
{
m_position = Position;
m_buttonList = ButtonList;
m_transparency = Transparency;
m_managerClass = new Game1();
}
public Rectangle MenuRegion
{
get { return m_menuRegion; }
set { m_menuRegion = value; }
}
static public Vector2 Position
{
get { return m_position; }
}
public void Update(GameTime gametime)
{
for (int i = 0; i < m_buttonList.Count; i++)
{
m_buttonList[i].Update(gametime);
if (m_buttonList[0].OnClicked)
{
SeperatingAxisTest();
}
}
}
public void Draw(SpriteBatch sB, SpriteFont sF)
{
sB.Draw(Game1.m_textureLibrary["Pixel"], new Rectangle((int)m_position.X - 5, (int)m_position.Y - 10, (m_buttonList[0].ButtonRegion.Width + 10), (m_buttonList[0].ButtonRegion.Height * m_buttonList.Count) + 20), Color.Blue);
for (int i = 0; i < m_buttonList.Count; i++)
{
m_buttonList[i].Draw(sB, sF, i, new Vector2(Position.X, Position.Y));
}
}
private void SeperatingAxisTest()
{
m_managerClass.RequestSeparatingAxisTest();
}
}
Program class:
public static class Program
{
[STAThread]
static void Main()
{
using (var game = new Game1())
game.Run();
}
}
Let me know if you need anything else. I'm still learning and will sell my soul to you for an answer.
Your Menu class is creating a new instance of Game1. This is, most likely, not what you want, since Game1 is instantiated in the entry point for you app. The Game1 instance has an instance of TitleScreen, which in turn has an instance of the Menu class, so a Menu should have no business creating its own game.
When this (other) instance is created, it invokes platform-specific (Windows) methods, creates an additional window handle (which is never shown) and configures the Mouse.WindowHandle.
And btw, setting WindowHandle manually does absolutely nothing in Monogame, so all these sources mentioning that are talking about XNA.
So, there are several remarks:
You should probably have a "screen manager" class which contains the current screen. It is strange to have a field of type TitleScreen in your game class, it should at least be of the base type (Screen), so that the game class draws and updates each screen transparently.
If you need a reference to the game class anywhere, don't instantiate a new one, but rather pass it along through the constructor.
m_managerClass is a bad name for a field which is actually a Game. Also google for C# naming conventions. Perhaps you even might want to download an existing monogame game template, e.g. check some of the samples online; the NetRumble sample seems to implement a screen manager.
Remove the Mouse.WindowHandle line, it should be set to your one-and-only game window by default.
tl;dr add the Game1 as a parameter wherever you might need it (but only where you need it).
abstract class Screen
{
private readonly Game1 _game;
public Game1 Game
{ get { return _game; } }
public Screen(Game1 game)
{
_game = game;
}
}
class TitleScreen : Screen
{
public TitleScreen(Game1 game)
: base(game)
{ ... }
}
class Menu
{
private readonly Screen _screen;
public Menu(Screen parentScreen, Vector2 pos, List<Button> list, float alpha)
{
_screen = parentScreen;
...
// if you need the game instance, just use _screen.Game
}
}
I need a custom layout as below in BlackBerry.
I did same layout in Android. Now I need same layout in BlackBerry. I am new to BlackBerryapp development. The Fields of BlackBerry like Views in Android seem to be very confusing things to me.
I tried with VerticalFieldManager & HorizontalFieldManager by mixing these with BitmapField & LabelField to produce my layout.
I failed particularly in placing LabelField at bottom of screen. I used USE_ALL_HEIGHT & FIELD_BOTTOM style to put at bottom, but it is showing after scrolling long time.
My requirement is the header and footer should not scroll when my middle list is scrolling.
The easiest way to add header and footer fields that don't scroll with the content in the middle of the screen is to use MainScreen#setBanner() and MainScreen#setStatus().Here's an example:
public class HeaderFooterListScreen extends MainScreen {
private static final int BG_COLOR = Color.BLACK;
private static final int HIGHLIGHT_COLOR = Color.BLUE;
private static final int FONT_COLOR = Color.WHITE;
private static final int ROW_HEIGHT = 60;
private Object[] _rowData;
private Field _header;
private Field _footer;
private Field _spacer;
private int _orientation;
public HeaderFooterListScreen() {
super(MainScreen.VERTICAL_SCROLL | MainScreen.VERTICAL_SCROLLBAR);
Background bg = BackgroundFactory.createSolidBackground(BG_COLOR);
setBackground(bg);
getMainManager().setBackground(bg);
// header
Bitmap headerImg = Bitmap.getBitmapResource("header.png");
_header = new BitmapField(headerImg);
setBanner(_header);
// list
_rowData = new Object[] { "row one", "row two", "row three" }; //, "row four", "row five", "row six", "row seven", "row eight", "row nine", "row ten" };
ListField list = new ListField();
int c = Color.RED;
XYEdges edgeColors = new XYEdges(c, c, c, c);
XYEdges edgeThicknesses = new XYEdges(5, 5, 5, 5);
list.setBorder(BorderFactory.createSimpleBorder(edgeThicknesses, edgeColors, Border.STYLE_SOLID));
list.setCallback(new CustomListFieldCallback());
list.setRowHeight(ROW_HEIGHT);
list.setSize(_rowData.length);
add(list);
// footer
_footer = new LabelField("Footer Showing Status As Text", Field.USE_ALL_WIDTH | DrawStyle.HCENTER) {
public void paint(Graphics g) {
// change font color
int oldColor = g.getColor();
g.setColor(FONT_COLOR);
super.paint(g);
g.setColor(oldColor);
}
};
_footer.setFont(_footer.getFont().derive(Font.PLAIN, 24));
setStatus(_footer);
}
private void centerList() {
if (_spacer != null && _spacer.getManager() != null) {
// delete the old spacer field, if there was one
delete(_spacer);
}
int listHeight = _rowData.length * ROW_HEIGHT;
int availableHeight = getHeight() - _footer.getHeight() - _header.getHeight();
if (availableHeight > listHeight) {
boolean firstRun = (_spacer == null);
// add a spacer above the list to force it down enough to be centered
final int SPACE = (availableHeight - listHeight) / 2;
_spacer = new Field() {
protected void layout(int width, int height) {
setExtent(width, SPACE);
}
protected void paint(Graphics graphics) {
}
};
insert(_spacer, 0);
if (firstRun) {
getMainManager().setVerticalScroll(0);
}
}
}
// called when device orientation changes
protected void sublayout(int width, int height) {
super.sublayout(width, height);
if (_orientation != Display.getOrientation()) {
_orientation = Display.getOrientation();
// run with invokeLater() to avoid recursive sublayout() calls
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
// TODO: may have to adjust header, too?
centerList();
}
});
}
}
private class CustomListFieldCallback implements ListFieldCallback {
private final int PAD = 10;
public void drawListRow(ListField listField, Graphics graphics,
int index, int y, int width) {
int oldColor = graphics.getColor();
if (listField.getSelectedIndex() == index) {
graphics.setColor(HIGHLIGHT_COLOR);
} else {
graphics.setColor(BG_COLOR);
}
graphics.fillRect(0, y, width, listField.getRowHeight());
graphics.setColor(FONT_COLOR);
String text = (String)get(listField, index);
graphics.drawText(text, PAD, y + PAD, DrawStyle.LEFT);
graphics.setColor(oldColor);
}
public Object get(ListField listField, int index) {
return _rowData[index];
}
public int getPreferredWidth(ListField listField) {
return Display.getWidth();
}
public int indexOfList(ListField listField, String prefix, int start) {
return -1; // TODO?
}
}
}
You didn't specify how you wanted the list in the middle to work, so I just made some guesses. I also wasn't sure if the red border was something you wanted, or just something you used to describe your layout. Edit your question, or post a new question, if you have more requirements for the list.
Field Concepts
If you're coming from Android, and are unclear about the role of BlackBerry UI classes, like Fields and Managers, here's some resources:
another Stack Overflow answer I posted
BlackBerry Advanced UI Sample Code on Github
BlackBerry Layout Managers Tutorial
Results
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);
}
}
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.