Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

У нас есть фотик и котик - работаем с камерой

Котик и фотик

Программное включение приложения Камера
Делаем фотографии и сохраняем результат. Простой пример
Миниатюра и полноразмерное изображение
Запуск камеры в нужном режиме
Снимаем и кадрируем

Практически все современные телефоны и планшеты снабжаются камерами, что позволяет фотографировать любимых котиков.

Сначала сделаем небольшие приготовления. Есть класс устройств, у которых нет камер, например, электронные ридеры. Чтобы пользователи этих устройств не скачивали зря ваше приложение, которое окажется для них бесполезным, пропишем в манифесте требование наличия камеры.


<uses-feature android:name="android.hardware.camera" android:required="true" />

Если камера в вашем приложении выполняет вспомогательную функцию, а приложение может работать и без неё, то установите значение false в предыдущем коде и проверяйте наличие камеры программно. Если камеры нет, то отключайте возможность съёмок для пользователя.


// Kotlin
val packageManager = packageManager
val isCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
println(isCamera)

// Java PackageManager packageManager = getPackageManager(); boolean isCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); System.out.println(isCamera);

Программное включение приложения Камера

Раньше на старых телефонах можно было программно запустить из своей программы системное приложение "Камера" (в этом случае вам не понадобятся дополнительные разрешения) через намерение.


Intent intent = new Intent();
intent.setAction(Intent.ACTION_CAMERA_BUTTON);
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
		KeyEvent.KEYCODE_CAMERA));
sendOrderedBroadcast(intent, null);

Особого смысла в этом нет, так как результат не возвращается в приложение. Но вполне подходило для быстрого запуска камеры. Впрочем, сейчас этот код может и не сработать.

А вообще у пользователя могут стоять разные приложения, способные фотографировать. Тогда у вас будет появляться диалоговое окно с выбором нужного приложения. Они все имеют в своём составе такую запись в манифесте (для общего развития):


<action android:name="android.media.action.IMAGE_CAPTURE"/>
<category android:name="android.intent.category.DEFAULT"/>

У Гугла есть своя программа Google Камера. Запустим её, зная имя пакета.


Intent intent = getIntent();

intent.setComponent(null);
intent.setPackage("com.google.android.GoogleCamera");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

При вызове метода getIntent() вместо new Intent() приложение запускалось сразу, иначе - выводилось диалоговое окно выбора программы из списка. Также нужно быть уверенным, что программа установлена, в примере нет кода проверки.

Делаем фотографии и сохраняем результат. Простой пример

Просто включить камеру не слишком интересно. Рассмотрим практичный пример, когда мы программно запустим приложение "Камера", а полученную фотографию сохраним в папке. Сначала сделаем простой вариант, а потом напишем более сложное приложение.

Используйте статическую константу MediaStore.ACTION_IMAGE_CAPTURE для создания намерения, которое потом нужно передать методу startActivityForResult(). Разместите на форме кнопку и ImageView, в который будем помещать полученный снимок. Этот код запускает стандартное приложение камеры. Полученное с камеры изображение можно обработать в методе onActivityResult():

Не включайте в манифест разрешение на работу с камерой, иначе получите крах приложения.


// Kotlin

// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.

package ru.alexanderklimov.photo

