- PVSM.RU - https://www.pvsm.ru -
Привет, %username%! Сегодня я хочу поделиться опытом разработки одного приложения для Android и трудностями, с которыми пришлось столкнуться при не совсем честном использовании камеры.
Идея приложения «Страж» жила внутри отдела разработки достаточно давно, но первая реализация появилась на платформе Symbian 2 года назад. Сама идея незамысловата – делать фотографии человека, взявшего телефон в руки. В первой реализации приложение было разделено на сигнальные модули и модули обратных вызовов. Сигнальные модули отвечали за регистрацию изменений определённого состояния телефона. Например: извлечение или установка SIM-карты или карты памяти, входящий или исходящий звонок, или совсем хитрые – главным сенсором был сенсор акселерометра, который определял момент поднятия телефона со стола. Модули обратных вызовов – это действия, которые выполняются по сигналам сенсоров. Были реализованы фотография и запись звука.
При портировании приложения на платформу Android подход заметно поменялся. Да и вообще от старого приложения осталась только идея, оно перестало быть модульным, а из всего функционала остался только функционал фотографирования. О реализации этого функционала и хочется рассказать.
Сначала приведу вольный перевод официальной документации, касающейся вопроса пользования камерой.
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
Чтобы получить картинку нужно:
Данный класс не потокобезопасный. Большинство операций (превью, фокусировка, получение фото) асинхронны и возвращают результат через коллбэки, которые будут вызваны в том же потоке, в котором был вызван метод open. Методы данного класса ни в коем случае не должны вызываться сразу из нескольких потоков.
Предупреждение: Разные устройства на ОС Android могут иметь разные возможности камеры (например, разрешение, возможность автофокусировки и т.п.).
Здесь перевод заканчивается и начинается самое интересное.
Из всего вышеперечисленного в глаза бросаются следующие проблемы:
С ними-то мы и будем бороться.
Когда возникает проблема из разряда «в доках написано, что так сделать нельзя», перво-наперво нужно заглянуть в исходники. Из них стало понятно, что прорисовка превью вынесена на уровень нативного кода setPreviewDisplay(Surface). Была принята попытка быстро разобраться в том, как вообще система определяет, стартовали мы превью или нет. Быстро пробраться через тернии C++ кода не получилось, поэтому я пошёл по пути наименьшего сопротивления — создал превью, но отобразил его незаметно для пользователя. Если поискать на stackoverflow, то можно найти другой способ – передавать в setPreviewDisplay SurfaceHolder, созданный динамически. А раз объект не добавлен в разметку Activity, то и отображаться он не будет. К сожалению, данный метод работает только для старых версий Android (до 3.0, если не ошибаюсь). В новых версиях разработчики исправили данное недоразумение.
Таким образом, приходим к единственному выводу – мы должны так или иначе отобразить превью на экране, вопрос теперь только в том, можно ли сделать это незаметно? К счастью, ответ – «да, можно». И вот что для этого нужно:
Прозрачное Activity делается одной строчкой манифеста, для этого определим её так:
<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 создается и добавляется в разметку динамически. В принципе можно было добавить его сразу в разметку, данный момент был вынесен в код, чтобы не лезть в разметку при необходимости переопределить поведение объекта.
Итак, прозрачное Activity есть, SurfaceHolder создаем динамически, что дальше? Дальше дело за главным – инициализировать камеру и сделать фото. Идея здесь в том, чтобы сделать фото сразу на старте Activity и закрыть её как можно быстрее. Определим нашу Activity так:
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 в случае необходимости. Далее приведу скопом методы Activity
@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;
}
}
Что интересного в данном коде? Распишу по пунктам.
The android.view.SurfaceHolder must already contain a surface when this method is called. If you are usingandroid.view.SurfaceView, you will need to register a android.view.SurfaceHolder.Callback withandroid.view.SurfaceHolder.addCallback(android.view.SurfaceHolder.Callback) and wait forandroid.view.SurfaceHolder.Callback.surfaceCreated(android.view.SurfaceHolder) before calling setPreviewDisplay() or starting preview.
This method must be called before startPreview().
onCreate(Bundle savedInstanceState) onResume() onPause() surfaceCreated(SurfaceHolder surfaceHolder) surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) onStop() surfaceDestroyed(SurfaceHolder surfaceHolder)
Итак, настало время немного обобщить происходящее. Вот что происходит в приложении:
Таким образом, общий порядок вызовов получается следующий:
onCreate(Bundle savedInstanceState) onResume() onPause() surfaceCreated(SurfaceHolder surfaceHolder) surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) onPictureTaken(byte[] bytes, Camera camera) onStop() surfaceDestroyed(SurfaceHolder surfaceHolder)
Приложение работает и стабильно делает фотки на моём телефоне (Nexus 4). Кроме него тестировал и на других моделях, в том числе Motorola Droid RAZR и HTС Sensation. Как я уже упоминал выше – на разных телефонах камеры работают по-разному. На некоторых телефонах, когда делается фото, слышен звук затвора. На других – фотография повернута не в ту сторону и исправляется это только редактированием EXIF’а. На некоторых телефонах и вовсе (я полагаю, из-за особенностей оболочки) порядок вызова методов жизненного цикла Activity может заметно отличаться. Связано всё это не только с огромным количеством производителей устройств на Android’е, но и с невероятной фрагментацией самой ОС (интересную заметку по этому поводу можно найти на 57 странице 1 номера журнала «Хакер» за 2014 год). Поэтому очень сильно хотелось бы:

