大阪市中央区 システムソフトウェア開発会社

営業時間:平日09:15〜18:15
MENU

Androidで矩形範囲内に文字列を描画する

著者:北本 敦
公開日:2021/09/29
最終更新日:2022/02/28
カテゴリー:技術情報
タグ:

北本です。
投稿間隔が空いてしまいましたが、今回も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)

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

<?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
ImageViewimageView
EditTexteditText
ButtondrawButton

 

実装
コード(MainActivity.kt)は以下のようになります。

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で最大行数も指定できませんので、この辺りは自前で処理しなければなりません。このあたりについてはまた次回取り上げられればと思います。

    上に戻る