Android Tips #12 ViewのonDraw()で回転させたテキストを描画する

2012.04.29

はじめに

今回は、ViewのonDraw()メソッド内でBitmap化したテキストを回転させる方法を解説したいと思います。
それだけでは物足りないので、NinePatchを適用したDrawableを描画する方法と
onDraw()メソッド内での処理の高速化についても併せて解説していきます。

テキストを回転させる

テキストの描画は Bitmapクラス, Canvasクラス, Paintクラス, FontMetricsクラスを使用します。
それに加えて、回転させるためにMatrixクラスも使用します。

Canvasクラス Bitmapへのテキストの描画
Paintクラス 描画するテキスト色やサイズなどの指定, テキスト幅の取得
FontMetricsクラス テキストの高さの取得
Matrixクラス Bitmapの回転

テキストの幅、高さを取得してBitmapを生成する

FontMetricsクラスはテキストの正確な高さを取得するために使用します。
Paint#getFontMetrics() メソッドでインスタンスを取得し、以下のプロパティでテキストの高さを得ることができます。
以下のようなメソッドを作っておくと便利です。

private int getTextHeight(Paint p) {
	FontMetrics fontMetrics = p.getFontMetrics();
	return (int) Math
			.ceil(Math.abs(fontMetrics.ascent)
					+ Math.abs(fontMetrics.descent)
					+ Math.abs(fontMetrics.leading));
}

Paintのプロパティを元に算出するので、FontMetricsインスタンスを取得する前に
Paintにフォントサイズなどのプロパティを指定する必要があります。

また、テキストの幅を取得するにはPaint#measureText()メソッドを使用します。

private int getTextWidth(Paint p) {
	return (int) Math.ceil(p.measureText(mText));
}

CanvasクラスでBitmapにテキストを描画する

CanvasクラスはBitmapに対して描画を行うクラスです。
コンストラクタ引数にBitmapインスタンスを渡すことで、対象のBitmapに描画することができます。
Canvas#drawText()メソッドでテキストを描画します。
また、Bitmapの幅, 高さは上記で作成したメソッドを使用しましょう。

// 描画するテキスト
String text = "テキスト";

// テキスト用のPaintオブジェクトを作成
Paint p = new Paint();
p.setTextSize(70);
p.setAntiAlias(true);
p.setColor(Color.CYAN);
p.setShadowLayer(5, 1, 1, Color.BLACK);

// テキストの幅・高さを取得
int width = getTextWidth(p);
int height = getTextHeight(p);

// テキストのBitmapを生成
Bitmap text = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(text);
c.drawText(mText, 0, Math.abs(p.getFontMetrics().ascent), p);

MatrixクラスでBitmapを回転させる

最後に、Matrixクラスを使用してBitmapを回転させます。
Matrix#postRotate()メソッドで回転角度を指定し、
Bitmap#createBitmap()メソッドで回転させたBitmapを生成します。

// 45度傾ける
Matrix matrix = new Matrix();
matrix.postRotate(45);
Bitmap rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);

上記メソッドで生成したBitmapをonDraw()内でCanvasに描画します。

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	// 背景の描画
	canvas.drawColor(Color.LTGRAY);
	int x = (getWidth() / 2) - (mTextWidth / 2);
	int y = (getHeight() / 2) - (mTextHeight / 2);
	// 中心位置に配置する
	canvas.drawBitmap(mTextBitmap, x, y, null);
}

実行結果

NinePatchを適用したDrawableをBitmapに変換する

まずはNinePatchな画像を作成

まずはNinePatchを適用した画像を作りましょう。
NinePatchを適用した画像の作成方法は以下のサイトがとても参考になりますので参照してください。

NinePatchDrawableをBitmapに変換する

上記で作成した画像ファイルをResources#getDrawable()メソッドで取り込みます。
NinePatchを適用したリソースは NinePatchDrawableクラス として取得できます。
通常のDrawableと異なる点は、NinePatchDrawableにサイズを指定する必要があるところです。
NinePatchDrawable#setBounds()メソッドでサイズを指定することができます。

private Bitmap getBackgroundBitmap(int width, int height) {
	// リソースからNinePatchを取得
	Resources resources = getContext().getResources();
	// NinePatchを当てたpngのリソースIDを参照
	int res = R.drawable.bg;
	NinePatchDrawable bg = (NinePatchDrawable) resources.getDrawable(res);
	bg.setBounds(new Rect(0, 0, width, height));
	
	// Bitmapに変換
	Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
	Canvas canvas = new Canvas(bitmap);
	bg.draw(canvas);
	
	return bitmap;
}

上記メソッドで生成したBitmapをonDraw()で描画すると、下図のような実行結果になります。

onDraw()処理の高速化

onDraw()での描画処理を高速化するには…

「オブジェクトやメソッド呼び出しを最小限にする」

これに尽きます。
onDraw()メソッドは頻繁に呼ばれるメソッドであるため、
その中でオブジェクトの生成やメソッドの呼び出し、複雑な処理などを行うとパフォーマンスを悪化させてしまいます。
例えば下記の例ではテキストの描画処理をすべてonDraw()メソッド内で行なっており、
Paintインスタンスの生成やテキストサイズの計算などの処理が頻繁に実行されてしまいます。

