スマホの解像度は様々であり、現在自分が使っているスマホの解像度はパソコンの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の仕様は次々変わる。特殊な使い方は避けた方が無難かもしれない。