Changing parameters of shapes defined in XML

Hi,

I have a drawable-list with some shapes (rectangles and polygons), defined in XML file. It is referenced from layout xml file as <drawable id="something" />.
The shape is quite complicated and I like the fact it is defined in XML file. But I need to change it's color in runtime before rendering. Do you guys have figured out how to do it?

I can get the drawable object by findDrawableById() or by new Rez.Drawables.Something(), but it seems to me there is no interface to change the color... Is there any other way to do it?

Thanks,
David
  • The following drawable-list definition...

    <drawable-list id="Smiley" background="Gfx.COLOR_YELLOW">
    <shape type="ellipse" x="79" y="90" width="20" height="30" color="Gfx.COLOR_BLACK" />
    <shape type="ellipse" x="139" y="90" width="20" height="30" color="Gfx.COLOR_BLACK" />
    </drawable-list>


    Is compiled into a class that looks like this...

    module Rez {
    module Drawables {

    class Smiley extends Toybox.WatchUi.Drawable {

    function initialize(params) {
    Drawable.initialize({});
    }

    function draw(dc) {
    varpX=0;
    varpY=0;
    varpWidth=dc.getWidth();
    varpHeight=dc.getHeight();
    drawComplex(dc,pX,pY,pWidth,pHeight);
    }

    function drawComplex(dc, pX, pY, pWidth, pHeight) {
    dc.setColor(Gfx.COLOR_YELLOW,Gfx.COLOR_TRANSPARENT);
    dc.fillRectangle((pX+0),(pY+0),(pWidth-0),(pHeight-0));
    dc.setColor(Gfx.COLOR_TRANSPARENT,Gfx.COLOR_YELLOW);
    dc.setColor(Gfx.COLOR_BLACK,Gfx.COLOR_TRANSPARENT);
    dc.fillEllipse(((pX+0)+79),((pY+0)+90),20-(0),30-(0));
    dc.setColor(Gfx.COLOR_BLACK,Gfx.COLOR_TRANSPARENT);
    dc.fillEllipse(((pX+0)+139),((pY+0)+90),20-(0),30-(0));
    }
    }
    }
    }


    As you can see, the colors are hard-coded into the definition of the class. You cannot modify them directly. That said, you can define variables to hold the color you want...

    <drawable-list id="Smiley" background="Gfx.COLOR_YELLOW">
    <shape type="ellipse" x="79" y="90" width="20" height="30" color="EyeColor" />
    <shape type="ellipse" x="139" y="90" width="20" height="30" color="EyeColor" />
    </drawable-list>


    Now the generated class will refer to those variables for its color (or height, or width, or position). Then you just need to define those variables in your source, and then modify them as necessary...

    var EyeColor = Gfx.COLOR_BLACK;

    class WidgetDelegate extends Ui.BehaviorDelegate
    {
    function initialize() {
    BehaviorDelegate.initialize();
    }

    const colors = [
    Gfx.COLOR_BLACK,
    Gfx.COLOR_BLUE,
    Gfx.COLOR_GREEN,
    Gfx.COLOR_RED
    ];

    function onSelect() {

    EyeColor = colors[ Math.rand() % colors.size() ];
    Ui.requestUpdate();

    return false;
    }

    function onBack() {
    return false;
    }
    }


    You can use this along with the Ui.animate() function to do simple animation. If you wanted to make the smiley blink when the screen is tapped or enter is pressed...

    using Toybox.Application as App;
    using Toybox.Graphics as Gfx;
    using Toybox.WatchUi as Ui;
    using Toybox.Math as Math;

    var EyeColor = Gfx.COLOR_BLACK;
    var EyeHeight = 30;

    class WidgetView extends Ui.View
    {
    function initialize() {
    View.initialize();
    }

    hidden var smiley;

    function onLayout(dc) {
    smiley = new Rez.Drawables.Smiley();
    }

    function onUpdate(dc) {
    smiley.draw(dc);
    }

    function blink() {
    closeEyes();
    }

    function closeEyes() {
    Ui.animate(smiley, :EyeHeight, Ui.ANIM_TYPE_EASE_IN_OUT, EyeHeight, 0, 0.5, self.method(:openEyes));
    }

    hidden const colors = [
    Gfx.COLOR_BLACK,
    Gfx.COLOR_BLUE,
    Gfx.COLOR_GREEN,
    Gfx.COLOR_RED
    ];

    function openEyes() {
    Ui.animate(smiley, :EyeHeight, Ui.ANIM_TYPE_EASE_IN_OUT, 0, 30, 0.5, null);
    EyeColor = colors[ Math.rand() % colors.size() ];
    }
    }

    class WidgetDelegate extends Ui.BehaviorDelegate
    {
    hidden var mView;

    function initialize(view) {
    BehaviorDelegate.initialize();
    mView = view;
    }

    function onSelect() {
    mView.blink();
    return false;
    }

    function onBack() {
    return false;
    }
    }

    class WidgetApp extends App.AppBase {

    function initialize() {
    AppBase.initialize();
    }

    function getInitialView() {
    var view = new WidgetView();
    return [ view, new WidgetDelegate(view) ];
    }
    }
  • Is compiled into a class that looks like this...


    Hi Travis,

    you are amazing. Exactly what I needed. Thank you very much.
    BTW, is there a way for regular poor person like me to see the code generated by Rez?

    Thanks again,
    David
  • BTW, is there a way for regular poor person like me to see the code generated by Rez?

    You want me to give away all of my secrets? :P

    It is pretty easy if you compile via the command line. Here is a sample command line for building the Drawables sample program...

    C:\Development\Garmin\connectiq-sdk-win-1.2.4\samples\Drawable>monkeyc -w -o bin\Drawable.prg -m manifest.xml -z resources\strings.xml;resources\bitmaps.xml;resources\drawables\train.xml;resources\resources.xml source\DrawableView.mc source\DrawableApp.mc -d square_watch
    WARNING: The launcher icon 'LauncherIcon' (16x16) is smaller than the specified launcher icon size of the device 'square_watch' (61x43). Padding will be added to the image to center it.
    WARNING: The app name should reference a string resource using @Strings.AppName
    WARNING: The launcher icon should reference a bitmap resource using @Drawables.LauncherIcon
    WARNING: source\DrawableView.mc:18: Class 'MyWatchView' does not initialize its super class, 'View'
    WARNING: source\DrawableApp.mc:9: Class 'MyApp' does not initialize its super class, 'AppBase'

    C:\Development\Garmin\connectiq-sdk-win-1.2.4\samples\Drawable>


    If you add the -g option to the compile line, it will generate the equivalent of an assembler file with the commented out source code and write it to standard error. The following command line shows how to generate that output and to redirect the output to a file for viewing.

    C:\Development\Garmin\connectiq-sdk-win-1.2.4\samples\Drawable>monkeyc -w -g -o bin\Drawable.prg -m manifest.xml -z resources\strings.xml;resources\bitmaps.xml;resources\drawables\train.xml;resources\resources.xml source\DrawableView.mc source\DrawableApp.mc -d square_watch 2> bin\Drawable.asm
    WARNING: The launcher icon 'LauncherIcon' (16x16) is smaller than the specified launcher icon size of the device 'square_watch' (61x43). Padding will be added to the image to center it.
    WARNING: The app name should reference a string resource using @Strings.AppName
    WARNING: The launcher icon should reference a bitmap resource using @Drawables.LauncherIcon
    WARNING: source\DrawableView.mc:18: Class 'MyWatchView' does not initialize its super class, 'View'
    WARNING: source\DrawableApp.mc:9: Class 'MyApp' does not initialize its super class, 'AppBase'

    C:\Development\Garmin\connectiq-sdk-win-1.2.4\samples\Drawable>write bin\Drawable.asm


    If you inspect that file, you'll see stuff like this (this is the definition of the Rez.Drawables.train class draw(dc) method)...

    globals_Rez_Drawables_train_draw:
    incsp 4
    Rez_24_20_start:
    # varpX=0;
    Rez_25_3:
    Rez_25_7:
    ipush 0
    lputv 2
    # varpY=0;
    Rez_26_3:
    Rez_26_7:
    ipush 0
    lputv 3
    # varpWidth=dc.getWidth();
    Rez_27_3:
    Rez_27_7:
    lgetv 1
    spush getWidth
    getv
    frpush
    invokem 1
    lputv 4
    # varpHeight=dc.getHeight();
    Rez_28_3:
    Rez_28_7:
    lgetv 1
    spush getHeight
    getv
    frpush
    invokem 1
    lputv 5
    # drawComplex(dc,pX,pY,pWidth,pHeight);
    Rez_29_3:
    lgetv 0
    spush drawComplex
    getv
    frpush
    lgetv 1
    lgetv 2
    lgetv 3
    lgetv 4
    lgetv 5
    invokem 6
    popv
    Rez_24_20_stop:
    return
    globals_Rez_Drawables_train_draw_end:
  • Hi Travis, following your instructions, I've managed to change the width of my battery level indicator, based on the level reported by the system. However, it seems I'm unable to change the color of the battery level bar I'm using.

    Here's what I'm doing:

    (1) Define rectangle in drawables-list (note valueBattery and colorBattery variables):
    <drawable-list id="ui_elements">
    <shape type="rectangle" x="center" y="50" width="valueBattery" height="20" color="colorBattery" corner_radius="2"/>
    </drawable-list>


    (2) Define defaults in testView.mc:
    using Toybox.WatchUi as Ui;
    using Toybox.Graphics as Gfx;

    var valueBattery = 10;
    var colorBattery = Gfx.COLOR_WHITE;

    class testView extends Ui.DataField {

    function drawBattery(dc) {
    valueBattery = Sys.getSystemStats().battery;
    if (valueBattery <= 10) {
    colorBattery = Gfx.COLOR_RED;
    }
    }

    function onUpdate(dc) {
    drawBattery(dc);
    }
    }


    Unfortunately, I get the following error:
    BUILD: ERROR: Rez:31: extraneous input 'olorBattery' expecting {'&', '*', 'or', '[', '<', '!=', '<=', '<<', '%', ')', '|', ',', '-', 'and', '>>', '^', '.', '+', '&&', '||', '>', '==', '/', '>=', 'instanceof', 'has'}


    Any ideas what could be going wrong? It's a bit of a bare bones example, let me know if you're missing some info..

    Thanks in advance for your help!
  • It looks like this hack was broken at some point since last February. The programmer's guide says that the color attribute is expected to be a Graphics color constant or a 24-bit integer of the form 0xRRGGBB, so you'll have to do this another way. I'd suggest doing it with a font as described in this post.

    Travis
  • Hi Travis, thanks for your suggestion! I couldn't figure out how to make a font with custom glyphs, so I went a different route.

    I included the battery outline and levels as separate PNGs in my drawables.xml, then declare an array with references to the various drawables and use the system battery level to load the right battery level drawable.

    var batteryRez = [
    Rez.Drawables.batteryOutline,
    Rez.Drawables.batteryLevel1,
    etc...
    ]


    function drawBatteryStatus(dc) {
    var valueBattery = Math.round((Sys.getSystemStats().battery + 4) / 100 * 12).toNumber();

    var batteryOutline = Ui.loadResource( batteryRez[0] );
    var batteryFill = Ui.loadResource( batteryRez[valueBattery] );

    dc.drawBitmap( xM - 25, 240 - 24, batteryOutline );
    dc.drawBitmap( xM - 25, 240 - 24, batteryFill );
    }


    Works :) (at least in the simulator, have yet to receive my 935 ;) )
  • Why not just use dc.drawRectangle() to draw the battery outline, and dc.fillRectangle() to show the level? Or use the bitmap for the outline, and fillRectangle() for the levels?
  • Jim, you are a genius hahahaa. Don't know why I didn't think of that.. You live and you learn :)

    function drawBatteryStatus(dc) {
    valueBattery = Math.round((Sys.getSystemStats().battery) / 100 * 48).toNumber(); // 48 is pixel width of battery level

    var batteryOutline = Ui.loadResource( Rez.Drawables.batteryOutline );
    dc.drawBitmap( xM - 25, 240 - 24, batteryOutline );

    dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);
    if (valueBattery <= 10) {
    dc.setColor(Gfx.COLOR_RED, Gfx.COLOR_TRANSPARENT);
    }
    dc.fillRoundedRectangle( xM - 23, 240 - 22, valueBattery, 16, 1 );
    dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
    }


    Result: