Android: 実用的なカメラプレビューの実装(1)

今回は、より実用的な実装を目指して、いろいろと手を加えてしまいました。

追加した機能は以下の通りです。
・マルチカメラ対応
・表示する位置の指定
・隙間なく表示するモード
・プレビュー準備完了コールバック
・プレビューのフレーム毎/ワンショットコールバック
・信頼性向上の施策

とりあえずコードはここにあります。
https://github.com/pikanji/CameraPreviewSample/tree/practical

下記コマンドで取得できます。
git clone git://github.com/pikanji/CameraPreviewSample.git

説明が必要と思われるところだけ抜粋して説明していきます。

マルチカメラ対応

まずはマルチカメラ対応ですが、これはユーザにカメラIDを指定させ
Camera.openの引数として渡すだけです。

public CameraPreview(Activity activity, int cameraId, LayoutMode mode) {

    . . .

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
        if (Camera.getNumberOfCameras() > cameraId) {
            mCameraId = cameraId;
        } else {
            mCameraId = 0;
        }
    } else {
        mCameraId = 0;
    }
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
    if (null == mCamera) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            mCamera = Camera.open(mCameraId);
        } else {
            mCamera = Camera.open();
        }
        Camera.Parameters cameraParams = mCamera.getParameters();
        mPreviewSizeList = cameraParams.getSupportedPreviewSizes();
        mPictureSizeList = cameraParams.getSupportedPictureSizes();
    }

    . . .
}

コンストラクタでカメラIDをもらっています。
通常0が一般的なバックカメラで、あれば1がフロントカメラのようです。

surfaceCreatedでカメラをopenしていますが、Gingerbread以上でマルチカメラに対応しているのでopen(int)やgetNumberOfCamera()はGingerbreadより前のバージョンでは実行されない様にします。

表示する位置の指定

表示する位置は、大きさをプレビューを挿入するレイアウトで指定し、中心座標をCameraPreview.setCenterPositionで設定して使用します。
下記コードは使用例になります。

RelativeLayout layout = new RelativeLayout(...);
preview = new CameraPreview(this, cameraId, CameraPreview.LayoutMode.FitToParent);
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.height = 500;
params.width = 500;
preview.setCenterPosition(270, 130);
layout.addView(mPreview, 0, previewLayoutParams);

プレビュークラスでの実装は後述するadjustSurfaceLayoutSizeメソッドでセンターポジションからRelativeLayoutのleftMarginとtopMarginを算出して設定しています。

隙間なく表示するモード

次に、隙間なく表示するモードと書きましたが、これまでは下図のように表示範囲の中にプレビューが全部収まる代わりに隙間ができていました。赤い枠が指定した表示エリアだと考えてください。

そこで、下図のようにプレビューの一部がはみ出てでも、この隙間がなくなるようにするモードを実装しまいした。

ただし、プレビューの一部を画面の外にはみ出して表示する方法が分からない(おそらく、今のところ不可能)ので、プレビューが画面の中に十分に収まっている事が正常動作の条件となります。

仮に、画面から数値的にはみ出る事になる場合は、はみ出る分、画像がつぶれて見える事になります。前述した位置やサイズを画面からプレビューの指定領域がはみ出ないように設定する必要があります。

実装は選択用にLayoutModeという列挙型を定義し、そのモードに応じてプレビューを拡大縮小する倍率を大きい方か小さい方か選択します。

public static enum LayoutMode {
    FitToParent,
    NoBlank
};

an adjustSurfaceLayoutSize(Camera.Size previewSize, boolean portrait, int availableWidth, int availableHeight) {
    float tmpLayoutHeight, tmpLayoutWidth;
    if (portrait) {
        tmpLayoutHeight = previewSize.width;
        tmpLayoutWidth = previewSize.height;
    } else {
        tmpLayoutHeight = previewSize.height;
        tmpLayoutWidth = previewSize.width;
    }

    float factH, factW, fact;
    factH = availableHeight / tmpLayoutHeight;
    factW = availableWidth / tmpLayoutWidth;
    if (mLayoutMode == LayoutMode.FitToParent) {
        if (factH < factW) {
            fact = factH;
        } else {
            fact = factW;
        }
    } else {
        if (factH < factW) {
            fact = factW;
        } else {
            fact = factH;
        }
    }

    RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)this.getLayoutParams();

    int layoutHeight = (int) (tmpLayoutHeight * fact);
    int layoutWidth = (int) (tmpLayoutWidth * fact);
    if (DEBUGGING) {
        Log.v(LOG_TAG, "Preview Layout Size - w: " + layoutWidth + ", h: " + layoutHeight);
        Log.v(LOG_TAG, "Scale factor: " + fact);
    }

    boolean layoutChanged;
    if ((layoutWidth != this.getWidth()) || (layoutHeight != this.getHeight())) {
        layoutParams.height = layoutHeight;
        layoutParams.width = layoutWidth;
        if (mCenterPosX >= 0) {
            layoutParams.topMargin = mCenterPosY - (layoutHeight / 2);
            layoutParams.leftMargin = mCenterPosX - (layoutWidth / 2);
        }
        this.setLayoutParams(layoutParams);
        layoutChanged = true;
    } else {
        layoutChanged = false;
    }

    return layoutChanged;
}

上記の17から31行目で表示エリアに対するプレビューサイズの倍率を求めています。
前のバージョンまでは縦と横の倍率の小さい方を適用する事で、表示エリアに収まる最大のサイズにしていましたが、大きい方の倍率を使用する事で表示エリアに隙間ができない様なサイズにプレビューを拡大縮小します。

また、表示する位置の指定でセンターポジションを設定していましたが、この47、48行目でマージンのパラメータに変換して設定しています。

コールバックの実装

そして、プレビューの表示が完了したときに呼ばれるコールバック機能も実装しました。私が開発していたもので必要だったんでw
下記コードの様にインタフェースを定義しコールバックオブジェクトをセットするという、いたってシンプルな物です。

public interface PreviewReadyCallback {
    public void onPreviewReady();
}

PreviewReadyCallback mPreviewReadyCallback = null;

public void setOnPreviewReady(PreviewReadyCallback cb) {
    mPreviewReadyCallback = cb;
}

Camera.startPreview()を呼んだ後に下記のようにコールバックのメソッドを実行します。

if (null != mPreviewReadyCallback) {
    mPreviewReadyCallback.onPreviewReady();
}

そして、プレビューのフレーム毎/ワンショットコールバックも設定できる様にします。
Cameraオブジェクトをカプセル化しているので、ただのラッパですがアクセスできる様にしました。

public void setOneShotPreviewCallback(PreviewCallback callback) {
    if (null == mCamera) {
        return;
    }
    mCamera.setOneShotPreviewCallback(callback);
}

public void setPreviewCallback(PreviewCallback callback) {
    if (null == mCamera) {
        return;
    }
    mCamera.setPreviewCallback(callback);
}

まだ他にもコールバックが必要な場合はあると思いますが、同じ要領で追加できるので説明は割愛します。

長くなったので、信頼性向上の施策については次回という事で。

カメラを使う Index

Android:開発備忘録 Index

コメントを残す