悪い例

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	
	String text = "テキスト";
	Paint paint = new Paint();
	paint.setAntiAlias(true);
	paint.setTextSize(20);
	paint.setColor(Color.RED);

	int textWidth = (int) Math.ceil(paint.measureText(text));
	FontMetrics fontMetrics = paint.getFontMetrics();
	int textHeight = (int) Math
			.ceil(Math.abs(fontMetrics.ascent)
					+ Math.abs(fontMetrics.descent)
					+ Math.abs(fontMetrics.leading));

	int x = (getWidth() / 2) - (textWidth / 2);
	int y = (getHeight() / 2) - (textHeight / 2);
	
	canvas.drawText(text, x, y, paint);
}

上記メソッド内でonDraw()メソッドで処理する必要があるコードはCanvas#drawText()メソッドのみです。
その他の処理はコンストラクタやonSizeChanged()メソッドなどで行うようにすることで、
処理を必要最低限に抑えることができます。

良い例

private String mText;
private int mTextX;
private int mTextY;
private int mTextWidth;
private int mTextHeight;
private Paint mPaint;

public SampleView(Context context) {
	super(context);
	mText = "テキスト";
	mPaint = new Paint();
	mPaint.setTextSize(20);
	mPaint.setAntiAlias(true);
	mPaint.setColor(Color.RED);
	mTextWidth = (int) Math.ceil(mPaint.measureText(mText));
	FontMetrics fontMetrics = mPaint.getFontMetrics();
	mTextHeight = (int) Math
			.ceil(Math.abs(fontMetrics.ascent)
					+ Math.abs(fontMetrics.descent)
					+ Math.abs(fontMetrics.leading));
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
	super.onSizeChanged(w, h, oldw, oldh);
	mTextX = (w / 2) - (mTextWidth / 2);
	mTextY = (h / 2) - (mTextHeight / 2);
}

@Override
protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
	canvas.drawText(mText, mTextX, mTextY, mPaint);
}

サンプルソース

これまで解説してきたコードを一つのクラスにまとめると以下のようになります。

CustomView.java

package jp.classmethod.sample;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Rect;
import android.graphics.drawable.NinePatchDrawable;
import android.view.View;

public class SampleView extends View {

	private String mText;
	private Bitmap mTextBitmap;
	private int mTextX;
	private int mTextY;
	private int mTextWidth;
	private int mTextHeight;
	
	public SampleView(Context context) {
		super(context);
		mText = "テキスト";
		mTextBitmap = getTextBitmap();
		mTextWidth = mTextBitmap.getWidth();
		mTextHeight = mTextBitmap.getHeight();
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		// 背景の描画
		canvas.drawColor(Color.LTGRAY);
		// 中心位置に配置する
		canvas.drawBitmap(mTextBitmap, mTextX, mTextY, null);
	}
	
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		mTextX = (w / 2) - (mTextWidth / 2);
		mTextY = (h / 2) - (mTextHeight / 2);
	}

	private Bitmap getTextBitmap() {
		// 45度傾ける
		Matrix matrix = new Matrix();
		matrix.postRotate(45);

		// テキスト用のPaintオブジェクトを作成
		Paint p = new Paint();
		p.setTextSize(70);
		p.setAntiAlias(true);
		p.setColor(Color.CYAN);
		p.setShadowLayer(5, 1, 1, Color.BLACK);
		
		// テキストの幅・高さを取得
		int width = getTextWidth(p);
		int height = getTextHeight(p);
		
		// テキストのBitmapを生成
		Bitmap text = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
		Canvas c = new Canvas(text);
		c.drawText(mText, 0, Math.abs(p.getFontMetrics().ascent), p);
		
		// 背景のBitmapを生成
		int padding = 20;
		int bgWidth = width + padding * 2;
		int bgHeight = height + padding * 2;
		Bitmap bg = getBackgroundBitmap(bgWidth, bgHeight);
		
		// テキストと背景を合成+回転
		Bitmap bitmap = Bitmap.createBitmap(bgWidth, bgHeight, Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(bitmap);
		canvas.drawBitmap(bg, 0, 0, null);
		canvas.drawBitmap(text, padding, padding, null);
		Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bgWidth, bgHeight, matrix, true);
		
		// 不要なBitmapを破棄
		text.recycle();
		text = null;
		bg.recycle();
		bg = null;
		bitmap.recycle();
		bitmap = null;
		
		return result;
	}
	
	private Bitmap getBackgroundBitmap(int width, int height) {
		// リソースからNinePatchを取得
		Resources resources = getContext().getResources();
		int res = R.drawable.bg;
		NinePatchDrawable bg = (NinePatchDrawable) resources.getDrawable(res);
		bg.setBounds(new Rect(0, 0, width, height));
		
		// Bitmapに変換
		Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(bitmap);
		bg.draw(canvas);
		
		return bitmap;
	}

	private int getTextWidth(Paint p) {
		return (int) Math.ceil(p.measureText(mText));
	}

	private int getTextHeight(Paint p) {
		FontMetrics fontMetrics = p.getFontMetrics();
		return (int) Math
				.ceil(Math.abs(fontMetrics.ascent)
						+ Math.abs(fontMetrics.descent)
						+ Math.abs(fontMetrics.leading));
	}
}

実行結果

まとめ

カスタムViewのonDraw()メソッドはBitmapを自由に描画することができ非常に便利です。
例えばListViewのアイテムをレイアウトコンポーネントではなくカスタムViewにすることで、
劇的なパフォーマンス向上が見込める場合もあります。
「onDraw()の処理をいかにシンプルにするか」意識することで効率的な描画をすることができます。
カスタムViewをつくるときに是非参考にしていただければと思います。