AndroidJava/文章を縦書き、横書きする

Canvasに文字列を縦書きする(2023-07-09)


縦書きは、やってやれないことはない、けど、わりと大変かもしれません。

まず、実例から。
canvas に text文字列を縦書きします。

static void drawTateText(Canvas canvas, String text){
    Paint paint = new Paint();
    float size = canvas.getWidth()/20f; // 画面に20行くらい入る大きさ
    paint.setTextSize(size);
    float start_x = canvas.getWidth()-size*5f; // 開始位置 x 座標(てきとう)
    float start_y = size*5f; // 開始位置 y 座標(おなじく)
    float x = start_x;
    float y = start_y;
    
    for(int i=1; i<=text.length(); i++){
        String c = text.substring(i-1, i); // 1文字ずつ取り出す
        float w = paint.measureText(c);   // 文字の横幅
        switch(c){
            // 文字ごとに処理を変える必要がある
            // 詳細については後述
            case "\n": // 改行の場合
                x -= size;   // 左に行送りして、
                y = start_y; // 先頭に戻す。
                continue;
        }
        canvas.drawText(c, x - w/2f, y, paint); // 横軸を中央ぞろえ
        y += size; // 縦位置を次の文字位置へ移動
    }
}


これを適当な画面にぶっこみます。
ここでは例として、AndroidStudio の新規プロジェクトで Enmty Views Activity をベースに作りましたが、Canvas さえあればばなんでも良いです。

public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       //setContentView(R.layout.activity_main); // 使わない
       MyView view = new MyView(this);
       setContentView(view);
   }
    
   static class MyView extends View {
       public MyView(Context context){ super(context); }
   
       protected void onDraw(Canvas canvas){
           canvas.drawColor(Color.WHITE); // 背景色は白にする
           drawTateText(canvas, "古池や\n岩に染み入る\n蝉の声"); // ここ
       }
   }

その結果は以下、

縦書きした例その1

ポイントとして、一文字ごとの縦幅は一定です。
ここでは paint.setTextSize() した値で計算していますが、
paint.getFontMetrics().top および bottom などから計算しても構いません。*1

そして、文字の横幅は可変です。
日本語よりも、英語の i と w の方が横幅の違いが分かると思います。

もちろん横幅の方も、
Typeface.MONOSPACE みたいに固定幅でそろえれば同じになりますが。
(テキストエディタやメモ帳とかは同じ横幅にする場合が多い)

それを踏まえて文字を送ったり、中心にそろえたりを計算すれば縦書きはできます。

が、本題はここからです。


drawTateText(canvas, "「すたーと」\n(Start)\nlong sample!");

これを出力した結果は以下。

縦書きした例その2

記号がそのままでは使えないのはひと目で分かりますが、
実は英語も、特に小文字の場合は幅がかなり不自然になります。 (上の例だとsample の p と l の間とか)。

そもそも英語は横書きしかしない言語ですよね、たぶん。
(というより日本語の場合は横でも縦でも違和感がないように、
 デザイナーの皆様がフォントを作っているから、うまく表示できるわけです)

「ー」くらいなら、そこだけ「丨(たてぼう)」に String.replace() で置換すればごまかせるかもしれません。
ですが、文字の種類によってはやはり縦書きは難しい、というのが結論です。

以上です。




さて、それを踏まえても意地でも縦書きしなければならない、
のっぴきならない事情を抱えた皆様に、もう少し踏み込んだ話を続けます。


まず、英語から縦書き。

英語にせよ記号にせよ前述の例、
c = text.substring(i-1, i) から switch(c) したように
文字種類ごとに処理を分ける必要があります。

そして、文字によっては
drawText(c, x - w/2f, y, paint) の y にさらに加えて +y1 か -y1 する。
y += size のときに +y2 か -y2 みたいにする必要があるわけです。

それぞれの文字に合わせて座標を微調整する必要があるのです。
・・・わざわざ?

とはいえ、
英語の場合はたかだか 26 文字(大文字小文字で52文字)しかありませんし、
もっと言えば、ざっくりと3通りくらいに分かれるんじゃないでしょうか。