import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private val REQUEST_TAKE_PHOTO = 1

    private lateinit var imageView: ImageView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        imageView = findViewById(R.id.imageView)
        val button: Button = findViewById(R.id.button)

        button.setOnClickListener {
            val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            try {
                startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO)
            } catch (e: ActivityNotFoundException) {
                e.printStackTrace()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) {
			// Фотка сделана, извлекаем миниатюру картинки
            val thumbnailBitmap = data?.extras?.get("data") as Bitmap
            imageView.setImageBitmap(thumbnailBitmap)
        }
		
		// Другой вариант с применением when
		when(requestCode){
            REQUEST_TAKE_PHOTO ->{
                if(resultCode == Activity.RESULT_OK && data !== null){
                    imageView.setImageBitmap(data.extras?.get("data") as Bitmap)
                }
            }
            else ->{
                Toast.makeText(this, "Wrong request code", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

// Java private static final int REQUEST_TAKE_PHOTO = 1; private ImageView imageView; // в методе onCreate() imageView = findViewById(R.id.imageView); // Щелчок кнопки public void onClick(View v) { Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try{ startActivityForResult(takePhotoIntent, REQUEST_TAKE_PHOTO); }catch (ActivityNotFoundException e){ e.printStackTrace(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_TAKE_PHOTO && resultCode == RESULT_OK) { // Фотка сделана, извлекаем миниатюру картинки Bundle extras = data.getExtras(); Bitmap thumbnailBitmap = (Bitmap) extras.get("data"); imageView.setImageBitmap(thumbnailBitmap); } }

Данный код запустит приложение, работающее с камерой, позволяя пользователю поменять настройки изображения, что освобождает вас от необходимости создавать своё собственное приложение для этих нужд. Вполне возможно, что у вас будет несколько приложений, умеющих делать фотографии, тогда сначала появится окно выбора программы.

При тестировании примера на своём телефоне я обнаружил небольшую проблему - когда снимок передавался обратно на моё приложение, то оно находилось в альбомном режиме, а потом возвращалось в портретный режим. При этом полученный снимок терялся. Поэтому перед нажатием кнопки я поворачивал телефон в альбомный режим, чтобы пример работал корректно. Возможно следует предусмотреть подобное поведение, например, запретить приложению реагировать на поворот и таким образом избежать перезапуска Activity. У некоторых телефонов такой проблемы нет.

По умолчанию фотография возвращается в виде объекта Bitmap, содержащего миниатюру. Этот объект находится в параметре data, передаваемом в метод onActivityResult(). Чтобы получить миниатюру в виде объекта Bitmap, нужно вызвать метод getParcelableExtra() из намерения, передав ему строковое значение data. В примере использовался упрощённый вариант.

Миниатюра и полноразмерное изображение

Если вы укажете исходящий путь URI с помощью параметра MediaStore.EXTRA_OUTPUT в запущенном намерении, полноразмерное изображение, снятое камерой, сохранится в заданном месте. В таком случае в метод onActivityResult() не будет передана миниатюра, а итоговое намерение продемонстрирует значение null.

В следующем примере показано, как при создании снимка получать миниатюру или полноценное изображение, используя намерение. Изображение будет сохранено во внешнем хранилище под именем test.jpg.

Пример работает на устройствах до Android 6.0, потом появились различные системные ограничения и нужно писать дополнительный код.

Добавим разрешение на запись файла в хранилище.


<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
В примере используются устаревшие пакеты android.support, используйте пакеты AndroidX

// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.

package ru.alexanderklimov.photocamera;

import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;

import java.io.File;

public class MainActivity extends AppCompatActivity {

    private static int TAKE_PICTURE_REQUEST = 1;
    private ImageView imageView;
    private Uri outputFileUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        imageView = findViewById(R.id.imageView);
    }

    public void onClick(View view) {
        //getThumbnailPicture();
        saveFullImage();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == TAKE_PICTURE_REQUEST && resultCode == RESULT_OK) {
            // Проверяем, содержит ли результат маленькую картинку
            if (data != null) {
                if (data.hasExtra("data")) {
                    Bitmap thumbnailBitmap = data.getParcelableExtra("data");
                    // Какие-то действия с миниатюрой
                    imageView.setImageBitmap(thumbnailBitmap);
                }
            } else {
                // Какие-то действия с полноценным изображением,
                // сохранённым по адресу outputFileUri
                imageView.setImageURI(outputFileUri);
            }
        }
    }

    private void getThumbnailPicture() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        startActivityForResult(intent, TAKE_PICTURE_REQUEST);
    }

    private void saveFullImage() {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        File file = new File(Environment.getExternalStorageDirectory(),
                "test.jpg");
        outputFileUri = Uri.fromFile(file);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
        startActivityForResult(intent, TAKE_PICTURE_REQUEST);
    }
}

В реальных приложениях для создания имён файлов используют текущую дату, чтобы обеспечить уникальность и не затереть существующую фотографию.

Запуск камеры в нужном режиме

Мы можем выбрать намерение, позволяющее включить камеру в нужном режиме: фотосъёмка или видео.


private static final int REQUEST_TAKE_PHOTO = 1;
private static final int REQUEST_TAKE_VIDEO = 2;

// фотосъёмка
public void capturePhoto() {
    Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(intent, REQUEST_TAKE_PHOTO);
    }
}

// видеосъёмка
public void captureVideo() {
    Intent intent = new Intent(MediaStore.INTENT_ACTION_VIDEO_CAMERA);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(intent, REQUEST_TAKE_VIDEO);
    }
}

Снимаем и кадрируем

Рассмотрим ещё один пример с режимом кадрирования. Основная часть кода остаётся прежней. Рекомендую проверять работу с камерой на реальных устройствах, так как многие производители заменяют стандартные методы съёмки своими прошивками и драйверами. В частности, намерение с кадрированием является проблемной, и в интернете многие жалуются на отсутствие поддержки этого способа. Пример для старых устройств до Android 6.0.

Создадим простенький макет из кнопки для запуска камеры и ImageView для вывода кадрированного изображения.


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/buttonCapture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:onClick="onClick"
        android:text="Запустить камеру" />

    <ImageView
        android:id="@+id/picture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@drawable/background" />

</LinearLayout>

Для большей красоты сделаем задний фон у ImageView с закруглёнными углами и обводкой. Для этого в атрибуте android:background мы прописали специальный стиль. Создайте папку res/drawable, а в ней файл background.xml следующего содержания:


