Androidスパイカメラ

Hi%username%! 今日は、1つのAndroidアプリケーションを開発した経験と、カメラをまったく正直に使用しないと直面する困難を共有したいと思います。

Sentinelアプリのアイデアは長い間開発部門に存在していましたが、2年前に最初の実装がSymbianプラットフォームに登場しました。 アイデア自体は簡単です-携帯電話を手に取った人の写真を撮ってください。 最初の実装では、アプリケーションはシグナルモジュールとコールバックモジュールに分割されました。 信号モジュールは、電話の特定の状態の変化を記録する役割を果たしました。 たとえば、SIMカードまたはメモリカードの取り外しまたは取り付け、着信または発信、または非常に注意が必要です-メインセンサーは加速度計センサーで、これにより電話がテーブルから持ち上げられた瞬間を判断します。 コールバックモジュールは、センサー信号に基づいて実行されるアクションです。 写真と録音が実装されました。

アプリケーションをAndroidプラットフォームに移植すると、アプローチが著しく変わりました。 そして一般に、古いアプリケーションからのアイデアのみが残り、モジュール式ではなくなり、すべての機能のうち写真機能のみが残りました。 この機能の実装についてお話したいと思います。



写真を撮る



まず、カメラの使用に関する公式文書の無料翻訳を提供します。



<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />
      
      





必要な画像を取得するには:

  1. getNumberOfCamerasおよびgetCameraInfoメソッドを使用して、目的のカメラのIDを見つけます );
  2. openメソッドを使用して、カメラオブジェクトへのリンクを取得します。
  3. getParametersメソッドを使用して、現在の(デフォルトの)設定を取得します。
  4. 必要に応じて、パラメータを変更し、 setParametersメソッドを使用して再度設定します。
  5. 必要に応じて、 setDisplayOrientationメソッドを使用してカメラの向きを設定します(垂直ビデオにはNO!)。
  6. 重要:正しく初期化されたSurfaceHolderオブジェクトをsetPreviewDisplayメソッドに渡します。 これを行わないと、カメラはプレビューを開始できません。
  7. 重要: startPreviewメソッドを呼び出して、 SurfaceHolderの更新を開始します。 写真を撮る前にプレビューを開始する必要があります。
  8. 最後に、 takePictureメソッドを呼び出して、データがonPictureTakenに戻るのを待ちます。
  9. takePictureメソッドを呼び出した後、プレビューが停止します。 別の写真を撮る必要がある場合は、startPreviewを再度呼び出す必要があります。
  10. カメラが不要になった場合、まずstopPreviewメソッドを使用してプレビューを停止する必要があります。
  11. 重要: release()メソッドを呼び出して、他のアプリケーション用にカメラリソースを解放します。 アプリケーションは、 onPauseメソッドでカメラリソースをすぐに解放する(およびonResumeメソッドで元に戻す)必要があります。


このクラスはスレッドセーフではありません。 ほとんどの操作(プレビュー、フォーカス、写真キャプチャ)は非同期であり、openメソッドが呼び出されたのと同じスレッドで呼び出されるコールバックを介して結果を返します。 このクラスのメソッドを一度に複数のスレッドから呼び出さないでください。

警告: Android OS上のデバイスによって、カメラの機能(解像度、オートフォーカスなど)が異なる場合があります。



ここで翻訳が終了し、楽しみが始まります。

上記のすべてのうち、次の問題が顕著です。

  1. プレビューを表示する必要があります。
  2. さまざまなデバイスで、カメラはさまざまな方法で機能します。


私たちは彼らと戦います。

「ドック上では、これを行うことができないと書かれている」というカテゴリから問題が発生した場合、最初にする必要があるのはソースを調べることです。 それらから、プレビューのレンダリングがネイティブコードsetPreviewDisplay(Surface)のレベルになっていることが明らかになりました。 プレビューを開始したかどうかをシステムが一般的に決定する方法をすばやく把握しようとしました。 私はC ++のとげをすぐに通過することができなかったので、最も抵抗の少ない道をたどりました-プレビューを作成しましたが、ユーザーに気付かれずに表示しました。 stackoverflowで検索すると、動的に作成されたsetPreviewDisplay SurfaceHolderに渡す別の方法を見つけることができます。 また、オブジェクトはアクティビティマークアップに追加されないため、表示されません。 残念ながら、この方法は古いバージョンのAndroidでのみ機能します(間違えなければ3.0まで)。 新しいバージョンでは、開発者はこの誤解を修正しました。

