トップAndroid Java > 文字列の描画

文字列の描画

文字フォントサイズの単位

スマホの解像度は様々であり、現在自分が使っているスマホの解像度はパソコンの3倍程度である。 文字サイズはパソコンと同等とするために、このページのプログラムではレンダリング規則で与えた サイズを3倍にしている。 Canvas#drawTextの文字サイズは引数のPaintインスタンスで与えるが、このサイズの単位はピクセル のため、こうしている。

Canvasに文字列を複数行に分けて描画する

Android Canvas の drawText() にはセンタリングなどの位置指定機能はない。 利用者がフォントのサイズ、文字列幅から位置を自力で計算する必要がある[1]。

一行ならまだしも、複数行になると面倒である。

横方向(幅)は簡単であるが、縦方向がややこしい。 記事[1]が詳しく、分かりやすい。

   Paint.FontMetrics fontMetrics = paint_.getFontMetrics();
   // fontMetrics.top      文字空間の上限 負
   // fontMetrics.ascent   文字の上限      負
   // fontMetrics.leading 文字の基準位置  0
   // fontMetrics.descent 文字の下限   正
   // fontMetrics.bottom   文字空間の下限 正

適当な行間も必要であるから、文字列の高さは fontMetrics.bottom - fontMetrics.top としておき、 必要に応じてこれを増減する。

問題は、drawText() に与える座標である。 記事[1]によれば、 1行の文字列では 文字列の中心座標を (x, y) としたとき drawText に与える y座標は

   y_ -= (_fontMetrics.ascent+_fontMetrics.descent) / 2;
となる。

一行の文字列描画はそれほど複雑ではないが、複数行になる場合は結構面倒である。 以下のプログラムで所望の結果が得られることを確認した。

Wrapメソッドにも行数がかかっているので、楽な方法ではない。

現在、Fact は 3.0 にしている。恐らく、スマホの解像度がパソコンの3倍であることによるであろう。 スマホ専用のプログラムであるから、この Fact は廃止して、プログラムを見直す。

解像度の違いは引数 dy、halo にも関係する。 SP (Scale-independent Pixels)を使った場合、パソコンに近いかどうかは未検討である。 いずれにせよ、レンダリング規則で与える数値はパソコンと同じとする。

    void drawText(Canvas canvas, Paint paint, TileToRender tile,
                  String text, float dy, float halo, float Fact, boolean fRect) {
        if (text == null) return;

        boolean simple = (dy == 0 && text.length() <= 4);
        String[] texts = Wrap(text).split("\n");
        int lines = texts.length;
        float w = 0;
        for (String s : texts) {
            float wi = paint.measureText(s);
            if (wi > w) w = wi;
        }   // 一番長い行の幅を求める
        Paint.FontMetrics fm = paint.getFontMetrics();
        float h = fm.bottom - fm.top;
        float ad = (fm.ascent + fm.descent) / 2;
        float hl = h * lines;

        double fact = tile.fact;
        double xpx = tile.xpx;
        double ypx = tile.ypx;
        float x = (float) (xc * fact - xpx);
        float y = (float) (yc * fact - ypx);

        int num_rects = tile.num_rects;
        if (fRect && num_rects < tile.rects.length - 1) {
            Rect rect = tile.rects[num_rects];
            if (simple) {
                rect.set((int) (x - w * Fact / 2), (int) (y - h * Fact / 2),
                        (int) (x + w * Fact / 2), (int) (y + h * Fact / 2));
            } else {
                int y1 = (int) (y - hl * Fact / 2);
                if (dy > 0) y1 += hl / 2 + dy;
                rect.set((int) (x - w * Fact / 2), y1,
                        (int) (x + w * Fact / 2), (int) (y1 + hl * Fact));
            }
            for (int n = 0; n < num_rects; n++) {
                if (Rect.intersects(rect, tile.rects[n])) {
                    return; // 重複するため描画しない
                }
            }
            tile.num_rects = num_rects + 1;   // 追加登録
        }

        for (int i = 0; i < lines; i++) {
            float yy = y - ad - h * (lines / 2) + (lines % 2 == 0 ? h / 2 : 0) + h * i;
            if (dy > 0) yy += hl / 2 + dy;
            if (halo > 0) {
                Paint paintHalo = tile.paintHalo;
                paintHalo.setStrokeWidth(halo * 2);                // 描画の幅
                paintHalo.setTextSize(paint.getTextSize());        // テキストサイズ
                // 縁取りを先に描画
                canvas.drawText(texts[i], x - w/2, yy, paintHalo);
            }
            canvas.drawText(texts[i], x - w / 2, yy, paint);
        }
    }

Wrapメソッド

    String Wrap(String text) {
        final int maxWidth = 4;   // 4文字
        if (text.length() <= maxWidth) return text;
        StringBuilder sb = new StringBuilder(text);
        int length = sb.length();
        int last = 0;
        for (int ix = 0; ix < length; ) {
            char prevChar = '\0';
            while (ix < length) {
                char currChar = sb.charAt(ix);
                char nextChar = ix+1 < length ? sb.charAt(ix+1) : '\0';
                if ((currChar == ' ' || currChar == ' ') &&
                        (prevChar == '\0' || isNotASCII(prevChar)) &&
                        (nextChar == '\0' || isNotASCII(nextChar)) ) {
                    sb.deleteCharAt(ix);
                    length = sb.length();
                    continue;   // スペースを無視する
                }
                ix++;
                if (currChar == '\n')  break;
                if ((ix >= length || ix-last >= maxWidth) && canLineBreak(currChar, nextChar)) {
                    sb.insert(ix, '\n');
                    length = sb.length();
                    ix++;
                    break;
                }
                prevChar = currChar;
            }
            last = ix;
        }
        return sb.toString();
    }

    boolean canLineBreak(char prevChar, char nextChar) {
        if (nextChar==' ' || nextChar==' ' || nextChar=='\0' || nextChar=='\n') {
            return true;
        }
        if (nextChar==')' || nextChar==')' || nextChar=='」') {
            return false;
        }
        return isNotASCII(prevChar) || isNotASCII(nextChar);  // 幅1文字が連続している場合
    }

    boolean isNotASCII(char c) {
        return (c >= 128);
    }

TextViewで複数行表示させて、ViewからBitmapを生成する

折れ曲がり表示(複数行表示)を TextView に任せる。

記事[2]の方法を使えば、TextViewを画面に配置せず、Bitmap として取り出せそうである。

Androidの仕様は次々変わる。特殊な使い方は避けた方が無難かもしれない。

リファレンス

[1] AndroidのCanvas文字列センタリング
[2] 画面に配置せずにViewからBitmapを生成する
[3] Create a multiline TextView in Android application
[4] Android、日本語テキストのサイズに注意