簡単にAndroidアプリにカメラ処理を実装する方法【コピペOK】

Androidでカメラアプリを作るには?
女の子
Androidでカメラアプリを作りたいんだけど、やり方が良くわかんなーい。
CameraXの使い方
ぴよ猫
CameraXを使うと簡単にカメラアプリを作れるよ。やり方を詳しく説明するね。

Androidでカメラアプリを作る方法

本記事の概要

CameraXライブラリを利用してAndroidアプリにカメラ機能を簡単に実装する方法を紹介します。

Androidでカメラ機能を実装する2つの方法

Androidアプリでカメラ機能を使う方法は大別すると2つあります。

  1. 自作アプリから標準カメラ機能を呼び出す
  2. 自作アプリの中にカメラ処理を実装する

1. 自作アプリから標準カメラアプリを呼び出す

自作アプリからIntentを用いて標準カメラアプリを呼び出すことで、カメラ処理を自分で実装せずに、簡単にに自作アプリにカメラ機能を組み込むことが出来ます。これが、自作アプリにカメラ機能を組み込む最もシンプルな方法です。

自作アプリから標準カメラアプリを呼び出す方法

しかし、これだと、自作アプリの中にプレビューの表示が出来ない等のデメリットがありますので、今回の記事では次に紹介する、本当に自作アプリにカメラ機能を実装する方法を紹介します。

2. 自作アプリの中にカメラ処理を実装する

Androidのカメラ操作用のライブラリを使い、自作アプリの中にカメラ処理を実装する方法です。今回、ご紹介するのはこちらの方法です。
自作アプリの中にカメラで写った画像をリアルタイムに表示することが出来るので、昨今流行りのAR(拡張現実)には欠かせない方法となります。

Androidカメラ操作用ライブラリCameraX

以前はAndroidでカメラを操作するのは、かなり難しかったのですが、つい最近CameraXというカメラ操作ライブラリがGoogleより提供され、簡単に自作アプリにカメラ処理を実装出来る様になりましたので、今回は、CameraXライブラリを作う方法を紹介します。

CameraXチュートリアル

GoogleがCameraXの使い方を紹介していますので、まずはこちらのページをご覧下さい。
CameraX のアーキテクチャ | Android Developers

このチュートリアルで分かる方は、以降の記事は読む必要がありません。ちょっとチュートリアルは分かり辛かったという方は、以降の記事にコピペすれば動くサンプルソースを載せましたので御覧ください。

Androidカメラ処理のサンプルコード

サンプル画面定義(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="mysampleapplication.MainActivity">

    <!-- プレビューエリア -->
    <TextureView
        android:id="@+id/camera"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <!-- カメラボタン -->
    <ImageButton
        android:id="@+id/capture_button"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_margin="24dp"
        app:srcCompat="@android:drawable/ic_menu_camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

カメラサンプルアプリ処理(MainActivity.java)

package mysampleapplication;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraX;

import android.Manifest;
import android.os.Bundle;
import android.widget.Toast;

import mysampleapplication.util.PermissionController;
import mysampleapplication.util.XCameraController;

import java.io.File;

/**
 * カメラ機能サンプルクラス
 */
public class MainActivity extends AppCompatActivity {
    /** 権限リクエストコード */
    private static final int MY_PERMISSIONS_REQUEST_CODE = 1242;
    /** カメラコントローラー */
    private XCameraController cameraCon = null;
    /** 権限制御コントローラー */
    private  PermissionController permissionController;
    /** 使用中のレンズ */
    private CameraX.LensFacing curentLens = CameraX.LensFacing.BACK;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // カメラコントローラー生成
        cameraCon = new XCameraController(findViewById(R.id.camera));
        // 権限制御コントローラー生成
        permissionController = new PermissionController(this,
                new String[]{Manifest.permission.CAMERA});

        // 権限確認(権限が無い場合はOSに権限設定をリクエスト。onRequestPermissionsResultが呼び出される。)
        if (permissionController.checkPermissionsAndRequest(MY_PERMISSIONS_REQUEST_CODE)) {
            // カメラプレビュー開始
            cameraCon.start(this);
        }

        // キャプチャーボタンにリスナーを登録
        findViewById(R.id.capture_button).setOnClickListener(v -> {
            // キャプチャー保存先Fileオブジェクトを作成
            File file = new File(getExternalMediaDirs()[0].getAbsolutePath() + "/test.jpg");

            // キャプチャー実行
            cameraCon.takePicture(file, (f, s, e) -> {
                // キャプチャー成功判定
                if(f != null){
                    Toast.makeText(getApplicationContext(),
                            "撮影しました。保存先: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getApplicationContext(),
                            "撮影に失敗しました。エラー:" + s, Toast.LENGTH_LONG).show();
                }
            });
        });

        findViewById(R.id.change_camera_button).setOnClickListener(v -> {
            if(curentLens == CameraX.LensFacing.BACK){
                curentLens = CameraX.LensFacing.FRONT;
            } else {
                curentLens = CameraX.LensFacing.BACK;
            }
            cameraCon.restart(this, curentLens);
        });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        // 権限設定リクエストのコールバックか判定する
        if (requestCode == MY_PERMISSIONS_REQUEST_CODE) {
            // 権限設定リクエストのコールバックの場合、権限確認を再実施
            // (権限が無い場合は権限設定画面を起動する)
            if (permissionController.checkPermissionsAndOpenSstting()) {
                // カメラプレビュー開始
                cameraCon.start(this);
            }
        }
    }
}

