【制作メモ】Kotlinで縦書きTextView作成

Androidスタジオが重すぎて、作業が頓挫しそう

先日iOSのこだまみくじに追加した「街みくじ」コンテンツですが、
Androidでは独立したアプリにしようと考えております。
なぜなら、先日PCのOSをアップデートする際に誤ってAndroidアプリのプログラムソースを消してしまったので、早めに作り直す必要があるからです。。
アップデートする際の秘密キーみたいなのも消しちゃったから、Androidのこだまみくじの今後はどうなるんだという事案がありますが、それはその時まで目を瞑る予定。

で、掲題ですが、街みくじをAndroidに移植するために必要になるのが「縦書き」なのですが、ライブラリ等は少し古そうだったので、自前でchatGPTに聞きながら作ることにしました。
まあ、予想通りというか、実装するのに非常に手間がかかったので今後のメモとしてソースを貼っておきます。
コピペしてそのまま使えるかは知りませんが、必要な方は自己責任で!

■縦書き前提条件
・特定の座標を指定して縦書きのTextVIewを使いたかった
・縦書きは小説とかではなく、1行のViewで足りる。
・今回は句読点は使わないので意識する必要なし

■縦書きクラス

import android.content.Context
import android.graphics.Canvas
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView

class VerticalTextViewX : AppCompatTextView {

    private val textPaint = TextPaint()

    // コンストラクタ1:xとyの座標を受け取って初期化
    constructor(context: Context, x: Float, y: Float) : super(context) {
        this.x = x
        this.y = y
        init()
    }

    /*
    // コンストラクタ2:ContextとAttributeSetを受け取って初期化の場合(今回は不要)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        init()
    }
    */

    // 初期化処理を行うメソッド
    private fun init() {
        textPaint.set(this.paint) // テキストの描画に使うTextPaintオブジェクトを設定
        textPaint.color = this.currentTextColor // テキストの色を設定
        textPaint.textSize = this.textSize // テキストのサイズを設定
    }

    // ビューのサイズを計測するメソッド
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        // 縦書きでは、テキストの横幅と高さを入れ替える
        setMeasuredDimension(measuredHeight, measuredWidth)
    }

    override fun onDraw(canvas: Canvas) {
        canvas.save()

        // 縦書きの場合は行間を設定するとより自然に見えます
        val lineHeight = textSize + lineSpacingExtra

        // テキストに対して特定の文字に対して位置調整を行った後の文字列を取得(今回は不要)
        //val textWithAdjustedPunctuation = adjustPunctuation(text.toString())

        var y = 0f // 文字の描画位置の初期値を設定

        // テキストの各文字を順番に描画していくループ
        for (i in 0 until textWithAdjustedPunctuation.length) {
            val character = textWithAdjustedPunctuation[i].toString() // テキストの1文字を取得
            val layout = StaticLayout(character, textPaint, width, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)

            // テキストの1文字のベースライン(テキストの基準線)の位置を取得
            val lineBaseline = layout.getLineBaseline(0).toFloat()

            // 1文字をキャンバス上に描画
            // 第1引数:描画するテキスト
            // 第2引数:描画するテキストのx座標(0fはキャンバスの左端)
            // 第3引数:描画するテキストのy座標(yは縦書きの場合、0fから始まって、行間(lineHeight)ずつ増加する)
            canvas.drawText(character, 0f, y + lineBaseline, textPaint)

            y += lineHeight // 次の文字の描画位置を調整
        }

        canvas.restore()
    }

/*
    // 特定の文字に対して位置調整を行う場合のメソッド(今回は不要・・のはず)
    private fun adjustPunctuation(text: CharSequence): CharSequence {
        val adjustedText = text.toString()
            .replace("。", " 。") // 句読点の前にスペースを追加
            .replace("、", " 、") // 読点の前にスペースを追加
        return adjustedText
    }
*/
}

で、このクラスを呼び出す時は

    //表示位置を指定して呼び出す    
    val TateTextView = VerticalTextView(this, 100f, 200f)
    TateTextView.text = "こだまみくじ縦書き"

    val parentLayout = findViewById<LinearLayout>(R.id.parent_layout)
    parentLayout.addView(vTateTextView)

って感じでです。

■縦書きクラス改良版
オリジナルフォントで縦書きを実装したかったので、フォントも渡す仕様に変えました。
なお、指定したフォントはひらがな、カタカナの文字が少し小さめだったので、縦書きにした際の文字をセンター表示にしました。
さらに、「。」「、」の時は右上になるようにしました。
(本当は「textWithAdjustedPunctuation」を使うのですが、2文字だったので直書で。。)

