LXF164: Android
|
|
|
Android
Программирование: Взаимодействие с web-камерой телефона
Содержание |
Делаем снимки
Не нравится программа для камеры в своем телефоне? Есть идея программы для работы с фото? Джульетта Кемп знакомит с Android Camera API.
В подавляющем большинстве телефонов с Android есть встроенные камеры, и с развитием технологий они становятся все удобнее. Программ, которые так или иначе работают с камерой, великое множество, и предоставляемое Android API максимально упрощает эту задачу.
Если вам нужен всего-то один снимок, возможно, проще воспользоваться Намерением [Intent] для активации встроенной камеры (см. врезку внизу), но если вам нужно больше рычагов управления, можно написать собственное Занятие [Activity] Camera.
На нашем уроке мы создадим Занятие, которое просто использует камеру, но, разумеется, ничто не мешает создать приложение с другим главным Занятием, где заодно будет и Занятие для камеры, вызываемое при необходимости.
Подготовка камеры
Прежде чем писать код, следует объявить требования для камеры в AndroidManifest.xml:
<manifest .... >
<uses-permission android:name=”android.permission.CAMERA” />
<uses-permission android:name=”android.permission.WRITE_EXTERNAL_STORAGE” />
<uses-feature android:name=”android.hardware.camera” />
Если вы не собираетесь сохранять изображения, WRITE_EXTERNAL_STORAGE не понадобится (но так как мы позже это сделаем, то добавим ее сразу). Директива uses-feature означает, что для установки приложения в телефоне обязательно должна быть камера. Если камера – только часть вашей программы, и пользователи прекрасно смогут работать с программой и без нее, добавьте android:required=“false” к этой строке. Также можно задать характеристики камеры; более подробно они описаны в одном из следующих разделов.
Теперь можно продолжить с кодом первоначальной настройки, в котором мы впервые получаем экземпляр объекта камеры. Метод onCreate() в MyCameraActivity должен выглядеть так:
@Override public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if (!checkCameraExists(this)) {
Toast.makeText(this, “Увы, камеры нет!”, Toast.LENGTH_LONG);
finish();
}
camera = getCameraInstance();
}
Один из параметров манифеста требует обязательного наличия камеры на устройстве, но ее наличие стоит проверить и здесь. Метод checkCameraExists() прост:
private boolean checkCameraExists(Context c) {
if (c.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
return true;
} else {
return false;
}
}
Метод getCameraInstance() ненамного сложнее:
private Camera getCameraInstance() {
Camera c = null;
try {
c = Camera.open();
} catch (Exception e) {
Toast.makeText(this, “Увы, камеры нет!”, Toast.LENGTH_LONG);
Log.e(TAG, “Нет камеры: исключение “ + e.getMessage());
e.getStackTrace();
finish();
}
return c;
}
Метод Camera.open() обращается к первой камере на задней поверхности устройства.
Камер на устройствах Android может быть несколько; для доступа к конкретной камере воспользуйтесь методом Camera.open(int cameraId). Метод Camera.getNumberOfCameras() вернет количество камер устройства, а метод Camera. getCameraInfo() – информацию о заданной камере. Для большинства задач нам подойдет первая камера на задней поверхности устройства.
В следующих разделах мы займемся просмотром изображения с камеры и созданием снимка. Но перед этим нужно сделать еще одну важную вещь: закончив работу с камерой, освободите ее.
Иначе другие процессы не смогут получить доступ к камере – это плохая практика; кроме того, это обеспокоит пользователей.
Внесем нужные изменения в метод onPause():
@Override protected void onPause() {
super.onPause();
releaseCamera();
}
private void releaseCamera() {
if (camera != null) {
camera.release();
camera = null;
}
}
@Override protected void onResume() {
if (camera == null) {
camera.getCameraInstance();
}
super.onResume();
}
Когда мы создадим код просмотра изображения с камеры, данный метод придется несколько усложнить, но мы займемся этим потом. А пока вам важнее всего помнить, что следует обязательно освобождать камеру, когда наше приложение приостанавливается.
Если нужно сделать быстрый снимок, пригодится Намерение:
{
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE);
i.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(i, CAPTURE_IMAGE_ACTIVITY_REQ);
}
protected Uri getOutputMediaFileUri(int type) {
// См. код в основном тексте для getOutputMediaFile()
return Uri.fromFile(getOutputMediaFile(type));
}
Поместите первый фрагмент кода в подходящее место; можно связать его с кнопкой, поместив в метод onClick() (как в коде на нашем DVD) или сделав пунктом меню. В этом фрагменте кода создается новое Намерение – намерение по умолчанию для создания снимка – и оно связывается с URI, указывающим, где сохранить результирующее изображение. Затем мы запускаем Намерение для запуска Занятия. Нам также нужно обработать результат:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQ) {
if (resultCode == RESULT_OK) {
if (data == null) {
// Здесь замечена ошибка! Снимок должен сохраниться в fileUri
Toast.makeText(this, “Снимок успешно сохранен”,
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, “Снимок успешно сохранен: “ + data.getData(),
Toast.LENGTH_LONG).show();
}
// Здесь вы можете сделать что-нибудь еще...
} else if (resultCode == RESULT_CANCELED){
// Пользователь отменил операцию; ничего не делаем
} else {
Toast.makeText(this, “Отказ вызова захвата изображения!”,
Toast.LENGTH_LONG).show();
}
}
}
На практике вы, наверное, захотите как-то обработать изображение. В некоторых устройствах замечена ошибка, связанная с сохранением изображения посредством ПО камеры. URI изображения должен быть возвращен вместе с Намерением. Однако иногда возвращается пустое Намерение, хотя файл был сохранен правильно. Чтобы обойти эту проблему, сохраните URi изображения, который вы отправляли с Намерением, и снова воспользуйтесь им при получении успешного результата.
Просмотр изображения с камеры
При повороте камеры в положение Пейзаж ее, кажется, можно не сбрасывать, и следует делать это только при переключении на Портрет (переключение камеры при повороте на Пейзаж вызовет «падение» при записи видео).
С помощью Camera API можно сделать снимок так, что пользователь ни о чем не узнает, но в подавляющем большинстве случаев сначала нужно показать изображение с камеры, а затем пользователь нажмет на кнопку. Для этого создадим собственный класс CameraPreview, унаследованный от SurfaceView:
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = “CameraPreview”;
private SurfaceHolder sh;
private Camera camera;
public CameraPreview(Context context, Camera cm) {
super(context);
camera = cm;
sh = getHolder();
sh.addCallback(this);
// устаревший, но требуемый pre-3.0
sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void surfaceCreated(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
} catch (IOException e) {
Log.e(TAG, “Ошибка установки предпросмотра: “ + e.getMessage());
e.getStackTrace();
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (sh.getSurface() == null) {
// Нет поверхности предпросмотра!
return;
}
// Остановка предпросмотра до внесения изменений.
try {
camera.stopPreview();
} catch (Exception e) {
// Попытка пресечь несуществующий предпросмотр
}
try {
camera.setPreviewDisplay(sh);
camera.startPreview();
} catch (Exception e) {
Log.e(TAG, “Ошибка при рестарте предпросмотра: ” + e.getMessage());
e.getStackTrace();
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// Занятие следит за освобождением предпросмотра камеры
}
}
Чтобы удачнее расположить компоненты, можно задать разные файлы main.xml в res/layout-land (для пейзажа) и res/layout-port (для портрета). Пример есть на DVD.
Этот класс унаследован от SurfaceView, осуществляющего рендеринг гораздо быстрее, чем стандартный View (и отрисовка может выполняться фоновыми нитями, чего не допускает View), что идеально для быстрой отрисовки, которая необходима нам для просмотра изображения с камеры.
Для доступа к поверхности изображения нам понадобится также реализовать интерфейс SurfaceHolder.CallBack. Здесь необходимы методы surfaceCreated(), SurfaceChanged() и surfaceDestroyed().
Мы получаем SurfaceHolder (с помощью метода родительского класса), затем добавим к ней Callback, который проинформирует клиента о любых изменениях с поверхностью.
Наша версия surfaceChanged() ничего не меняет. Желая ввести изменения, делайте это между остановкой и запуском предпросмотра. Одна из ситуаций, когда это может пригодиться – обработка изменения положения устройства (камеры и экрана) в пространстве.
В устройствах с Android версии до 2.2 предпросмотр автоматически включался как Пейзаж, и для максимальной совместимости проще всего принудительно установить режим Пейзажа, добавив атрибут к элементу activity в AndroidManifest.xml:
<activity [ ... ] android:screenOrientation=”landscape” >
Однако в версиях с 2.2 и выше это можно убрать, взамен задав положение просмотра согласно положению устройства. Добавьте такие строки в метод surfaceChanged() в CameraPreview.java между вызовами stopPreview() и startPreview():
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
camera.setDisplayOrientation(90);
}
Не забудьте удалить настройку пейзажа из AndroidManifest.xml, и вы увидите, что предпросмотр поворачивается вслед за поворотом камеры.
Создав класс CameraPreview, добавьте метод setUpLayout(), вызываемый из метода onCreate() в MyCameraActivity.
Здесь он выделен в отдельный метод, чтобы проще приостанавливать/возобновлять программу; подробности см. ниже.
private void setUpLayout() {
setContentView(R.layout.main);
preview = new CameraPreview(this, camera);
FrameLayout frame = (FrameLayout) findViewById(R.id.camera_preview);
frame.addView(preview);
}
Также нужно добавить кое-что в файл main.xml:
<?xml version=”1.0” encoding=”utf-8”?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”horizontal”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
>
<FrameLayout
android:id=”@+id/camera_preview”
android:layout_width=”fill_parent”
android:layout_height=”fill_parent”
android:layout_weight=”1”
/>
<Button
android:id=”@+id/button_capture”
android:text=”Capture”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_gravity=”center”
/>
</LinearLayout>
Кодом кнопки мы займемся в следующем разделе. Скомпилируйте и запустите программу, и на экране должно появиться изображение с камеры с кнопкой Capture [Сделать снимок] рядом, хотя и при нажатии кнопки пока ничего не произойдет.
Наконец, ранее я говорила, что после написания кода просмотра методы onPause() и onResume() нужно немного усложнить. Если просто освободить камеру, то при возобновлении приложения старый класс предпросмотра тоже попробует обратиться к объекту камеры, который у него был (и который теперь освобожден), и вызовет исключение.
Чтобы это исправить, нужно освобождать объект при приостановке приложения, а при возобновлении – создавать новый:
protected void onPause() {
releaseCamera();
super.onPause();
}
Чтобы удачнее расположить компоненты, можно задать разные файлы main.xml в res/layout-land (для пейзажа) и res/layout-port (для портрета). Пример есть на DVD.
private void releaseCamera() {
if (camera != null) {
camera.stopPreview();
camera.release();
camera = null;
preview = null;
}
}
На эмуляторе есть «камера», но она ничего не показывает (только пустой экран). Для проверки работы предпросмотра понадобится настоящее устройство.
protected void onResume() {
if (camera == null) {
camera = getCameraInstance();
setUpLayout();
}
super.onResume();
}
Теперь вы сможете выйти из программы и затем перезапустить ее без ошибок.
Создание снимка
У нас есть кнопка Capture; теперь нужно связать с ней какие-нибудь действия. Добавьте следующий код в setUpLayout():
Button captureButton = (Button) findViewById(R.id.button_capture); captureButton.setOnClickListener(
new View.OnClickListener() {
public void onClick(View v) {
getImage();
}
}
);
Метод getImage() выглядит так:
protected static final int MEDIA_TYPE_IMAGE = 0;
protected static final int MEDIA_TYPE_VIDEO = 1;
private void getImage() {
PictureCallback picture = new PictureCallback() {
public void onPictureTaken(byte[] data, Camera cam) {
File picFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (picFile == null) {
Log.e(TAG, “Ошибка при создании медиа-файла; верны ли разрешения на запись?”);
return;
}
try {
FileOutputStream fos = new FileOutputStream (picFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
Log.e(TAG, “Файл не найден: “ + e.getMessage());
e.getStackTrace();
} catch (IOException e) {
Log.e(TAG, “Ошибка I/O файла: “ + e.getMessage());
e.getStackTrace();
}
}
};
camera.takePicture(null, null, picture);
}
private File getOutputMediaFile(int type) {
File directory = new File(Environment.getExternalStorage PublicDirectory(Environment.DIRECTORY_PICTURES), getPackageName());
if (!directory.exists()) {
if (!directory.mkdirs()) {
Log.e(TAG, “Не удалось создать каталог для хранения.”);
return null;
}
}
String timeStamp = new SimpleDateFormat(“yyyMMdd_HHmmss”).format(new Date());
File file;
if (type == MEDIA_TYPE_IMAGE) {
file = new File(directory.getPath() + File.separator + “IMG_” + timeStamp + “.jpg”);
} else if (type == MEDIA_TYPE_VIDEO) {
file = new File(directory.getPath() + File.separator + “VID_” + timeStamp + “.mp4”);
} else {
return null;
}
return file;
}
В методе getOutputMediaFile() мы сохраняем фотографии в публичном каталоге на внешнем устройстве (SD-карте). Это означает, что если удалить приложение, фотографии останутся. Если вы предпочли бы их удалить, сохраняйте их в каталоге, полученном с помощью Context.getExternalFilesDir(). Однако учтите, что пользователи могут не ожидать того, что их фотографии будут удалены при удалении программы работы с камерой! Впрочем, при некоторых обстоятельствах такое имеет смысл – тщательно продумайте это для своего приложения.
Интерфейс PictureCallback предоставляет доступ к данным изображения из программы для работы с фотографиями; здесь мы получаем файл, куда нужно записать данные (что по сути занимает большую часть кода!), и записываем их в этот файл.
Настроив PictureCallback, мы сообщаем камере, что нужно снять фотографию с помощью встроенного метода takePicture(), и передаем эти данные PictureCallback при вызове этого метода (т. е. при нажатии кнопки Capture).
При попытке запустить программу сейчас вы увидите, что после создания снимка предварительный просмотр «зависает» и больше не работает. Чтобы это исправить, добавьте вызов camera.startPreview() после создания снимка.
Однако у нас остается другая проблема: пока изображение сохраняется в файл, предпросмотр будет «заморожен».
Чтобы это исправить, сохраним фотографию в фоновом режиме, создав AsyncTask:
private void getImage() {
PictureCallback picture = new PictureCallback() {
public void onPictureTaken(byte[] data, Camera cam) {
new SaveImageTask().execute(data);
camera.startPreview();
}
};
camera.takePicture(null, null, picture);
}
class SaveImageTask extends AsyncTask<byte[], String, String> {
@Override protected String doInBackground(byte[]... data) {
File picFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (picFile == null) {
Log.e(TAG, “Ошибка при создании медиа-файла; верны ли разрешения на запись?”);
return null;
}
try {
// блоки try/catch, как сделано выше...
}
return null;
}
}
Я рассказывала о нем в предыдущей статье; по сути это вспомогательный класс, от которого можно унаследоваться, чтобы легко запустить процесс в фоновой нити (обычно все, что запускается в коде, работает в одной основной нити. Помните, что интерфейс Android небезопасен с точки зрения нитей, поэтому выполняйте в фоне только задачи, не обновляющие графический интерфейс!). В данном случае мы просто выносим в фон код сохранения файла, чтобы повторно перезапустить просмотр сразу после создания снимка.
Камерой можно снимать не только фотографии, но и видео. Если вы хотите это делать, сначала установите право android.permision.RECORD_AUDIO uses-permission в AndroidManifest.xml.
Съемка видео сложнее съемки фотографий, и для нее нужно выполнить следующие действия в указанном порядке:
1. Разблокировать камеру. 2. Создать новый объект MediaRecorder и добавить в него камеру. 3. Задать источники аудио и видео. 4. Задать профиль с настройками и форматом кодирования видео, начиная с API 8 (для устройств в Android версии менее 2.2 это делается вручную). 5. Задать выходной файл. 6. Включить предпросмотр. 7. Подготовить MediaRecorder. 8. Начать запись.
Следующий код выполняет шаги 1–7:
private MediaRecorder mr;
protected boolean prepareForVideoRecording() {
camera.unlock();
mr = new MediaRecorder();
mr.setCamera(camera);
mr.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mr.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mr.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
mr.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());
mr.setPreviewDisplay(preview.getHolder().getSurface());
try {
mr.prepare();
} catch (IllegalStateException e) {
Log.e(TAG, “IllegalStateException при подготовке MediaRecorder “ + e.getMessage());
e.getStackTrace();
releaseMediaRecorder();
return false;
} catch (IOException e) {
Log.e(TAG, “IOException при подготовке MediaRecorder “ + e.getMessage());
e.getStackTrace();
releaseMediaRecorder();
return false;
}
return true;
}
Затем можно настроить кнопку для остановки и записи воспроизведения:
private void setUpVideoButton() {
videoButton = new Button(this);
setUpButton(videoButton, “Старт видео”);
videoButton.setOnClickListener(
new View.OnClickListener() {
public void onClick(View v) {
if (isRecording) {
mr.stop();
releaseMediaRecorder();
camera.lock();
videoButton.setText(“Старт видео”);
isRecording = false;
} else {
if (prepareForVideoRecording()) {
mr.start();
videoButton.setText(“Стоп видео”);
isRecording = true;
} else {
// Что-ир не в порядке! Освободите камеру
releaseMediaRecorder();
Toast.makeText(MyCameraActivity.
this, “Извините: видео не стартует”, Toast.LENGTH_LONG).
show();
}
}
}
}
);
}
Тогда вы сможете запускать и останавливать запись одной кнопкой. Задайте все настройки, затем запустите MediaRecorder, и готово. Обратите внимание, что файл просто сохраняется на внешний носитель; чтобы сделать с ним еще что-нибудь, нужно дописать код.
Возможности камеры и эффекты
В камерах современных смартфонов может быть масса дополнительных возможностей – вспышка, баланс белого, режим съемки быстро движущихся объектов, даже распознавание лиц (поддерживается только в последнем релизе Android). Чтобы узнать обо всех доступных вам возможностях, изучите API. Для выяснения, какие возможности доступны на вашем устройстве, можно воспользоваться классом Camera.Parameters. Узнав, какие параметры/возможности вам доступны, можете работать с ними единообразно. Здесь я включу вспышку; этот код легко расширить на другие возможности. Мы создадим метод setUpFlash(), который вызывается из setUpLayout():
private void setUpFlash() {
final Camera.Parameters params = camera.getParameters();
final List<String> flashList = params.getSupportedFlashModes();
if (flashList == null) {
// Вспышки нет!
return;
}
final CharSequence[] flashCS = flashList.toArray(new CharSequence[flashList.size()]);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(“Выберите тип вспышки”);
builder.setSingleChoiceItems(flashCS, -1,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
params.setFlashMode(flashList.get(which));
camera.setParameters(params);
dialog.dismiss();
}
});
final AlertDialog alert = builder.create();
alert.show();
}
Сперва проверьте, есть ли на этой камере вспышка, получив Camera.Parameters для этой камеры и поддерживаемые режимы вспышки из параметров. Если режимов нет, мы возвращаемся, ничего не делая.
Чтобы включить вспышку, используем AlertDialog (можно использовать и что-то другое, на ваше предпочтение). Диалогам AlertDialog нужен массив CharSequence для получения списка компонентов, и нам нужно создать его из списков режимов вспышки. setSingleChoiceItems() создает AlertDialog с радиокнопками; если вам нужен список без радиокнопок, воспользуйтесь setItems().
Наконец, когда один из элементов списка выбран, мы задаем соответствующий режим вспышки (учтите, что для получения нужного объекта нам нужно вернуться к flashList). Чтобы изменения вступили в силу, нужно вызвать camera.setParameters(); без этого параметры не будут установлены и режим вспышки не изменится, пока вы не сделаете снимок. После выбора параметра мы закрываем диалог для возврата в главное окно.
Создайте и покажите диалоговое окно, и все готово! Если скомпилировать и запустить программу, вы увидите, что окно появляется сразу при запуске программы, а это не то, что нам нужно. Во избежание этого, добавьте кнопку программно (см. врезку).
Чтобы улучшить внешний вид программы, используйте графическую кнопку вместо текстовой; кроме того, есть еще масса возможностей, с которыми можно поэкспериментировать при желании. Остается только вопрос, что делать со всеми этими снимками... |
Информацию о расположении компонентов лучше всего хранить в XML. Но иногда нам необходимо делать это динамически или программно – как в нашем случае, когда кнопка Flash должна появиться, только если у камеры есть вспышка. Добавьте следующий код в конец метода setUpFlash():
LinearLayout lin = (LinearLayout) findViewById(R.id.linearlayout);
Button flashButton = new Button(this);
flashButton.setText(“Flash”);
flashButton.setLayoutParams(new
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
lin.addView(flashButton);
flashButton.setOnClickListener{
new View.OnClickListener() {
public void onClick(View v) {
alert.show();
}
}
);
Этот код почти не требует пояснений; мы получаем родительский объект LinearLayout из XML, создаем кнопку и добавляем ее в LinearLayout. Также показано, как программно задать высоту и ширину кнопки. Все остальные атрибуты кнопки тоже можно задать программно.
Наконец, OnClickListener показывает диалоговое окно AlertDialog вспышки при нажатии на кнопку.