web-dev-qa-db-ja.com

Android:Html.TagHandlerの使用方法

Androidアプリケーションを構築しようとしています。投稿コンテンツの書式設定されたhtmlを表示するために、TextViewとHtml.fromHtml()メソッドを選択しました。残念ながら、いくつかのhtmlタグ。不明なタグは、TagHandlerを実装するクラスによって処理され、自分で生成する必要があります。

今、私はたくさんグーグルしました、そしてこのクラスがどのように働くべきであるかの例を見つけることができません。テキストに下線を引くためのuタグがあるとしましょう(これは非推奨ですが、何でもかまいません)。 TagHandlerはどのように見えますか?

これは次の方法で呼び出されます。

public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {

最初の2つの引数は問題ありません。私はoutput.append()を使用して出力を変更する必要があると思います。しかし、そこに下線が引かれたものをどのように添付しますか?

44
janoliver

だから、ようやく自分で考え出した。

public class MyHtmlTagHandler implements TagHandler {

    public void handleTag(boolean opening, String tag, Editable output,
            XMLReader xmlReader) {
        if(tag.equalsIgnoreCase("strike") || tag.equals("s")) {
            processStrike(opening, output);
        }
    }

    private void processStrike(boolean opening, Editable output) {
        int len = output.length();
        if(opening) {
            output.setSpan(new StrikethroughSpan(), len, len, Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, StrikethroughSpan.class);
            int where = output.getSpanStart(obj);

            output.removeSpan(obj);

            if (where != len) {
                output.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private Object getLast(Editable text, Class kind) {
        Object[] objs = text.getSpans(0, text.length(), kind);

        if (objs.length == 0) {
            return null;
        } else {
            for(int i = objs.length;i>0;i--) {
                if(text.getSpanFlags(objs[i-1]) == Spannable.SPAN_MARK_MARK) {
                    return objs[i-1];
                }
            }
            return null;
        }
    }


}

そして、TextViewの場合、次のように呼び出すことができます。

myTextView.setText (Html.fromHtml(text.toString(), null, new MyHtmlTagHandler()));

誰かがそれを必要とする場合。

乾杯

102
janoliver

このソリューションは Android sdk にあります

Android.text.html。行596-626。コピー/貼り付け

private static <T> Object getLast(Spanned text, Class<T> kind) {
    /*
     * This knows that the last returned object from getSpans()
     * will be the most recently added.
     */
    Object[] objs = text.getSpans(0, text.length(), kind);

    if (objs.length == 0) {
        return null;
    } else {
        return objs[objs.length - 1];
    }
}

private static void start(SpannableStringBuilder text, Object mark) {
    int len = text.length();
    text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
}

private static <T> void end(SpannableStringBuilder text, Class<T> kind,
                        Object repl) {
    int len = text.length();
    Object obj = getLast(text, kind);
    int where = text.getSpanStart(obj);

    text.removeSpan(obj);

    if (where != len) {
        text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
}

使用するには、次のようにTagHandlerをオーバーライドします。

public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {

    if(tag.equalsIgnoreCase("strike") || tag.equals("s")) {

        if(opening){
            start((SpannableStringBuilder) output, new Strike();

        } else {
            end((SpannableStringBuilder) output, Strike.class, new StrikethroughSpan());
        }
    }       
}

/* 
 * Notice this class. It doesn't really do anything when it spans over the text. 
 * The reason is we just need to distinguish what needs to be spanned, then on our closing
 * tag, we will apply the spannable. For each of your different spannables you implement, just 
 * create a class here. 
 */
 private static class Strike{}
12
Chad Bingham

私はジャノリバーの答えを受け取り、より多くのオプションをサポートしようとする私のバージョンを思いつきました

_        String text = ""; // HTML text to convert
        // Preprocessing phase to set up for HTML.fromHtml(...)
        text = text.replaceAll("<span style=\"(?:color: (#[a-fA-F\\d]{6})?; )?(?:font-family: (.*?); )?(?:font-size: (.*?);)? ?\">(.*?)</span>",
                               "<font color=\"$1\" face=\"$2\" size=\"$3\">$4</font>");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )face=\"'(.*?)', .*?\"", "face=\"$1\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"xx-small\"", "$1size=\"1\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"x-small\"", "$1size=\"2\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"small\"", "$1size=\"3\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"medium\"", "$1size=\"4\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"large\"", "$1size=\"5\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"x-large\"", "$1size=\"6\"");
        text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"xx-large\"", "$1size=\"7\"");
        text = text.replaceAll("<strong>(.*?)</strong>", "<_em>$1</_em>");  // we use strong for bold-face
        text = text.replaceAll("<em>(.*?)</em>", "<strong>$1</strong>");    // and em for italics
        text = text.replaceAll("<_em>(.*?)</_em>", "<em>$1</em>");          // but Android uses em for bold-face
        text = text.replaceAll("<span style=\"background-color: #([a-fA-F0-9]{6}).*?>(.*?)</span>", "<_$1>$2</_$1>");
        text_view.setText(Html.fromHtml(text, null, new Html.TagHandler() {
            private List<Object> _format_stack = new LinkedList<Object>();

            @Override
            public void handleTag(boolean open_tag, String tag, Editable output, XMLReader _) {
                if (tag.startsWith("ul"))
                    processBullet(open_tag, output);
                else if (tag.matches(".[a-fA-F0-9]{6}"))
                    processBackgroundColor(open_tag, output, tag.substring(1));
            }

            private void processBullet(boolean open_tag, Editable output) {
                final int length = output.length();
                if (open_tag) {
                    final Object format = new BulletSpan(BulletSpan.STANDARD_GAP_WIDTH);
                    _format_stack.add(format);
                    output.setSpan(format, length, length, Spanned.SPAN_MARK_MARK);
                } else {
                    applySpan(output, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }

            private void processBackgroundColor(boolean open_tag, Editable output, String color) {
                final int length = output.length();
                if (open_tag) {
                    final Object format = new BackgroundColorSpan(Color.parseColor('#' + color));
                    _format_stack.add(format);
                    output.setSpan(format, length, length, Spanned.SPAN_MARK_MARK);
                } else {
                    applySpan(output, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }

            private Object getLast(Editable text, Class kind) {
                @SuppressWarnings("unchecked")
                final Object[] spans = text.getSpans(0, text.length(), kind);

                if (spans.length != 0)
                    for (int i = spans.length; i > 0; i--)
                        if (text.getSpanFlags(spans[i-1]) == Spannable.SPAN_MARK_MARK)
                            return spans[i-1];

                return null;
            }

            private void applySpan(Editable output, int length, int flags) {
                if (_format_stack.isEmpty()) return;

                final Object format = _format_stack.remove(0);
                final Object span = getLast(output, format.getClass());
                final int where = output.getSpanStart(span);

                output.removeSpan(span);

                if (where != length)
                    output.setSpan(format, where, length, flags);
            }
        }));
_

これは、箇条書き、前景色、および背景色を取得するようです。 font-faceでも機能する可能性がありますが、AndroidはDroid/Roboto以外のフォントをサポートしているようです)。

これは概念実証に近いため、正規表現をString処理に変換することをお勧めします。これは、正規表現が前処理の組み合わせをサポートしていないためです。つまり、これは、 String。これもフォントサイズを変更できないようです。「16sp」、「medium」、「4」のように定義して、変更を確認しませんでした。誰かがうまく機能するようになったら、共有を気にしますか?

私は現在、これに番号付き/番号付きリストのサポートを追加できるようにしたいと考えています。

  1. 項目
  2. 項目
  3. 項目

注:いずれかで始まる人にとって、handleTag(...)に指定されている「タグ」は、タグの名前(「スパン」など)にすぎないようです。タグに割り当てられている属性が含まれていない場合( "がある場合など)、背景色の抜け穴を確認できます。

4
Dandre Allison

私たちはこのライブラリを内部で開発してきました https://github.com/square1-io/rich-text-Android しばらくの間、多くのコンテンツ集約型のニュースアプリで使用しています。

ライブラリは、ビデオやimgなどの最も一般的なhtmlタグを画像のリモートダウンロードで解析できます。カスタムビューであるRichTextViewをTextViewの代わりとして使用して、解析されたコンテンツを表示できます。

最近公開しましたので、ドキュメントはまだ不完全ですが、提供されている例を参考にして、ニーズに合っているかどうかを確認してください。

2
Roberto Prato

Html.Java APIではそれを見ることができますが、スタイルとテキスト配置は<p><div>などのタグで使用できるはずです。<p align="center">で機能しないまたは<p style="text-align: center">および他の多くのバリアント。このテキストの中央揃え、およびフォントサイズ、ttfファイルからの複数のフォントフェイス、背景色などの他のスタイルを実行できないため、TextViewに基づいて独自のhtmlTextViewを作成しましたが、独自のtagHandlerクラスを使用しています。 1つまたは2つの小さな刺激が与えられた場合、ほとんどのタグは問題ありませんが、私のカスタム配置タグはleftcentrerightは特別な条件(私が理解していない)でのみ機能し、それ以外は機能しません。彼らは機能しないか、アプリをクラッシュさせません!これは私の配置タグハンドルです。他のすべてのカスタムタグハンドラと同じ構造ですが、動作がおかしいです。私のタグハンドラーの基本的な形式は同じであり、が私によって考案されていません!何時間もウェブで検索した後、タグハンドラテンプレートを見つけました。それを投稿してくれた人には感謝しますが、私の記憶力と組織力は誰がどこにいるのか本当に思い出せないほどなので、このコードを自分のものだと認識したら教えてください。私が持っている唯一のリンク(ここにあります)は私のコードにあります: stackoverflow:Android:Html.TagHandlerの使用方法?

  private void ProcessAlignment(Layout.Alignment align, boolean opening, Editable output) {
    int len = output.length();
    if (opening) {
        output.setSpan(new AlignmentSpan.Standard(align), len, len, Spannable.SPAN_MARK_MARK);
    } else {
        Object obj = getLast(output, AlignmentSpan.Standard.class);
        int where = output.getSpanStart(obj);

        output.removeSpan(obj);

        if (where != len) {
            output.setSpan(new AlignmentSpan.Standard(align), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
}

問題は、終了タグが正しい開始タグに接続されていないことだと思います。

private Object getLast(Editable text, Class kind) {
            Object[] objs = text.getSpans(0, text.length(), kind);

            if (objs.length == 0) {
                return null;
            } else {
                for (int i = objs.length - 1; i >= 0; --i) {
                    if (text.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK) {
                        return objs[i];
                    }
                }
                return null;
            }
        }

これは全体のクラスであり、何かが正しくありません。最大の要素は私の理解です!多分誰かが私がよりよく理解するのを助けることができます...

public class htmlTextView extends AppCompatTextView {
static Typeface mLogo;
static Typeface mGAMZ;
static Typeface mChalk;
static Typeface mSouvenir;
int GS_PAINTFLAGS = FILTER_BITMAP_FLAG | ANTI_ALIAS_FLAG | SUBPIXEL_TEXT_FLAG | HINTING_ON;

public htmlTextView(Context context) {
    super(context);
    initialise();
}

public htmlTextView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    initialise();
}

public htmlTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initialise();
}


private void initialise() {
    mLogo = Typeface.createFromAsset(theAssetManager, "fonts/logo.ttf");
    mGAMZ = Typeface.createFromAsset(theAssetManager, "fonts/GAMZ One.ttf");
    mChalk = Typeface.createFromAsset(theAssetManager, "fonts/swapfix.ttf");
    mSouvenir = Typeface.createFromAsset(theAssetManager, "fonts/Souvenir Regular.ttf");

    setPaintFlags(GS_PAINTFLAGS);
}

public void setDefaultTypefaceSouvenir() {
    setTypeface(mSouvenir);
}

public void setDefaultTypefaceGAMZ() {
    setTypeface(mGAMZ);
}

public void setDefaultTypefaceChalk() {
    setTypeface(mChalk);
}

/*public myTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}*/

public void setHTML(String htmltext) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // Nougat API 24
        setText(Html.fromHtml(htmltext, Html.FROM_HTML_MODE_LEGACY,
                null, new TypefaceTagHandler()));
    } else {
        setText(Html.fromHtml(htmltext, null, new TypefaceTagHandler()));
    }
}

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
}

@Override
public Bitmap getDrawingCache(boolean autoScale) {
    return super.getDrawingCache(autoScale);
}

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
}

// http://stackoverflow.com/questions/4044509/Android-how-to-use-the-html-taghandler
private static class TypefaceTagHandler implements Html.TagHandler {


    private void ProcessAlignment(Layout.Alignment align, boolean opening, Editable output) {
        int len = output.length();
        if (opening) {
            output.setSpan(new AlignmentSpan.Standard(align), len, len, Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, AlignmentSpan.Standard.class);
            int where = output.getSpanStart(obj);

            output.removeSpan(obj);

            if (where != len) {
                output.setSpan(new AlignmentSpan.Standard(align), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private void ProcessTypefaceTag(Typeface tf, boolean opening, Editable output) {
        int len = output.length();
        if (opening) {
            output.setSpan(new CustomTypefaceSpan("", tf), len, len,
                    Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, CustomTypefaceSpan.class);
            int where = output.getSpanStart(obj);

            output.removeSpan(obj);

            if (where != len) {
                output.setSpan(new CustomTypefaceSpan("", tf), where, len,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private void ProcessScaleTag(float scalefactor, boolean opening, Editable output) {
        int len = output.length();
        if (opening) {
            output.setSpan(new RelativeSizeSpan(scalefactor), len, len,
                    Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, RelativeSizeSpan.class);
            int where = output.getSpanStart(obj);

            output.removeSpan(obj);

            if (where != len) {
                output.setSpan(new RelativeSizeSpan(scalefactor), where, len,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private void ProcessBox(int colour, boolean opening, Editable output) {
        int len = output.length();
        if (opening) {
            output.setSpan(new BackgroundColorSpan(colour), len, len,
                    Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, BackgroundColorSpan.class);
            int where = output.getSpanStart(obj);

            output.removeSpan(obj);

            if (where != len) {
                output.setSpan(new BackgroundColorSpan(colour), where, len,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    private void ProcessTextColour(int colour, boolean opening, Editable output) {
        int len = output.length();
        if (opening) {
            output.setSpan(new ForegroundColorSpan(colour), len, len,
                    Spannable.SPAN_MARK_MARK);
        } else {
            Object obj = getLast(output, ForegroundColorSpan.class);
            int where = output.getSpanStart(obj);

            output.removeSpan(obj);

            if (where != len) {
                output.setSpan(new ForegroundColorSpan(colour), where, len,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

    final HashMap<String, String> attributes = new HashMap<>();

    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
        String Attr = "";
        //if (!opening) attributes.clear();
        processAttributes(xmlReader);

        if ("txt".equalsIgnoreCase(tag)) {
            Attr = attributes.get("clr");
            System.out.println("clr Attr: " + Attr + ", opening: " + opening);
            if (Attr == null || Attr.isEmpty()
                    || "black".equalsIgnoreCase(Attr)
                    || Attr.charAt(0) == 'k') {
                System.out.println("did black, opening: " + opening);
                ProcessTextColour(parseColor("#000000"), opening, output);
            } else {
                if (Attr.equalsIgnoreCase("g")) {
                    ProcessTextColour(parseColor("#b2b3b3"), opening, output);
                } else {
                    System.out.println("did colour, opening: " + opening);
                    ProcessTextColour(parseColor(Attr), opening, output);
                }
            }
            return;
        }

        if ("box".equalsIgnoreCase(tag)) {
            ProcessBox(parseColor("#d7d6d5"), opening, output);
            return;
        }


        if ("scl".equalsIgnoreCase(tag)) {
            Attr = attributes.get("fac");
            System.out.println("scl Attr: " + Attr);
            if (Attr != null && !Attr.isEmpty()) {
                ProcessScaleTag(parseFloat(Attr), opening, output);
            }
            return;
        }

        if ("left".equalsIgnoreCase(tag)) {
            ProcessAlignment(Layout.Alignment.ALIGN_NORMAL, opening, output);
            return;
        }

        if ("centre".equalsIgnoreCase(tag)) {
            ProcessAlignment(Layout.Alignment.ALIGN_CENTER, opening, output);
            return;
        }

        if ("right".equalsIgnoreCase(tag)) {
            ProcessAlignment(Layout.Alignment.ALIGN_OPPOSITE, opening, output);
            return;
        }

        if ("logo".equalsIgnoreCase(tag)) {
            ProcessTypefaceTag(mLogo, opening, output);
            return;
        }
        if ("gamz".equalsIgnoreCase(tag)) {
            ProcessTypefaceTag(mGAMZ, opening, output);
            return;
        }

        if ("chalk".equalsIgnoreCase(tag)) {
            System.out.println("chalk " + (opening ? "opening" : "closing"));
            ProcessTypefaceTag(mChalk, opening, output);
            return;
        }
    }

    private Object getLast(Editable text, Class kind) {
        Object[] objs = text.getSpans(0, text.length(), kind);

        if (objs.length == 0) {
            return null;
        } else {
            for (int i = objs.length - 1; i >= 0; --i) {
                if (text.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK) {
                    return objs[i];
                }
            }
            return null;
        }
    }

    private void processAttributes(final XMLReader xmlReader) {
        try {
            Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
            elementField.setAccessible(true);
            Object element = elementField.get(xmlReader);
            Field attsField = element.getClass().getDeclaredField("theAtts");
            attsField.setAccessible(true);
            Object atts = attsField.get(element);
            Field dataField = atts.getClass().getDeclaredField("data");
            dataField.setAccessible(true);
            String[] data = (String[])dataField.get(atts);
            Field lengthField = atts.getClass().getDeclaredField("length");
            lengthField.setAccessible(true);
            int len = (Integer)lengthField.get(atts);

            /**
             * MSH: Look for supported attributes and add to hash map.
             * This is as tight as things can get :)
             * The data index is "just" where the keys and values are stored.
             */
            for(int i = 0; i < len; i++)
                attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
        }
        catch (Exception e) {
            Log.d(TAG, "Exception: " + e);
        }
    }

}

private static class CustomTypefaceSpan extends TypefaceSpan {
    private final Typeface newType;

    public CustomTypefaceSpan(String family, Typeface type) {
        super(family);
        newType = type;
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        applyCustomTypeFace(ds, newType);
    }

    @Override
    public void updateMeasureState(TextPaint Paint) {
        applyCustomTypeFace(Paint, newType);
    }

    private void applyCustomTypeFace(Paint paint, Typeface tf) {
        int oldStyle;
        Typeface old = Paint.getTypeface();
        if (old == null) {
            oldStyle = 0;
        } else {
            oldStyle = old.getStyle();
        }

        int fake = oldStyle & ~tf.getStyle();
        if ((fake & Typeface.BOLD) != 0) {
            Paint.setFakeBoldText(true);
        }

        if ((fake & Typeface.ITALIC) != 0) {
            Paint.setTextSkewX(-0.25f);
        }
        Paint.setTypeface(tf);
    }
}

}

HtmlTextViewは、アクティビティから次のように作成されます。

 protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    theAssetManager = getAssets();
    htmlTextView tv = new htmlTextView(this);
    tv.setDefaultTypefaceSouvenir();
    tv.setTextColor(BLACK);
    tv.setBackgroundColor(0xfff0f0f0);
    tv.setPadding(4, 4, 4, 4);
    tv.setTextSize(30);
    tv.setMovementMethod(new ScrollingMovementMethod());
    tv.setHTML(getString(R.string.htmljumblies));
    //tv.setHTML(getString(R.string.htmltest));
    RelativeLayout rl = (RelativeLayout) findViewById(R.id.rl);
    rl.addView(tv);
}

htmljumbliesは、strings.xmlで次のように定義されています。この特定のバージョンではアプリがクラッシュしますが、最初の<centre></centre>タグが7行目と9行目から削除された場合The Jumblies集中的に表示されますか?混乱してイライラする!それらを保持し、<centre></centre>タグの折りたたみThe Jumbliesを削除すると、何も起こりません。ヘッダーの行が中央揃えではありません!

    <string name="htmljumblies">
    <![CDATA[&DoubleLongRightArrow;<logo><scl fac="1.1"><font color="#e5053a">GAMZ</font></scl></logo>
        <chalk><scl fac="1.8"> SWAP </scl></chalk>
        <scl fac="1.00">Set <b>1</b>, Game <b>1</b></scl>
        <br>
        <centre>
        <gamz><font color="#e5053a"><scl fac="1.50">a</scl></font><scl fac="0.90">(9)</scl></gamz>, <gamz><font color="#00a3dd"><scl fac="1.50">e</scl></font><scl fac="0.90">(8)</scl></gamz>, <gamz><font color="#fba311"><scl fac="1.50">i</scl></font><scl fac="0.90">(8)</scl></gamz>, <gamz><font color="#bc5e1e"><scl fac="1.50">o</scl></font><scl fac="0.90">(8)</scl></gamz>, <gamz><font color="#bf30b5"><scl fac="1.50">u</scl></font><scl fac="0.90">(9)</scl></gamz>
        </centre>
        <br>
        This is an example of my custom <b>htmlTextView</b> drawn from HTML format
        text with custom tags to use custom fonts, colouring typeface sizing and highlight boxes.
        The default font is <b><i>Souvenir</i></b>, but 3 other fonts are used:<br>
        The <font color="#e5053a"><b><logo><scl fac="1.1">GAMZ</scl></logo></b></font>
        <font color="#000080"><gamz><scl fac="0.8"><box>letter</box>
        <box>fonts</box><sc></gamz></font>
        and <chalk><scl fac="1.8">swapfix</scl></chalk>, essentially
        <chalk><scl fac="0.9">Staccato 555</scl></chalk>,
        as used in the words <chalk><scl fac="1.2">SWAP</scl></chalk> and
        <chalk><scl fac="1.2">FIX</scl></chalk>
        on the <font color="#e5053a"><b><logo><scl fac="1.1">GAMZ</scl></logo></b></font>
        boxes.
        <br>
        <centre>
        <scl fac="2"><box><b> <u>The Jumblies</u> </b></box></scl><br>
        <font color="#0000ff">
        They went to sea in a Sieve, they did,<br>
        In a Sieve they went to sea:<br>
        In spite of all their friends could say,<br>
        On a winter\'s morn, on a stormy day,<br>
        In a Sieve they went to sea!<br>
        And when the Sieve turned round and round,<br>
        And every one cried, \'You\'ll all be drowned!\'<br>
        They called aloud, \'Our Sieve ain\'t big,<br>
        But we don\'t care a button! we don\'t care a fig!<br>
        In a Sieve we\'ll go to sea!\'<br>
        Far and few, far and few,<br>
        Are the lands where the Jumblies live;<br>
        Their heads are green, and their hands are blue,<br>
        And they went to sea in a Sieve.<br>
        <br>
        They sailed away in a Sieve, they did,<br>
        In a Sieve they sailed so fast,<br>
        With only a beautiful pea-green veil<br>
        Tied with a riband by way of a sail,<br>
        To a small tobacco-pipe mast;<br>
        And every one said, who saw them go,<br>
        \'O won\'t they be soon upset, you know!<br>
        For the sky is dark, and the voyage is long,<br>
        And happen what may, it\'s extremely wrong<br>
        In a Sieve to sail so fast!\'<br>
        Far and few, far and few,<br>
        Are the lands where the Jumblies live;<br>
        Their heads are green, and their hands are blue,<br>
        And they went to sea in a Sieve.<br>
        <br>
        The water it soon came in, it did,<br>
        The water it soon came in;<br>
        So to keep them dry, they wrapped their feet<br>
        In a pinky paper all folded neat,<br>
        And they fastened it down with a pin.<br>
        And they passed the night in a crockery-jar,<br>
        And each of them said, \'How wise we are!<br>
        Though the sky be dark, and the voyage be long,<br>
        Yet we never can think we were rash or wrong,<br>
        While round in our Sieve we spin!\'<br>
        Far and few, far and few,<br>
        Are the lands where the Jumblies live;<br>
        Their heads are green, and their hands are blue,<br>
        And they went to sea in a Sieve.<br>
        <br>
        And all night long they sailed away;<br>
        And when the Sun went down,<br>
        They whistled and warbled a moony song<br>
        To the echoing sound of a coppery gong,<br>
        In the shade of the mountains brown.<br>
        \'O Timballo! How happy we are,<br>
        When we live in a Sieve and a crockery-jar,<br>
        And all night long in the moonlight pale,<br>
        We sail away with a pea-green sail,<br>
        In the shade of the mountains brown!\'<br>
        Far and few, far and few,<br>
        Are the lands where the Jumblies live;<br>
        Their heads are green, and their hands are blue,<br>
        And they went to sea in a Sieve.<br>
        <br>
        They sailed to the Western Sea, they did,<br>
        To a land all covered with trees,<br>
        And they bought an Owl, and a useful Cart,<br>
        And a pound of Rice, and a Cranberry Tart,<br>
        And a Hive of silvery Bees.<br>
        And they bought a Pig, and some green Jack-daws,<br>
        And a lovely Monkey with Lollipop paws,<br>
        And forty bottles of Ring-Bo-Ree,<br>
        And no end of Stilton Cheese.<br>
        Far and few, far and few,<br>
        Are the lands where the Jumblies live;<br>
        Their heads are green, and their hands are blue,<br>
        And they went to sea in a Sieve.<br>
        <br>
        And in twenty years they all came back,<br>
        In twenty years or more,<br>
        And every one said, \'How tall they\'ve grown!<br>
        For they\'ve been to the Lakes, and the Torrible Zone,<br>
        And the hills of the Chankly Bore!\'<br>
        And they drank their health, and gave them a feast<br>
        Of dumplings made of beautiful yeast;<br>
        And every one said, \'If we only live,<br>
        We too will go to sea in a Sieve,---<br>
        To the hills of the Chankly Bore!\'<br>
        Far and few, far and few,<br>
        Are the lands where the Jumblies live;<br>
        Their heads are green, and their hands are blue,<br>
        And they went to sea in a Sieve.</centre></font>
    ]]>
</string>
1
JosieH