So far, none of the route information I've seen has actually required scrolling, but I don't think I can rule it out. On the other hand, I'm very tempted to ignore it because while I am used to the idea of scrolling being handled by the UI toolkit, it seems that this is very much not the case here. So if I am going to do it myself, I am left having to hand-roll it.
Normally, I would consider all my options (sorting the data so that the trams appear in order would be one way to make sure the most relevant information does appear on the screen), but given the nature of this blog and what seems to be involved, I think I am just going to go for it.
Up until now, we have been using the UI toolkit with Layout and built-in components (specifically the TextArea). But it would seem it is also possible to draw directly to the device using the Dc passed into onUpdate. This is, obviously, going to be much harder than just saying "here, TextArea, show this string", but I'm up for the challenge if you are, especially since there are a couple of samples out there on the internet.
So let us go back to square one. I'm going to remove the whole of onLayout and comment out the variable textArea and all it's usages. I'm only going to comment these out - rather than remove them - to remind myself of what the logic should be. When everything is wrapped up, I'll delete anything that's left, but in the meantime I suspect I may just end up slipping in an alternate definition of textArea.
So, starting with onUpdate, we can try showing the "Please Wait..." message without using the textArea. We have a drawing context, dc, and this has (among other things) a drawText method.
This conveniently seems to do centering for us, so if we can find the centre of the screen, we can presumably do what we want fairly easily. Fortuitously, there is an example of doing just that earlier on the page, and the dc object has getWidth and getHeight methods.
if (showWait) {Sadly, this doesn't seem to work. It's not immediately clear why. But thinking through what is happening here, I'm aware that onUpdate is called in a context, and the drawing context passed in needs to be established and colors set. Let's try something simpler. Let's set the foreground color to be white to be sure, and then let's try and draw a box in the lower-right (3 o'clock to 6 o'clock) portion of the display.
// textArea.setText("\n\nPlease Wait.\nLoading Data...\n");
showWait = false;
dc.drawText(
dc.getWidth() / 2,
dc.getHeight() / 2,
Graphics.FONT_SMALL,
"Please Wait.",
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
);
}
function onUpdate(dc as Dc) as Void {No, nothing doing. Reviewing the code and thinking about the logic some more, I realize that there is a not-completely-innocous call to View.onUpdate() at the end of my onUpdate. It even has an associated comment that it redraws the screen. Now, up until now I have wanted that at the end of my function so that any updates I may have made to the TextArea are applied before it is redrawn. But now I am not using the layout but redrawing directly, that is probably clearing off the screen the moment I have drawn to it. Let's reverse that, and call it first.
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(dc.getWidth()/2, dc.getHeight()/2, dc.getWidth()/2, dc.getHeight()/2);
if (showWait) {
function onUpdate(dc as Dc) as Void {Well, at least we get the box. Reviewing the code again, I notice that there is a showWait variable that I've used to make sure that we don't show the "waiting..." message once we have data; but if we come through onUpdate more than once, we will clear off the message. Removing the assignment to false, the code magically works.
// Call the parent onUpdate function to redraw the layout
View.onUpdate(dc);
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(dc.getWidth()/2, dc.getHeight()/2, dc.getWidth()/2, dc.getHeight()/2);
dc.fillRectangle(dc.getWidth()/2, dc.getHeight()/2, dc.getWidth()/2, dc.getHeight()/2);
if (showWait) {
// textArea.setText("\n\nPlease Wait.\nLoading Data...\n");
// showWait = false;
dc.drawText(
dc.getWidth() / 2,
dc.getHeight() / 2,
Let's Throw All That Away
I've checked all that in on a branch (the HEAD is tagged $METROLINK_MC_ALWAYS_SHOW), but only so that I can have it there to show on this blog. I'm going to throw it all away - I went down a blind alley.Admittedly, I did what I did for good reasons - I could not scroll a text area, so I needed to replace it with something else. It seemed the simplest thing was to draw directly on the screen, but actually that isn't so simple. To carry on from where I was, I would need to figure out how to persist the message to be displayed so that I could keep onUpdate reasonably simple. And that would ultimately lead to another abstraction. Without even doing it, I can see that I would end up reimplementing the whole of the layout mechanism. Why?
The motivator, of course, is that I can't use (as far as I can tell) my own components with the XML layout compiler in Monkey C. But just because I can't use their compiler doesn't mean I can't use my own layout. The compiler in fact generates very simple code. This is to be found in $bin/gen/.../source:
module Rez {And let's face it, that's really quite a simple piece of code. It's basically just creating and initializing a text area and packaging it up in an array. I could do that myself.
module Drawables {
...
} // Drawables
module Layouts {
...
function RouteLayout(dc as Graphics.Dc) as Array<WatchUi.Drawable> {
var rez_cmp_local_textarea_routeInfo = new WatchUi.TextArea({:identifier=>"routeInfo", :width=>240, :text=>"", :justification=>Gfx.TEXT_JUSTIFY_CENTER, :height=>240, :font=>[Graphics.FONT_MEDIUM] as Array<Graphics.FontType>});
return [rez_cmp_local_textarea_routeInfo] as Array<WatchUi.Drawable>;
}
} // Layouts
module Strings {
...
} // Strings
} // Rez
So winding back to where we were, I'm going to extract that code and put the line that creates the TextArea in my intialize constructor and put the array line in onLayout and check that everything still works without the XML file being involved.
function initialize(routes as Array<Route>) {Unsurprisingly, that now works. So I want to replace the concept of a TextArea with a ScrollArea which is a component which knows how to draw itself and works in every way like a TextArea except it also has methods to scrollUp and scrollDown when we swipe up and down.
self.routes = routes;
self.textArea = new WatchUi.TextArea({:identifier=>"routeInfo", :width=>240, :text=>"", :justification=>Graphics.TEXT_JUSTIFY_CENTER, :height=>240, :font=>[Graphics.FONT_MEDIUM] as Array<Graphics.FontType>});
View.initialize();
}
function onLayout(dc as Dc) as Void {
setLayout([self.textArea] as Array<WatchUi.Drawable>);
}
ScrollArea
Looking at the manual page for TextArea, I can see that it inherits from WatchUi.Drawable, so I know that I want to do the same. So let's create a skeleton class for ScrollArea and use that instead of TextArea in our view:import Toybox.WatchUi;And that "works" as long as you don't want the text to display.
import Toybox.Lang;
class ScrollArea extends Drawable {
var tx as String?;
function initialize(options) {
Drawable.initialize(options);
}
function setText(tx as String) {
self.tx = tx;
}
}
In order to have the text display, it is necessary to add a draw method to the ScrollArea class, and the simplest implementation of that is just to repeat what we did above directly in the view's onUpdate method. As we learnt before, if you want to be able to read the text, you need to specify a drawing color.
Either unsurprisingly, or amazingly (depending on your general optimism level), that's all we need to do to replace TextArea: the drawText method automatically handles newline processing and centering.
function draw(dc as Dc) {So all we need to do now is enable this to scroll.
System.println("in draw with " + tx);
if (tx != null) {
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.drawText(
dc.getWidth() / 2,
dc.getHeight() / 2,
Graphics.FONT_SMALL,
tx,
Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER
);
}
}
We already have an InputDelegate listening for these events, so we just wire up the SWIPE_UP and SWIPE_DOWN events, redirecting them to the view. The view, in turn, handles these events by passing them off to the ScrollArea and then requesting the view to be refreshed. The events are handled in the ScrollArea by either incrementing or decrementing a (scroll) offset, and then the y position in draw is determined by subtracting a quarter of the screen height for every unit of offset.
Everything here is about coordinates and directions: SWIPE_UP and SWIPE_DOWN act in the opposite manner to which I would expect, so I translate SWIPE_UP to scrollDown. This increments offset, because we want to be further down the scroll, but we achieve that by saying we start drawing the (center of) the scroll further up the screen. It might be clearer to not use the word "scroll" at all and connect SWIPE_UP to viewUp which decrements offset, which can then be added to the y value. Or not.
In the navigation handler, we have:
case SWIPE_UP: {In the view, we add:
view.scrollDown();
break;
}
case SWIPE_DOWN: {
view.scrollUp();
break;
}
function scrollUp() {We add these methods in the ScrollArea:
textArea.scrollUp();
WatchUi.requestUpdate();
}
function scrollDown() {
textArea.scrollDown();
WatchUi.requestUpdate();
}
function scrollDown() {And then wrap it all up in draw:
offset ++;
}
function scrollUp() {
offset --;
}
}
function draw(dc as Dc) {I feel that more should be done here - specifically, there should be some kind of "bounds" within which the offset should be kept, but, to be quite honest, I don't care enough. If you do, add it. If I do later, I'll add it. Frankly, there are a bunch more things I'd like to add - but even so, I'm not going to right now. For now, I need to get this blog published :-)
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.drawText(
dc.getWidth() / 2,
dc.getHeight() / 2 - (dc.getHeight() * self.offset)/4,
Graphics.FONT_SMALL,
So I checked that in and tagged it as METROLINK_MC_SCROLL_AREA.
No comments:
Post a Comment