Androidで矩形範囲内に文字列を描画する(Android 6.0未満編)
北本です。
かなり間が空いてしまいましたが、前回の続きです。
StaticLayoutを使ってAndroidで矩形範囲内に文字列を描画する方法を紹介したのが前回でしたが、そこで使用したStaticLayout.BuilderはAndroid 6.0未満では対応していません。今回は、Android 6.0未満向けにStaticLayout.Builderを使用せず同様の処理を実現してみます。
ソースは前回に掲載したものを利用します。
まず、build.gradleを編集して、minSdkVersionを23(Android 6.0)未満に変更してみましょう。
すると、StaticLayout.Builderを使用している箇所でエラーが出るはずです。
その箇所を
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
で囲ってAndroid 6.0以上の場合に実行するようにし、elseで6.0未満向けの処理を書くことにします。
以下のようなコードとなります。
MainActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | package com.example.drawtext import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.os.Build import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.text.Layout import android.text.StaticLayout import android.text.TextPaint import android.text.TextUtils import com.example.drawtext.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.drawButton.setOnClickListener{ drawImage()} } private fun drawImage(){ val imgWidth = 200f // 画像幅 val imgHeight = 200f // 画像高さ val rectWidth = 100f // 矩形幅 val rectHeight = 100f // 矩形高さ val x = (imgWidth - rectWidth) / 2 // 矩形左上x座標 val y = (imgHeight - rectHeight) / 2 // 矩形左上y座標 val text = binding.editText.text.toString() // 描画文字列 val bmp = Bitmap.createBitmap(imgWidth.toInt(), imgHeight.toInt(), Bitmap.Config.ARGB_8888) var canvas = Canvas(bmp) var paint = Paint() // 全体を塗りつぶし paint.color = Color.YELLOW paint.style = Paint.Style.FILL canvas.drawRect(0f, 0f, imgWidth, imgHeight, paint) // 矩形描画 paint.color = Color.BLUE paint.style = Paint.Style.STROKE canvas.drawRect(x, y, x + rectWidth, y + rectHeight, paint) // 文字列描画 var textPaint = TextPaint() textPaint.color = Color.BLACK textPaint.strokeWidth = 1f textPaint.style = Paint.Style.FILL textPaint.textSize = 16f textPaint.isAntiAlias = true val alignment = Layout.Alignment.ALIGN_NORMAL val spacingAdd = 4f val spacingMulti = 1.1f val includePad = true val maxLines = (rectHeight / (textPaint.getFontMetrics(null) * spacingMulti + spacingAdd)).toInt() var staticLayout: StaticLayout if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ val builder = StaticLayout.Builder.obtain(text, 0, text.length, textPaint, rectWidth.toInt()) .setAlignment(alignment) .setLineSpacing(spacingAdd, spacingMulti) .setIncludePad(includePad) .setMaxLines(maxLines) .setEllipsize(TextUtils.TruncateAt.END) staticLayout = builder.build() } else{ // Android6.0未満ではStaticLayout.Builderが使えないため別処理 staticLayout = StaticLayout(text, textPaint, rectWidth.toInt(), alignment, spacingMulti, spacingAdd, includePad) // 文字列が長い場合にはみ出ないように切り捨てる if(staticLayout.lineCount > maxLines){ var lower = 0 var upper = text.length var mid = 0 // 行数がmaxLineを超えない最大の文字数を求める while(lower < upper){ mid = (lower + upper) / 2 staticLayout = StaticLayout(text.substring(0, mid), textPaint, rectWidth.toInt(), alignment, spacingMulti, spacingAdd, includePad) if(staticLayout.lineCount > maxLines){ upper = mid } else{ lower = mid + 1 } } // 末尾を削って省略符号を付ける for(i in 0..mid){ staticLayout = StaticLayout(text.substring(0, mid - i) + "…", textPaint, rectWidth.toInt(), alignment, spacingMulti, spacingAdd, includePad) if(staticLayout.lineCount <= maxLines) break; } } } canvas.translate(x, y) staticLayout.draw(canvas) binding.imageView.setImageBitmap(bmp) } } |
前半は前回と同じままです。変化しているのは、if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)とそのelseの中です。あと、includePadという変数が追加されています。
Android 6.0未満の場合は、StaticLayout.Builderが使用できないので、StaticLayoutのコンストラクタを使います。コンストラクタは以下のように定義されています。
public StaticLayout (
CharSequence source,
TextPaint paint,
int width,
Layout.Alignment align,
float spacingmult,
float spacingadd,
boolean includepad)
第1引数は描画対象の文字列、第2引数は描画に使うTextPaintオブジェクト、第3引数は描画範囲の幅、第4変数はアラインメント、第5変数と第6変数は行間の設定で、前回StaticLayout.Builderを扱った際にも同様な設定をしています。第7変数のIncludePadについては前回触れていませんでしたが、StaticLayout.Builderを使う場合でもsetIncludePadメソッドで設定できます。IncludePadはStaticLayout.Builderの場合は設定しなければデフォルトでtrueになりますが、StaticLayoutのコンストラクタでは引数を省略できないため明示的に設定する必要があります。
では、このIncludePadとは何なのかという話ですが、setIncludePadについて公式のドキュメントには以下のように書かれています。
フォントアセント及びフォントディセントを超えた余白(アラビア語やカナラ語のようないくらかの言語において文字切れを避けるために必要)を含めるかどうかを設定する。
正直に申しますと、アラビア語やカナラ語についてよく知らないので、あまりピンときません。フォントアセントやフォントディセントについては、詳細は他のサイト等を見ていただくこととしますが、それぞれ、フォントのベースラインから上端までの高さ、ベースラインから下端までの高さみたいなものです。基本的には概ねこれらの高さの範囲に文字は収まりますが、アラビア語やカナラ語ではこの範囲を収まらないような文字が存在するのでしょうか?
使用する言語によっては、もしかすると、行の高さの計算の上で影響があるかもしれませんが、日本語や英語等を使用する限りでは恐らく大丈夫でしょう。取り敢えず、ここではStaticLayout.Builderのデフォルトと同様、trueにしています。
StaticLayout.Builderと違い、コンストラクタの場合はsetMaxLinesやsetEllipsizeに相当する設定ありません。そのため、このあたりは自前で処理しています。
行数については、staticLayout.lineCountで描画された時の行数が取得できるので、それがmaxLinesより大きい場合は、文字数を削ってstaticLayout.lineCountで取得した行数に収まるようにします。行数がmaxLineを超えない最大の文字数を求めますが、ここでは申し訳程度に二分探索を使ってみました。
続いて、末尾に省略記号「…」を付与します。ここは単純に、先ほど求めた文字数分だけ抜き出した文字列から一文字ずつ減らしていきながら、「…」を付与して描画した際に行数がmaxLineを超えない文字数を探しています。
あとは、前回と同様に描画するだけです。
このように、StaticLayout.Builderで実現していた処理を、Android 6.0未満でも対応しようとすると別途に処理を書く必要が出てきます。弊社のアプリ「ポケットマンダラ」のAndroid版は、一応6.0未満のバージョンにも対応しているもののサポートページに「6.0以上を推奨」と記載しているのですが、その理由はこの辺りにあります。
時代とともに古いバージョンが淘汰されていくことで、今回のように別処理を用意する手間を掛ける必要もなくなっていくことでしょう。この記事を書いている時点でも、既に6.0以上のバージョンがシェアの94.1%を占めているようですので、6.0未満向けの対応をしなくても大部分のユーザーはカバーできます。少数のユーザを取るか、手間を省くか、取捨選択をどうするかですね。