<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:dither="true" >

    <gradient
        android:angle="90"
        android:centerColor="#00000000"
        android:endColor="#99ffffff"
        android:startColor="#99ffffff" />

    <padding
        android:bottom="10dp"
        android:left="10dp"
        android:right="10dp"
        android:top="10dp" />

    <corners android:radius="5dp" />

    <stroke
        android:width="2dp"
        android:color="#ccffffff" />

</shape>

Этот шаг не является обязательным и его можно пропустить.

При нажатии кнопки запускаем приложение Камера и ожидаем результата.


public void onClick(View view) {
	try {
		// Намерение для запуска камеры
		Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
		startActivityForResult(captureIntent, CAMERA_REQUEST);
	} catch (ActivityNotFoundException e) {
		// Выводим сообщение об ошибке
		String errorMessage = "Ваше устройство не поддерживает съемку";
		Toast toast = Toast
				.makeText(this, errorMessage, Toast.LENGTH_SHORT);
		toast.show();
	}
}

После того, как пользователь сделал нужный кадр, программа Камера возвращает результат обратно в наше приложение. Результат обрабатывается в методе onActivityResult():


protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	if (resultCode == RESULT_OK) {
		// Вернулись от приложения Камера
		if (requestCode == CAMERA_REQUEST) {
			// Получим Uri снимка
			picUri = data.getData();
			// кадрируем его
			performCrop();
		}
		// Вернулись из операции кадрирования
		else if(requestCode == PIC_CROP){
			Bundle extras = data.getExtras();
			// Получим кадрированное изображение
			Bitmap thePic = extras.getParcelable("data");
			// передаём его в ImageView
			ImageView picView = (ImageView)findViewById(R.id.picture);
			picView.setImageBitmap(thePic);
		}
	}
}

Получив полноразмерное изображение, мы пытаемся откадрировать его. Для этого создадим метод performCrop(), который запускает специальное намерение, предназначенное для этих целей. В успешном случае результат снова возвращается в наше приложение, но уже с другим кодом PIC_CROP. Теперь мы имеем нужное изображение, которое можно вывести на экран.

При кадрировании мы указываем желаемые размеры (код метода ниже). Если указать слишком больше размеры (больше 400), то результат не возвращается. Попробуйте добавить ещё два параметра:


//куда сохраняем
intent.putExtra("output", picUri); 
intent.putExtra("outputFormat", "JPEG");

Результат работы приложения, когда запускается намерение кадрирования и итоговый результат. Желательно тренироваться на кошках.

Crop

Итоговый результат

Исходник полностью.


package ru.alexanderklimov.photo;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Bitmap;

import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;

import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends Activity {

	final int CAMERA_REQUEST = 1;
	final int PIC_CROP = 2;
	private Uri picUri;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		setContentView(R.layout.activity_main);
		setTitle("Съёмка и Кадрирование");
	}

	public void onClick(View v) {
		try {
			// Намерение для запуска камеры
			Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
			startActivityForResult(captureIntent, CAMERA_REQUEST);
		} catch (ActivityNotFoundException e) {
			// Выводим сообщение об ошибке
			String errorMessage = "Ваше устройство не поддерживает съемку";
			Toast toast = Toast
					.makeText(this, errorMessage, Toast.LENGTH_SHORT);
			toast.show();
		}
	}

	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		if (resultCode == RESULT_OK) {
			// Вернулись от приложения Камера
			if (requestCode == CAMERA_REQUEST) {
				// Получим Uri снимка
				picUri = data.getData();
				// кадрируем его
				performCrop();
			}
			// Вернулись из операции кадрирования
			else if(requestCode == PIC_CROP){
				Bundle extras = data.getExtras();
				// Получим кадрированное изображение
				Bitmap thePic = extras.getParcelable("data");
				// передаём его в ImageView
				ImageView picView = (ImageView)findViewById(R.id.picture);
				picView.setImageBitmap(thePic);
			}
		}
	}
	
	private void performCrop(){
		try {
			// Намерение для кадрирования. Не все устройства поддерживают его
			Intent cropIntent = new Intent("com.android.camera.action.CROP");
			cropIntent.setDataAndType(picUri, "image/*");
			cropIntent.putExtra("crop", "true");
			cropIntent.putExtra("aspectX", 1);
			cropIntent.putExtra("aspectY", 1);
			cropIntent.putExtra("outputX", 256);
			cropIntent.putExtra("outputY", 256);
			cropIntent.putExtra("return-data", true);
			startActivityForResult(cropIntent, PIC_CROP);
		}
		catch(ActivityNotFoundException anfe){
		    String errorMessage = "Извините, но ваше устройство не поддерживает кадрирование";
		    Toast toast = Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT);
		    toast.show();
		}
	}
}

Дополнительное чтение

Снимаем на камеру в Android 7.0+

Обсуждение урока на форуме

Реклама