How to format and display long text using Garmin ConnectIQ SDK

2.6k Views Asked by At

I'm trying to display a message which may be longer than screen width. What's the best way to format the message to display it as multiline?

Note, that the messages comes from server, so it's not a hardcoded resource string.

I cannot see any tools for that, and neither Toybox::WatchUi::Text drawable nor Dc.drawText seem to have support for paragraph formatting.

Dc.getTextDimensions allows to determine width and height of text, so this is potentially helpful, but native apps (e.g. message notifier) do display properly formatted paragraphs, so I have an impression I'm missing something.

5

There are 5 best solutions below

0
On BEST ANSWER

Since API Level 3.1.0 there is very appealing fitTextToArea() which can be used to get a text string to fit in a specified area.

Parameters:

  • text — (Lang.String) — The text to fit into the given area, which may include newlines
  • font — (Graphics.FontType) — The font to use when determining line break placement
  • width — (Lang.Number) — The width of the area to fit within
  • height — (Lang.Number) — The height of the area to fit within
  • truncate — (Lang.Boolean) — If true, the resulting string may be truncated to fit within the provided area using the provided font

Returns:

Returns a String suitable for display in the given area. The String will be truncated if the truncate parameter is true and the String cannot fit into the specified area. Otherwise, null will be returned.

For example:

//! Draw the item's label and bitmap
//! @param dc Device context
public function draw(dc as Dc) as Void {
    var font = Graphics.FONT_XTINY;

    dc.setColor(Graphics.COLOR_DK_GRAY, Graphics.COLOR_TRANSPARENT);
    dc.drawText(dc.getWidth() / 2,
                dc.getHeight() / 2,
                font,
                Graphics.fitTextToArea(_description, font, dc.getWidth()-10, dc.getHeight(), true),
                Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
}

Reference

0
On

I ended up using heuristics to calculate max number of characters that fit in the line, and wrapping line if necessary:

function formatText(dc, text, width, font) {
    // characters below worked better than "EeeTtaAooiNshRdlcum   "
    // which I used initially and is based on frequency of characters
    // in English words
    var chars = "AbCdEfGhIj";
    var oneCharWidth = dc.getTextWidthInPixels(chars, font) / chars.length();
    var charPerLine = mListWidth / oneCharWidth;
    if (text.length() > charPerLine) {
        var result = text.substring(0, charPerLine);
        result += "\n";
        result += text.substring(charPerLine, text.length());
        return [result, 0];
    } else {
        return [text, 1];
    }
}

The actual function in my open source IQ Connect Garminello application is here.

0
On

I came up with a pretty good recursive solution, which takes into account quite a few possibilities (having "\n" or " " in the string, and if a word is simply too long, it splits the word):

function convertTextToMultiline(dc, text){
var extraRoom = 0.8;
var oneCharWidth = dc.getTextWidthInPixels("EtaoiNshrd",Graphics.FONT_SMALL)/10;
var charPerLine = extraRoom * dc.getWidth()/oneCharWidth;
return convertTextToMultilineHelper(text, charPerLine);
}

function convertTextToMultilineHelper(text, charPerLine) {
    if (text.length() <= charPerLine) {
        return text;
    } else {
        var i = charPerLine + 1;
        for (; i >= 0; i--) {
            if (text.substring(i, i + 1).equals("\n")) {
                break;
            }
        }
        if (i >= 0) {
            var line = text.substring(0, i);
            var textLeft = text.substring(i + 1, text.length());
            var otherLines = convertTextToMultilineHelper(textLeft, charPerLine);
            return line + "\n" + otherLines;
        } else {
            var lastChar = charPerLine + 1;
            while (!(text.substring(lastChar, lastChar + 1).equals(" ") || text.substring(lastChar, lastChar + 1).equals("\n"))&& lastChar >= charPerLine/2) {
                lastChar--;
            }
            if (lastChar >= charPerLine/2) {
                var line = text.substring(0, lastChar + 1);
                var textLeft = text.substring(lastChar + 1, text.length());
                var otherLines = convertTextToMultilineHelper(textLeft, charPerLine);
                return line + "\n" + otherLines;
            } else {
                var line = text.substring(0, charPerLine) + "-";
                var textLeft = text.substring(charPerLine, text.length());
                var otherLines = convertTextToMultilineHelper(textLeft, charPerLine);
                return line + "\n" + otherLines;
            }
        }
    }
}

It might not be the prettiest or the most efficient solution, but it sure works!

0
On

I guess you have to do some manual calculation on the text width using https://developer.garmin.com/downloads/connect-iq/monkey-c/doc/Toybox/Graphics/Dc.html#getTextWidthInPixels-instance_method

I'm facing almost the same problem, although for my solution, I will cut off characters that doesn't fit into the screen width/textbox width

I'm making a utility function to calculate the string i can fit into a specific width that will look like this:

function getTextWidthMaxWidth(dc, text, font, maxWidth) {
    var dispText = text;
    //chunk off a char until it fits maxWidth
    while (dc.getTextWidthInPixels(dispText, font) > maxWidth) {
        dispText = dispText.subString(0, dispText.Length() - 1);
    }
    return dispText;
}
0
On

Nowadays this is what TextArea is for.

using Toybox.WatchUi;
using Toybox.Graphics as Gfx;


class ErrorView extends WatchUi.View {

    hidden var _message;
    hidden var _textArea;
    

    function initialize(message) {
        _message = message;
        View.initialize();
    }

    function onLayout(dc) {
        _textArea = new WatchUi.TextArea({
            :text=>_message.toString(),
            :color=>Graphics.COLOR_WHITE,
            :font=>[Graphics.FONT_XTINY],
            :locX =>WatchUi.LAYOUT_HALIGN_CENTER,
            :locY=>WatchUi.LAYOUT_VALIGN_CENTER,
            :width=>dc.getWidth()* 2/3,
            :height=>dc.getHeight() * 2/3
        });
        
    }
    
    function onUpdate(dc) {
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        dc.clear();
        _textArea.draw(dc);
    }

}