Это все вопрос развития в недалеком будущем.
Сейчас же приложение доступно на Google.Play в текущей версии. Оно бесплатно, поскольку главной целью при его создании было исследование глубин Андроида. Для интересующихся ссылка на google.play [17].
Спасибо за внимание!
Автор: bejibx
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/56992
Ссылки в тексте:
[1] Camera: http://developer.android.com/reference/android/hardware/Camera.html
[2] getNumberOfCameras: http://developer.android.com/reference/android/hardware/Camera.html#getNumberOfCameras()
[3] getCameraInfo: http://developer.android.com/reference/android/hardware/Camera.html#getCameraInfo(int, android.hardware.Camera.CameraInfo)
[4] open: http://developer.android.com/reference/android/hardware/Camera.html#open(int)
[5] getParameters: http://developer.android.com/reference/android/hardware/Camera.html#getParameters()
[6] setParameters: http://developer.android.com/reference/android/hardware/Camera.html#setParameters(android.hardware.Camera.Parameters)
[7] setDisplayOrientation: http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
[8] setPreviewDisplay: http://developer.android.com/reference/android/hardware/Camera.html#setPreviewDisplay(android.view.SurfaceHolder)
[9] SurfaceHolder: http://developer.android.com/reference/android/view/SurfaceHolder.html
[10] startPreview: http://developer.android.com/reference/android/hardware/Camera.html#startPreview()
[11] takePicture: http://developer.android.com/reference/android/hardware/Camera.html#takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback)
[12] onPictureTaken: http://developer.android.com/reference/android/hardware/Camera.PictureCallback.html#onPictureTaken(byte[], android.hardware.Camera)
[13] onPause: http://developer.android.com/reference/android/app/Activity.html#onPause()
[14] onResume: http://developer.android.com/reference/android/app/Activity.html#onResume()
[15] setPreviewDisplay: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.3_r2.1/android/hardware/Camera.java#Camera.setPreviewDisplay%28android.view.SurfaceHolder%29
[16] например: http://developer.android.com/images/activity_lifecycle.png
[17] ссылка на google.play: https://play.google.com/store/apps/details?id=com.vulkan.guardian
[18] Источник: http://habrahabr.ru/post/215693/
Нажмите здесь для печати.