Androidで矩形範囲内に文字列を描画する
北本です。
投稿間隔が空いてしまいましたが、今回もAndroidについての記事です。
canvasで文字列を矩形領域内をはみ出さないように折り返しを入れて描画する方法を紹介します。
.NETの場合はGraphics.DrawStringメソッドでRectangle構造体を引数に渡して実現できますが、Androidの場合はStaticLayoutを使います。
以下のようなアプリを作成して実験してみます。
- 画面にはImageViewとEditTextとButtonを配置。
- Buttonを押下することで、EditTextに入力された文字列が矩形に囲まれた画像が生成され、ImageViewに表示される。文字列が入り切らない場合は末尾を省略するものとする。
下準備
Android StudioでEmptyActivity、Minimum SDK 23でプロジェクトを作成し、build.gradle(:app)を編集してビューバインディングを有効にします。
build.gradle(:app)
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 | plugins { // 省略 // } android { compileSdkVersion 30 buildToolsVersion "30.0.3" defaultConfig { applicationId "com.example.drawtext" minSdkVersion 23 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } // 中略 // buildFeatures{ viewBinding = true } } dependencies { // 省略 // } |
レイアウト
画面(MainActivity)は以下のようなXMLになります。
activity_main.xml
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 | <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/imageView" android:layout_width="300dp" android:layout_height="300dp" android:layout_marginStart="16dp" android:layout_marginLeft="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> <Button android:id="@+id/drawButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="描画" app:layout_constraintEnd_toEndOf="@+id/editText" app:layout_constraintStart_toStartOf="@+id/editText" app:layout_constraintTop_toBottomOf="@+id/editText" /> <EditText android:id="@+id/editText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:ems="10" android:gravity="start|top" android:inputType="textMultiLine" android:maxLines="5" android:minLines="1" app:layout_constraintEnd_toEndOf="@+id/imageView" app:layout_constraintStart_toStartOf="@+id/imageView" app:layout_constraintTop_toBottomOf="@+id/imageView" /> </androidx.constraintlayout.widget.ConstraintLayout> |
idの設定は以下の通りです。
ビュー | id |
ImageView | imageView |
EditText | editText |
Button | drawButton |
実装
コード(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 | 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 maxLines = (rectHeight / (textPaint.getFontMetrics(null) * spacingMulti + spacingAdd)).toInt() var staticLayout: StaticLayout val builder = StaticLayout.Builder.obtain(text, 0, text.length, textPaint, rectWidth.toInt()) .setAlignment(alignment) .setLineSpacing(spacingAdd, spacingMulti) .setMaxLines(maxLines) .setEllipsize(TextUtils.TruncateAt.END) staticLayout = builder.build() canvas.translate(x, y) staticLayout.draw(canvas) binding.imageView.setImageBitmap(bmp) } } |
上記コードのうち、後半部のStaticLayoutを扱っている箇所を解説します。
StaticLayoutのコンストラクタはdeprecatedなので、StaticLayout.Builderを使用してインスタンスを取得します。ただし、Android 6.0未満はStaticLayout.Builderに対応していません。
StaticLayout.Builderのobtainメソッドでは、
第1引数に描画対象の文字列、
第2引数に文字列の始点インデックス(今回は文字列全体を描画対象とするので0)、
第3引数に文字列の終点インデックス(今回は文字列全体を描画対象とするので文字列の長さ)、
第4引数に描画に使うTextPaintオブジェクト
第5引数に描画範囲の幅(今回は矩形の幅)
を指定します。
setAlignmentメソッドでは、アラインメントが設定できます。今回は左揃え(アラビア語などの場合は右揃え)にするのでLayout.Alignment.ALIGN_NORMALを指定しています。
setLineSpacingメソッドでは、行間が設定できます。
第1引数のspacingAddは、指定した値だけ各行の高さが加算されます。
第2引数のspacingMultiは、各行の高さが指定した値で乗算されます。
setMaxLinesメソッドでは、最大の描画行数を指定できます。今回は、矩形の高さに収まるように1行の高さから行数を算出しています。1行の高さはtextPaint.getFontMetrics(null)で求められます。前述のspacingAdd、spacingMultiによる行間も考慮して計算していますが、setLineSpacingで行間指定をしない場合は、デフォルトでspacingAddは0.0、spacingMultiは1.0になるため、矩形の高さをtextPaint.getFontMetrics(null)で除算するだけで大丈夫です。
setEllipsizeメソッドでは、文字列が範囲に描画しきれない場合の省略法を設定できます。今回は末尾を省略するため、TextUtils.TruncateAt.ENDを指定しています。
このようにして各種設定をしたら、StaticLayout.BuilderのbuildメソッドでStaticLayoutのインスタンスを取得します。そして、canvas.translateでcanvasの原点を矩形の左上頂点に移動させ、StaticLayout.drawメソッドで描画を実行します。
以上、StaticLayout.Builderを利用した文字列の描画について紹介しましたが、残念ながらAndroid 6.0未満ではStaticLayout.Builderが使えません。MaxLinesで最大行数も指定できませんので、この辺りは自前で処理しなければなりません。このあたりについてはまた次回取り上げられればと思います。