AndroidのCameraXを使うと、カメラプレビューの上に独自の描画レイヤーを重ねることができます。 本記事では、プレビュー上をタッチすると赤丸を描画し、カメラ画像と合成して保存する機能を実装します。
さらに、座標ずれ問題・サイズ不一致・2回目以降の保存エラーなど、実装時に発生しやすい問題の対処方法もまとめています。
1. CameraXでカメラプレビューを表示する
java
private void startCamera() {
ProcessCameraProvider cameraProviderFuture =
ProcessCameraProvider.getInstance(requireContext());
cameraProviderFuture.addListener(() -> {
try {
Preview preview = new Preview.Builder().build();
imageCapture = new ImageCapture.Builder().build();
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
cameraProvider.unbindAll();
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
} catch (Exception e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(requireContext()));
}
2. DrawingView:タッチした位置に赤丸を描画する
java
public class DrawingView extends View {
private Paint paint;
private List<Point> points = new ArrayList<>();
public DrawingView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (Point p : points) {
canvas.drawCircle(p.x, p.y, 20, paint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN ||
event.getAction() == MotionEvent.ACTION_MOVE) {
points.add(new Point(event.getX(), event.getY()));
invalidate();
return true;
}
return false;
}
public void reset() {
points.clear();
invalidate();
}
private static class Point {
float x, y;
Point(float x, float y) { this.x = x; this.y = y; }
}
}
3. レイアウト:PreviewView と DrawingView を重ねる
xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<faithit.image.ad_98list.ui.home.DrawingView
android:id="@+id/drawingView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
4. ImageProxy → Bitmap に変換する
java
private Bitmap imageProxyToBitmap(ImageProxy image) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
5. カメラ画像と描画画像を合成して保存する
java
private void saveImage(Bitmap cameraBitmap) {
Bitmap drawingBitmap = Bitmap.createBitmap(
drawingView.getWidth(),
drawingView.getHeight(),
Bitmap.Config.ARGB_8888);
Canvas drawingCanvas = new Canvas(drawingBitmap);
drawingView.draw(drawingCanvas);
Bitmap combinedBitmap = Bitmap.createBitmap(
cameraBitmap.getWidth(),
cameraBitmap.getHeight(),
Bitmap.Config.ARGB_8888);
Canvas combinedCanvas = new Canvas(combinedBitmap);
combinedCanvas.drawBitmap(cameraBitmap, 0, 0, null);
combinedCanvas.drawBitmap(drawingBitmap, 0, 0, null);
String fileName = "IMG_" +
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.JAPAN)
.format(new Date()) + ".png";
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES + "/MyAppFolder");
ContentResolver resolver = requireActivity().getContentResolver();
Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream out = resolver.openOutputStream(uri)) {
combinedBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
} catch (Exception e) {
e.printStackTrace();
}
}
drawingView.reset();
}
6. よくある問題と対処方法(カメラ画像編集2の内容)
● タッチ座標がカメラ画像とずれる
PreviewView は内部で画像を拡大・縮小するため、 DrawingView の座標と一致しないことがあります。
→ PreviewView の幅・高さを使ってスケール補正する。
● DrawingView のサイズとカメラ画像のサイズが一致しない
保存時にズレる原因の多くはこれ。
→ DrawingView の Bitmap を cameraBitmap のサイズに合わせる。
● 2回目以降の保存で失敗する
原因:ImageProxy が close されていない、または DrawingView の状態が残っている。
java
image.close();
drawingView.reset();
● DrawingView が PreviewView を隠してしまう
DrawingView の背景を透明にする:
xml
android:background="@android:color/transparent"
6. これを利用したアプリ

【プライバシー保護】撮影と同時にデータを暗号化、写真閲覧アプリ。
撮影同時に暗号化: 毎日残したい写真(寝起き顔、体重計、体形、髪型)を撮影と同時にローカルに暗号化保存。閲覧機能も装備。
徹底したプライバシー保護: ファイルはローカル環境で暗号化・管理されます。クラウドに依存しません。
📥 広告 アプリ紹介「暗号化動画プレイヤー守」をPlayストアでチェックする
・価格:無料
・仕様:完全ローカル・通信なし
・広告:ファイル一覧下部のみ(写真・動画再生中はなし)


コメント