スマホの解像度は様々であり、現在自分が使っているスマホの解像度はパソコンの3倍程度である。 文字サイズはパソコンと同等とするために、このページのプログラムではレンダリング規則で与えた サイズを3倍にしている。 Canvas#drawTextの文字サイズは引数のPaintインスタンスで与えるが、このサイズの単位はピクセル のため、こうしている。
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); } }
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 に任せる。
記事[2]の方法を使えば、TextViewを画面に配置せず、Bitmap として取り出せそうである。
Androidの仕様は次々変わる。特殊な使い方は避けた方が無難かもしれない。