ためしに実際に abcdefghijklmnopqrstuvwxyz と並べてみてください。

  • A群-上にはみ出る: bdfhijklt
  • B群-下にはみ出る: gjpqy
  • C群-上にも下にも、はみ出ない

こんなふうに文字の種類ごとに移動する量を微調整すれば、
縦書きでも、わりとそれなりの見た目にできます。

つぎに、記号

結論から言えば、右に90度回転させて、
さらに(英語と同様に)座標を微調整すれば、それっぽくなります。
"(" とか、"ー" とか、90度回転させれば縦書き用に使えそうですよね?

では、そもそも文字をどうやって回転させるのか?
それには Path と canvas.drawTextOnPath() を使います

static void draw4lines(Canvas canvas){
    // 合計4本、線を引きます
    Paint[] paint = new Paint[4];
    for(int i=0; i<paint.length; i++){
        paint[i] = new Paint();
        paint[i].setTextSize(canvas.getWidth()/20f);
        paint[i].setStrokeWidth(canvas.getWidth()/200f);
    }
    Path[] path = new Path[4];
    for(int i=0; i<path.length; i++){
        path[i] = new Path();
    }
    
    float MAX = 1000; // 適当な数字
    // 黒い線を左上から右上まで引く
    paint[0].setColor(Color.BLACK);
    path[0].moveTo(200, 100);
    path[0].lineTo(MAX-100, 100);
    
    // 赤い線を、右上から右下まで引く
    paint[1].setColor(Color.RED);
    path[1].moveTo(MAX-100, 200);
    path[1].lineTo(MAX-100, MAX-100);
    
    // 緑の線を、右下から左下まで引く
    paint[2].setColor(Color.GREEN);
    path[2].moveTo(MAX-200, MAX-100);
    path[2].lineTo(100, MAX-100);
    
    // 青の線を、左下から左上まで引く
    paint[3].setColor(Color.BLUE);
    path[3].moveTo(100, MAX-200);
    path[3].lineTo(100, 100);
    
    for(int i=0; i<path.length; i++){
        // 線を引く
        paint[i].setStyle(Paint.Style.STROKE);
        canvas.drawPath(path[i], paint[i]);
        // その線の上に、文字を重ねる
        paint[i].setStyle(Paint.Style.FILL);
        canvas.drawTextOnPath(i + ".文字列onPath", path[i], 0, 0, paint[i]);
    }
}

さきほどの drawTateText と 今回の draw4lines に置き換えて、
結果は以下。

いろんな方向で書いた例

色ごとの文字列に注目してください。
赤の文字列は90度回転していますよね?

文字列は横でも縦でも斜めでも、drawTextOnPath を使って書くことができます。

なので、さきほどの縦書きに話を戻して、
一文字ずつ canvas.drawText していたところを
一文字ずつ canvas.drawTextOnPath に変えれば
文字に合わせて回転したりしなかったりが実現できます。

前述の縦書きサンプルの switch(c) で90度回転が必要か判定して、
drawTextOnPathで書いて、
y += size + y1 を、文字に合わせてy1を微調整する、という流れです。


注意点として、
drawTextOnPath で使う path が文字幅よりも短かった場合、
(lineTo する長さが paint.measureText より少なかった場合)
文字列が途切れて表示されなくなってしまいます。
線が足りなくならないようにしっかり計算するか、いっそ大き目の長さで線を引いたほうが良いでしょう。
(丸め誤差とかで 0.0000001 ピクセル足りなくて表示されない、みたいなことがあります)


最初は座標計算で混乱して吐く or 泣くするかもしれませんが、3文字くらいでどうにか成功させれば、あとは for文でループさせるだけなので、どうにかなります。
頭の中で計算するより、実機で表示させて調整したほうが早いです、というか脳内計算とか私には無理でした。目視確認しつつ調整です。


以上です。
がんばって「ー」も正しく縦書きしてみて下さい。*2


*1 bottom - topです。たしか top はマイナス値なので注意。
*2 そもそも私が縦書きした経緯ですが、最近つくったアプリを縦画面固定の仕様にしたら AndroidStudioに横画面対応もしろと注意されたので、対応したんです。ボタンを押せば回転します、画面ではなく文字だけ。