したがって、私たちは唯一の結論に達します-どうにかしてプレビューを画面に表示する必要があります、唯一の質問は今それが気付かれずに実行できるかどうかです? 幸いなことに、答えはイエスです。 そして、これはあなたがこれに必要なものです:

  1. 透明なアクティビティ。
  2. アクティビティの左上隅にある1 x 1ピクセルのFrameLayout。


透過的なアクティビティは、マニフェストの1行で実行されます。このため、次のように定義します。

 <activity android:name=".activities.CameraActivity" android:exported="false" android:launchMode="singleTask" android:excludeFromRecents="true" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
      
      





次の簡単なレイアウトを作成します。

 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/surfaceHolder" android:layout_width="1.0px" android:layout_height="1.0px" />
      
      





SurfaceHolderオブジェクトが作成され、マークアップに動的に追加されます。 原則として、それをマークアップにすぐに追加することが可能でした。この瞬間は、必要に応じてマークアップに入らないようにコードに入れ、オブジェクトの動作を再定義しました。

透明なアクティビティがあります。SurfaceHolderを動的に作成します。次は何ですか? 次に、カメラを初期化して写真を撮ります。 ここでのアイデアは、アクティビティの開始時に写真を撮り、できるだけ早くそれを閉じることです。 アクティビティを次のように定義します。

 public class CameraActivity extends Activity implements Camera.PictureCallback, SurfaceHolder.Callback { private static final int NO_FRONT_CAMERA = -1; private Camera mCamera; private boolean mPreviewIsRunning = false; private boolean mIsTakingPicture = false; public class CameraPreview extends SurfaceView { public CameraPreview(Context context) { super(context); } } ...
      
      







したがって、SurfaceHolder(surfaceCreated、surfaceChanged、surfaceDestroyed)およびCamera(onPictureTaken)からのイベントはそこにストリーミングされます。 内部クラスのCameraPreviewは、前述したように、必要に応じてSurfaceViewの動作をすばやく簡単に変更できるようにするためにのみ必要です。 次に、アクティビティメソッドをまとめて引用します。



いくつかのコード
 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.surface_holder); SurfaceView surfaceView = new CameraPreview(this); ((FrameLayout) findViewById(R.id.surfaceHolder)).addView(surfaceView); SurfaceHolder holder = surfaceView.getHolder(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); holder.addCallback(this); } @Override protected void onResume() { startPreview(); super.onResume(); } @Override protected void onPause() { stopPreview(); super.onPause(); } @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { final int cameraId = getFrontCameraId(); if (cameraId != NO_FRONT_CAMERA) { try { mCamera = Camera.open(cameraId); Camera.Parameters parameters = mCamera.getParameters(); if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) parameters.setRotation(270); List<String> flashModes = parameters.getSupportedFlashModes(); if (flashModes != null && flashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); List<String> whiteBalance = parameters.getSupportedWhiteBalance(); if (whiteBalance != null && whiteBalance.contains(Camera.Parameters.WHITE_BALANCE_AUTO)) parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO); List<String> focusModes = parameters.getSupportedFocusModes(); if (focusModes != null && focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); List<Camera.Size> sizes = parameters.getSupportedPictureSizes(); if (sizes != null && sizes.size() > 0) { Camera.Size size = sizes.get(0); parameters.setPictureSize(size.width, size.height); } List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes(); if (previewSizes != null) { Camera.Size previewSize = previewSizes.get(previewSizes.size() - 1); parameters.setPreviewSize(previewSize.width, previewSize.height); } mCamera.setParameters(parameters); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) mCamera.enableShutterSound(false); } catch (RuntimeException e) { A.handleException(e, true); finish(); return; } } else { Log.e(Value.LOG_TAG, "Could not find front-facing camera"); finish(); return; } try { mCamera.setPreviewDisplay(surfaceHolder); } catch (IOException ioe) { A.handleException(ioe, true); finish(); } } @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) { startPreview(); } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { releaseCamera(); } @Override public void onPictureTaken(byte[] bytes, Camera camera) { mIsTakingPicture = false; releaseCamera(); //noinspection PrimitiveArrayArgumentToVariableArgMethod new SaveImageTask().execute(bytes); finish(); } private int getFrontCameraId() { final int numberOfCameras = Camera.getNumberOfCameras(); for (int i = 0; i < numberOfCameras; i++) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(i, info); if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) return i; } return NO_FRONT_CAMERA; } private void startPreview() { if (!mPreviewIsRunning && mCamera != null) { try { mCamera.startPreview(); mCamera.autoFocus(new Camera.AutoFocusCallback() { @Override public void onAutoFocus(boolean b, Camera camera) { if (!mIsTakingPicture) { try { mIsTakingPicture = true; mCamera.setPreviewCallback(null); mCamera.takePicture(null, null, CameraActivity.this); } catch (RuntimeException e) { A.handleException(e, true); finish(); } } } }); mPreviewIsRunning = true; } catch (Exception e) { A.handleException(e, true); finish(); } } } private void stopPreview() { if (!mIsTakingPicture && mPreviewIsRunning && mCamera != null) { mCamera.stopPreview(); mPreviewIsRunning = false; } } private void releaseCamera() { if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } }
      
      







このコードで興味深いのは何ですか? ポイントにサインします。

  1. 最も重要なことは、メソッド呼び出しメソッドです。 ドキュメントには、何をどの順序で呼び出す必要があるかが記載されていますが、いつ表示されるかは示されていません。 たとえば、setPreviewDisplayメソッド。 カメラを初期化し、onCreateまたはonResumeでこのメソッドをすぐに呼び出すと、写真は機能しません。 それでは、このメソッドをいつ呼び出すのかをどのようにして知るのでしょうか? 正しい答えは、ソース内のsetPreviewDisplayメソッドのコメントからです。 そこからの短い抜粋です:

    このメソッドが呼び出されるとき、android.view.SurfaceHolderにはすでにサーフェスが含まれている必要があります。 android.view.SurfaceViewを使用している場合は、android.view.SurfaceHolder.Callbackをandroid.view.SurfaceHolder.addCallback(android.view.SurfaceHolder.Callback)に登録し、andandroid.view.SurfaceHolder.Callback.surfaceCreatedを待つ必要があります( android.view.SurfaceHolder)setPreviewDisplay()を呼び出す前、またはプレビューを開始する前。

    このメソッドは、startPreview()の前に呼び出す必要があります。

  2. 2番目のポイントは、アクティビティに対するSurfaceHolderオブジェクトのライフサイクルに関連しています。 アクティビティのライフサイクルはドキュメントに記載されていますが、SurfaceHolderではすべてが明確ではないため、経験的に把握する必要がありました。



     onCreate(savedInstanceStateのバンドル)
     onResume()
     onPause()
     surfaceCreated(SurfaceHolder surfaceHolder)
     surfaceChanged(SurfaceHolder surfaceHolder、int形式、int幅、int高さ)
     onStop()
     surfaceDestroyed(SurfaceHolder surfaceHolder)
    


  3. 次の興味深い点は、アクティビティライフサイクルメソッドの呼び出しの順序に関連しています。 「if(mCamera!= Null)および変数mPreviewIsRunning、mIsTakingPictureの精神でこれらのすべてのチェックが必要なのはなぜですか?」 残念ながら、この場合に私ができる唯一の答えはそれです。 また、ここでのポイントは、状況によっては、アクティビティライフサイクルメソッドの呼び出しの順序が公式ドックで指定されている順序と異なる場合があることです( たとえば 、この図から)。 基本的に、電話のロック画面がオンのときにインシデントが発生します。 onStopメソッドが続けて2回呼び出された後、何も起こらなかったようにonStartをバイパスして、onResumeが呼び出された場合がありました。 この場合、同じバージョンのAndroidが搭載されていても、メソッド呼び出しメソッドはデバイスによって異なる場合があります。 長い間、私はこれを理解しようとして、なぜこれが起こっているのかを理解しようとしました。 その結果、私はそれに多くの時間を費やし、現在の実装を作成しました。


それで、何が起こっているのかを要約する時が来ました。 アプリケーションで行われることは次のとおりです。

  1. 目的のイベントでアクティビティを開始します(私の場合、画面を含めることで)。
  2. onCreateでSurfaceHolderを作成し、コールバックを受信するアクティビティを登録します。
  3. surfaceCreatedの呼び出しを待って、その中でカメラを初期化します。
  4. カメラが初期化された後、takePictureの呼び出しを試みます。 メソッドの呼び出し順序は、デバイス、OSバージョン、および画面ロックのタイプに大きく依存するため、onResume | surfaceChangedはプレビューを開始し、onPauseはプレビューを停止します。 また、onResume | onPauseは、surfaceCreatedの前後に発生する可能性があるため、すべての場所でカメラの「初期化」を確認します。
  5. ドキュメントによると、surfaceChangedメソッドは、surfaceCreatedの後に少なくとも1回呼び出されることが保証されていますが、理論的には、写真を撮るプロセスで必要な回数だけ呼び出すことができます。 誤ってプレビューを数回開始しないように、変数mPreviewIsRunningを追加します。 プレビューを開始し、takePictureを呼び出して待機します。
  6. onPictureTakenで写真をキャッチします。 カメラを解放し、AsyncTaskを作成して写真を保存し、アクティビティを閉じます。


したがって、呼び出しの一般的な順序は次のとおりです。



 onCreate(savedInstanceStateのバンドル)
 onResume()
 onPause()
 surfaceCreated(SurfaceHolder surfaceHolder)
 surfaceChanged(SurfaceHolder surfaceHolder、int形式、int幅、int高さ)
 onPictureTaken(バイト[]バイト、カメラカメラ)
 onStop()
 surfaceDestroyed(SurfaceHolder surfaceHolder)


おわりに



アプリケーションは動作し、携帯電話で安定して写真を撮ります(Nexus 4)。 それに加えて、Motorola Droid RAZRやHTS Sensationなどの他のモデルでテストしました。 上で述べたように、異なる電話では、カメラの動作は異なります。 一部の携帯電話では、写真を撮るとシャッター音が聞こえます。 その他の場合-写真が間違った方向に回転し、EXIFを編集することによってのみ修正されます。 一部の電話では、まったく(シェルの特性のためだと思います)、Activityライフサイクルメソッドを呼び出す手順は著しく異なる場合があります。 これはすべて、Android上の膨大な数のデバイスのメーカーだけでなく、OS自体の信じられないほどの断片化とも関連しています(このテーマに関する興味深いメモは、2014年のHackerマガジンの57ページにあります)。 したがって、私は非常にしたい:

  1. さまざまな電話モデルのプロファイルを追加し、このプロファイルに基づいて写真を撮ります。 たとえば、撮影時にシャッター音を出す携帯電話の場合、撮影の直前にミュートを追加します。
  2. 多数のテストモデルを十分に活用してアプリケーションを操作し、Activityメソッドの呼び出しの違いの理由を理解してください。
  3. Androidのソースコードを詳しく調べてください。 最後に、ネイティブ部分に入り、プレビューを初期化した後にのみtakePictureを呼び出すことができる理由を理解します。 これに対処する方法を考えてください。




これは近い将来の開発の問題です。

これで、アプリケーションは現在のバージョンでGoogle.Playで利用できます。 作成の主な目標はAndroidの深さを調査することだったため、無料です。 興味のある方は、 google.playへリンク

ご清聴ありがとうございました!



All Articles