gradle.properties

  # AndroidX利用宣言
android.useAndroidX=true
android.enableJetifier=true

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="mysampleapplication">

      ・・・
<uses-permission android:name="android.permission.CAMERA" />
</manifest>

アプリの動作確認

Androidでカメラアプリ

おー、カメラのプレビューが表示されました!!
ボタンを押すと写真も取れます。

サンプルソースのメインの処理は数行

メインの処理は数行だけです。シンプルですね?

カメラプレビューの表示開始

cameraCon.start(this);

キャプチャーの取得

File file  = new  File(getExternalMediaDirs()[0].getAbsolutePath() + "/test.jpg");
// キャプチャー実行
cameraCon.takePicture(file, (f, s, e) -> {
    // キャプチャー成功判定
    if(f != null){
        Toast.makeText(getApplicationContext(),
                "撮影しました。保存先: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show();
    } else {
        Toast.makeText(getApplicationContext(),
                "撮影に失敗しました。エラー:" + s, Toast.LENGTH_LONG).show();
    }
});

カメラ処理の本体はクラス化しています

まあ、見て分かる通りCameraX出て来てません(^^;
見ての通り、独自クラスを用意しています。次から紹介するクラスがカメラ処理の本体です。(クラス化した方が、メイン処理と、カメラをグリグリ操作する処理を分離出来るので、クラス化しました。)

カメラ操作の本体

カメラ制御クラス(XCameraController.java)

メソッド

  • void start(LifecycleOwner lifecycleOwner, CameraX.LensFacing lensFacing)
    プレビュー開始
  • void start(LifecycleOwner lifecycleOwner, CameraX.LensFacing lensFacing)
    プレビュー再開
  • void takePicture(File targetFile, CaptureCallback callback)
    キャプチャーの取得

クラス

package mysampleapplication.util;

import android.graphics.Matrix;
import android.util.Rational;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.view.ViewGroup;

import java.io.File;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureConfig;
import androidx.camera.core.Preview;
import androidx.camera.core.PreviewConfig;
import androidx.lifecycle.LifecycleOwner;

/**
 * カメラ制御クラス
 */
public class XCameraController {
    /** プレビュー表示View */
    private TextureView previewScreen;
    /** イメージキャプチャー */
    private ImageCapture imageCapture;

    /**
     * コンストラクタ
     * @param previewScreen プレビュー表示View
     */
    public XCameraController(TextureView previewScreen){
        this.previewScreen = previewScreen;
        previewScreen.addOnLayoutChangeListener((v, left, top, right, bottom,
            oldLeft, oldTop, oldRight, oldBottom) -> updateTransform());
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }

    /**
     * プレビュー開始
     * @param lifecycleOwner プレビュー処理を行うActivity(ライフサイクルのオーナー)
     */
    public void start(LifecycleOwner lifecycleOwner){
        restart(lifecycleOwner, CameraX.LensFacing.BACK);
    }

    /**
     * プレビュー開始
     * @param lensFacing 使用するカメラ
     * @param lifecycleOwner プレビュー処理を行うActivity(ライフサイクルのオーナー)
     */
    public void start(LifecycleOwner lifecycleOwner, CameraX.LensFacing lensFacing){
        restart(lifecycleOwner, lensFacing);
    }

    /**
     * カメラを再起動する
     * @param lensFacing 使用するカメラ
     * @param lifecycleOwner プレビュー処理を行うActivity(ライフサイクルのオーナー)
     */
    public void restart(LifecycleOwner lifecycleOwner, CameraX.LensFacing lensFacing){
        CameraX.unbindAll();

        // プレビューの生成
        Preview preview = createPreview(lensFacing);
        // プレビューをライフサイクルオーナーにbindする
        CameraX.bindToLifecycle(lifecycleOwner, preview);

        // キャプチャーの生成
        imageCapture = createImageCapture(lensFacing);
        // キャプチャーをライフサイクルオーナーにbindする
        CameraX.bindToLifecycle(lifecycleOwner, preview, imageCapture);
    }

    /**
     * キャプチャーの取得
     * @param targetFile キャプチャーの保存先ファイル
     * @param callback コールバック関数
     */
    public void takePicture(File targetFile, CaptureCallback callback) {
        // キャプチャーの実行
        imageCapture.takePicture(targetFile, new ImageCapture.OnImageSavedListener(){
            @Override
            public void onError(@NonNull ImageCapture.UseCaseError useCaseError,
                                @NonNull String message,
                                @Nullable Throwable caus){
                callback.accept(null, message, caus);
            }

            @Override
            public void onImageSaved(@NonNull File file){
                callback.accept(file, null,null);
            }
        });
    }

    /**
     * プレビューの作成
     * @param lensFacing 使用するレンズ
     * @return プレビュー
     */
    private Preview createPreview(CameraX.LensFacing lensFacing){
        PreviewConfig.Builder builder = new PreviewConfig.Builder();
        // アスペクト比(1:1)
        builder.setTargetAspectRatio(new Rational(1, 1));
        // プレビューのサイズ
        builder.setTargetResolution(new Size(
                previewScreen.getWidth(), previewScreen.getHeight()));
        // 使用するレンズの設定
        builder.setLensFacing(lensFacing);

        // プレビューの作成
        Preview preview = new Preview(builder.build());
        // プレビュー更新処理をリスナー登録
        preview.setOnPreviewOutputUpdateListener((output) -> {
                ViewGroup parent = (ViewGroup)previewScreen.getParent();
                parent.removeView(previewScreen);
                parent.addView(previewScreen, 0);
                previewScreen.setSurfaceTexture(output.getSurfaceTexture());
                updateTransform();
            }
        );
        return preview;
    }

    /**
     * キャプチャーの作成
     * @param lensFacing 使用するレンズ
     * @return キャプチャー
     */
    private ImageCapture createImageCapture(CameraX.LensFacing lensFacing){
        ImageCaptureConfig.Builder builder = new ImageCaptureConfig.Builder();
        // アスペクト比(1:1)
        builder.setTargetAspectRatio(new Rational(1, 1));
        // 使用するレンズの設定
        builder.setLensFacing(lensFacing);
        // キャプチャーモード(画質よりも時間を優先)
        builder.setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY);
        // キャプチャーの作成
        return new ImageCapture(builder.build());
    }

    /**
     * プレビューの更新
     */
    private void updateTransform() {
        if(previewScreen.getWidth() == 0){
            return;
        }

        Matrix matrix = new Matrix();
        float centerX = previewScreen.getWidth() / 2f;
        float centerY = previewScreen.getHeight() / 2f;

        float rotationDegrees = 0;
        switch (previewScreen.getDisplay().getRotation()) {
            case Surface.ROTATION_0:
                rotationDegrees = 0;
                break;
            case Surface.ROTATION_90:
                rotationDegrees = 90;
                break;
            case Surface.ROTATION_180:
                rotationDegrees = 180;
                break;
            case Surface.ROTATION_270:
                rotationDegrees = 270;
                break;
        }
        matrix.postRotate(rotationDegrees * (-1), centerX, centerY);
        previewScreen.setTransform(matrix);
    }

    /**
     * キャプチャーのコールバック関数
     */
    public interface CaptureCallback {
        /**
         * キャプチャーのコールバック関数
         * @param file 保存先ファイル(キャプチャー失敗時はnull)
         * @param message エラーメッセージ(キャプチャー成功時はnull)
         * @param caus エラー(キャプチャー成功時はnull)
         */
        void accept(File file, String message, Throwable caus);
    }
}

権限制御クラス(PermissionController.java)

メソッド

  • boolean checkPermissionsAndRequest(int requestCode)
    権限の有無をチェックし権限が無い場合、権限付与をOSにリクエストする
  • boolean checkPermissionsAndOpenSstting()
    権限の有無をチェックし権限が無い場合、権限付与をOSにリクエストする。

ソース

package mysampleapplication.util;

import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.Settings;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
/**
 * アプリケーションの権限設定制御クラス
 */
public class PermissionController {
    /** 権限設定対象アプリケーションActivity */
    private AppCompatActivity activity;
    /** 設定権限リスト */
    private String[] targetPermissionList;

    /**
     * コンストラクタ
     * @param activity 権限設定対象アプリケーションActivity
     * @param targetPermissionList 設定権限リスト
     */
    public PermissionController(AppCompatActivity activity, String[] targetPermissionList){
        this.activity = activity;
        this.targetPermissionList = targetPermissionList;
    }
    /**
     * 権限の有無をチェックし権限が無い場合、権限付与をOSにリクエストする。
     * @param requestCode 権限設定リクエストコード(onRequestPermissionsResultに渡されるコード)
     * @return 権限の有無
     */
    public boolean checkPermissionsAndRequest(int requestCode){
        // 設定権限リストに記載の権限が全て設定されているか確認する
        for(String s : targetPermissionList) {
            // 権限有無を確認
            if (ContextCompat.checkSelfPermission(activity, s)
                    != PackageManager.PERMISSION_GRANTED) {
                // 権限が無い場合は、権限の付与をOSにリクエストする
                ActivityCompat.requestPermissions(activity,
                        targetPermissionList, requestCode);
                return false;
            }
        }
        return true;
    }
    /**
     * 権限の有無をチェックし権限が無い場合、権限設定画面を起動する
     * @return 権限の有無
     */
    public boolean checkPermissionsAndOpenSstting(){
        // 設定権限リストに記載の権限が全て設定されているか確認する
        for(String s : targetPermissionList) {
            // 権限有無を確認
            if (ContextCompat.checkSelfPermission(activity, s)
                    != PackageManager.PERMISSION_GRANTED) {
                // 権限が無い場合は、権限設定画面を起動する
                openPermissionsSetting();
                return false;
            }
        }
        return true;
    }
    /**
     * 権限設定画面の起動処理
     */
    private void openPermissionsSetting(){
        // 権限設定を促すダイアログを起動
        AlertDialog.Builder builder = new AlertDialog.Builder(activity);
        builder.setMessage("アプリケーションの権限設定をして下さい。")
                .setPositiveButton("OK" , (d, i) -> {
                    // 権限設定画面起動
                    Intent intent = new Intent();
                    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                    intent.setData(Uri.fromParts("package",
                            activity.getApplicationContext().getPackageName(), null));
                    activity.getApplicationContext().startActivity(intent);
                    activity.finish();
                });
        builder.show();
    }
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です