package com.kodama.machimikujiapp

import android.content.Context
import android.graphics.Canvas
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import android.graphics.Typeface


class VerticalTextViewX : AppCompatTextView {

    private val textPaint = TextPaint()
    // customTypefaceをVerticalTextViewXクラスのメンバ変数として定義する
    private val customTypeface: Typeface?

    // コンストラクタ1:xとyの座標を受け取って初期化
// コンストラクタ2:xとyの座標とフォントを受け取って初期化
    constructor(context: Context, x: Float, y: Float, customTypeface: Typeface?) : super(context) {
        this.x = x
        this.y = y
        this.customTypeface = customTypeface
        init()
    }

    // 初期化処理を行うメソッド
    private fun init() {
        textPaint.color = this.currentTextColor // テキストの色を設定
        textPaint.textSize = this.textSize // テキストのサイズを設定
        if (customTypeface != null) {
            this.typeface = customTypeface
        }
        // フォント設定の確認用ログ
        println("CustomTypeface: $customTypeface")
        println("TextView Typeface: ${this.typeface}")
    }




    // ビューのサイズを計測するメソッド
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        // 縦書きでは、テキストの横幅と高さを入れ替える
        setMeasuredDimension(measuredHeight, measuredWidth)
    }

    override fun onDraw(canvas: Canvas) {
        canvas.save()

        // 縦書きの場合は行間を設定するとより自然に見えます
        val lineHeight = textSize + lineSpacingExtra

        // テキストに対して特定の文字に対して位置調整を行った後の文字列を取得
        //val textWithAdjustedPunctuation = adjustPunctuation(text.toString())

        var y = 0f // 文字の描画位置の初期値を設定

        // 以下の行を追加してtextPaintにフォントを設定する
        if (customTypeface != null) {
            textPaint.typeface = customTypeface
        }

        // テキストの各文字を順番に描画していくループ
        for (i in 0 until textWithAdjustedPunctuation.length) {
        // 1文字をキャンバス上に描画
        val character = textWithAdjustedPunctuation[i].toString() // テキストの1文字を取得
        val textWidth = textPaint.measureText(character) // テキストの幅を計測

        // テキストの1文字の中央に描画するための x 座標を計算
        val centerX = (width - textWidth) / 2

        // テキストの1文字のベースライン(テキストの基準線)の位置を取得
        val lineBaseline = layout.getLineBaseline(0).toFloat()

            // 特定の文字の場合は位置を右上にする
            if (character == "、" || character == "。") {
                // 1文字をキャンバス上に描画
                val textWidth = textPaint.measureText(character) // テキストの幅を計測
                val offsetY = -lineHeight / 2 // さらに上に移動させる分のオフセット値
                canvas.drawText(character, centerX + textWidth / 2, y + lineBaseline + offsetY, textPaint)

            } else {
                // 1文字をキャンバス上に描画
                canvas.drawText(character, centerX, y + lineBaseline, textPaint)
            }
        // 1文字をキャンバス上に描画
       //canvas.drawText(character, centerX, y + lineBaseline, textPaint)

            y += lineHeight // 次の文字の描画位置を調整
        }

        canvas.restore()
    }


    // 特定の文字に対して位置調整を行うメソッド
    private fun adjustPunctuation(text: CharSequence): CharSequence {
        val adjustedText = text.toString()

        return adjustedText
    }

    /*
   //「。」「、」を除外する場合はこっちを使う
    // 特定の文字に対して位置調整を行うメソッド
    private fun adjustPunctuation(text: CharSequence): CharSequence {
        val adjustedText = StringBuilder()


        for (i in 0 until text.length) {
            val character = text[i]

            if (character == '。' || character == '、') {
                // 句読点の場合はスペースを追加して除外
                adjustedText.append(" ")
            } else {
                adjustedText.append(character)
            }
        }
        return adjustedText.toString()
    }

     */
}


そしてフォントも渡すので以下になります。

    val customFontResourceId = R.font.apricotjapanesefont

        val customTypeface = ResourcesCompat.getFont(this, customFontResourceId)

        val verticalTextView = VerticalTextViewX(this, 100f, 200f, customTypeface)

        verticalTextView.text = "縦書きテキストを表示します。"
        val parentLayout = findViewById<LinearLayout>(R.id.parent_layout)
        parentLayout.addView(verticalTextView)




以上!
SwiftでAndroidアプリも作れる時代になってほしいものです。。と言っておきます!!