Androidアプリケーションにおいて、リソース効率を高め、UIの一貫性を保つため、ランタイムで画像を動的に加工するニーズは非常に多い。代表的な操作には、解像度に応じたスケーリング、アスペクト比を維持したクロッピング、任意角度での回転、および加工後の永続化(ストレージへの保存)が含まれる。本稿では、これらの機能を統合的に提供するユーティリティクラス ImageProcessor の設計と実装を紹介する。
核心APIの活用
画像操作の基盤となるのは BitmapFactory と Bitmap.createBitmap() である。後者は行列変換(Matrix)を用いて、単一のAPI呼び出しで複数のトランスフォームを合成可能である。
主要な処理メソッド
1. リソースIDからBitmapを安全に読み込む
/**
* リソースIDをもとにARGB_565形式でBitmapを生成
* メモリ使用量を抑制し、GC負荷を軽減
*/
public static Bitmap loadFromResource(@NonNull Context context, @DrawableRes int resId) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
try (InputStream stream = context.getResources().openRawResource(resId)) {
return BitmapFactory.decodeStream(stream, null, options);
} catch (IOException e) {
Log.w("ImageProcessor", "Failed to decode resource: " + resId, e);
return null;
}
}2. 指定サイズへの等比スケーリング
/**
* 元画像を指定幅・高さに等比縮小/拡大
* アスペクト比を保持し、不要な余白を発生させない
*/
public static Bitmap resizeTo(@NonNull Bitmap src, int targetWidth, int targetHeight) {
if (src.isRecycled()) return null;
float scaleX = (float) targetWidth / src.getWidth();
float scaleY = (float) targetHeight / src.getHeight();
Matrix transform = new Matrix();
transform.setScale(scaleX, scaleY, 0, 0);
Bitmap result = Bitmap.createBitmap(
src, 0, 0, src.getWidth(), src.getHeight(),
transform, true
);
if (!src.equals(result)) {
src.recycle();
}
return result;
}3. アスペクト比固定クロッピング(中心切り出し)
/**
* 指定比率(例: 4:3 → num1=4, num2=3)で中央部を切り出す
* 元画像より狭い領域を抽出し、余白を除去
*/
public static Bitmap cropToAspect(@NonNull Bitmap src, int longSide, int shortSide) {
int w = src.getWidth();
int h = src.getHeight();
float targetRatio = (float) longSide / shortSide;
float currentRatio = (float) w / h;
int cropW, cropH, offsetX = 0, offsetY = 0;
if (currentRatio > targetRatio) {
// 幅が広い → 高さ基準で切り出し
cropH = h;
cropW = Math.round(h * targetRatio);
offsetX = (w - cropW) / 2;
} else {
// 高さが高い → 幅基準で切り出し
cropW = w;
cropH = Math.round(w / targetRatio);
offsetY = (h - cropH) / 2;
}
Bitmap cropped = Bitmap.createBitmap(src, offsetX, offsetY, cropW, cropH, null, false);
if (!src.equals(cropped)) src.recycle();
return cropped;
}4. 円形マスク適用(Circle Crop)
/**
* 正方形領域を円形マスクで切り抜き、透明背景の円形Bitmapを生成
*/
public static Bitmap toCircularBitmap(@NonNull Bitmap src) {
int size = Math.min(src.getWidth(), src.getHeight());
Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
final RectF bounds = new RectF(0, 0, size, size);
// 背景を透明に初期化
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// 円形クリッピングパスを描画
canvas.drawOval(bounds, paint);
// SRC_IN合成モードで元画像を重ねる
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(src,
new Rect(0, 0, src.getWidth(), src.getHeight()),
bounds, paint
);
if (!src.equals(output)) src.recycle();
return output;
}5. 任意角度での回転
/**
* 指定角度(度単位)で画像を回転。中心を基点とする
*/
public static Bitmap rotateBy(@NonNull Bitmap src, float degrees) {
Matrix rotation = new Matrix();
rotation.postRotate(degrees, src.getWidth() / 2f, src.getHeight() / 2f);
Bitmap rotated = Bitmap.createBitmap(
src, 0, 0, src.getWidth(), src.getHeight(),
rotation, true
);
if (!src.equals(rotated)) src.recycle();
return rotated;
}6. JPEG形式で外部ストレージへ保存
/**
* 指定ディレクトリにJPEGファイルとして保存(品質可変)
* Android 10以降ではScoped Storage対応が必要
*/
public static boolean saveAsJpeg(@NonNull File directory, @NonNull Bitmap bitmap,
@NonNull String fileName, int quality) {
if (!directory.exists() && !directory.mkdirs()) {
Log.e("ImageProcessor", "Cannot create directory: " + directory);
return false;
}
File file = new File(directory, fileName.endsWith(".jpg") ? fileName : fileName + ".jpg");
try (FileOutputStream out = new FileOutputStream(file)) {
return bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out);
} catch (IOException e) {
Log.e("ImageProcessor", "Save failed: " + file.getAbsolutePath(), e);
return false;
}
}