Tirar fotos

Esta lição ensina como capturar uma foto delegando o trabalho a outro app de câmera no dispositivo. Caso prefira criar seu próprio recurso de câmera, consulte Como controlar a câmera.

Suponha que você esteja implementando um serviço meteorológico com colaboração coletiva, que cria um mapa meteorológico global misturando fotos do céu tiradas por dispositivos que executam seu app cliente. Integrar as fotos é só uma pequena parte do aplicativo. O objetivo é tirar fotos com o mínimo de complicações possível, e não reinventar a câmera. A maioria dos dispositivos Android já tem pelo menos um aplicativo de câmera instalado. Nesta lição, você aprende a fazer com que ele tire uma foto para você.

Solicitar o recurso de câmera

Se uma função essencial do seu aplicativo é tirar fotos, restrinja a visibilidade dele no Google Play para dispositivos que tenham câmera. Para anunciar que seu aplicativo depende de uma câmera, coloque uma tag <uses-feature> no arquivo de manifesto:

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

Se seu aplicativo usa, mas não precisa de uma câmera para funcionar, configure android:required como false. Com isso, o Google Play permitirá que dispositivos sem câmera façam o download do seu aplicativo. Será sua responsabilidade verificar a disponibilidade da câmera no tempo de execução, chamando hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Se uma câmera não estiver disponível, os recursos da câmera precisarão ser desativados.

Tirar uma foto com um app de câmera

O Android delega ações a outros aplicativos invocando um Intent que descreva o que você quer que seja feito. Esse processo envolve três partes: o próprio Intent, uma chamada para iniciar o Activity externo e um código para processar o vídeo quando o foco retorna à sua atividade.

Veja uma função que invoca um intent para capturar uma foto.

Kotlin

    val REQUEST_IMAGE_CAPTURE = 1

    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            takePictureIntent.resolveActivity(packageManager)?.also {
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
            }
        }
    }
    

Java

    static final int REQUEST_IMAGE_CAPTURE = 1;

    private void dispatchTakePictureIntent() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
    }
    

O método startActivityForResult() é protegido por uma condição que chama resolveActivity(), que retorna o primeiro componente de atividade que pode processar o intent. Fazer essa verificação é importante porque, se você chamar startActivityForResult() usando um intent que nenhum app pode processar, seu app falhará. Portanto, desde que o resultado não seja nulo, é seguro usar o intent.

Ver a miniatura

Se o simples ato de tirar uma foto não é o principal objetivo do seu app, você provavelmente desejará recuperar a imagem do aplicativo da câmera e fazer algo com ela.

O aplicativo Câmera do Android codifica a foto no Intent de retorno entregue ao onActivityResult() como um pequeno Bitmap nos extras, sob a chave "data". O código a seguir recupera essa imagem e a exibe em ImageView.

Kotlin

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            val imageBitmap = data.extras.get("data") as Bitmap
            imageView.setImageBitmap(imageBitmap)
        }
    }
    

Java

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            Bundle extras = data.getExtras();
            Bitmap imageBitmap = (Bitmap) extras.get("data");
            imageView.setImageBitmap(imageBitmap);
        }
    }
    

Observação: essa imagem de miniatura de "data" pode ser útil para um ícone, mas não é útil para outros fins. Processar uma imagem no tamanho original exige um pouco mais de trabalho.

Salvar a foto no tamanho original

O aplicativo Câmera do Android salva uma foto em tamanho original se você fornecer um arquivo em que ela possa ser salva. Você precisa fornecer um nome de arquivo totalmente qualificado em que o app da câmera possa salvar a foto.

Geralmente, as fotos tiradas pelo usuário com a câmera do dispositivo são salvas no armazenamento externo público do dispositivo, para que possam ser acessadas por todos os apps. O diretório adequado para as fotos compartilhadas é fornecido pelo getExternalStoragePublicDirectory(), com o argumento DIRECTORY_PICTURES. Como o diretório fornecido por esse método é compartilhado por todos os apps, a leitura e gravação nele exigem as permissões READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE, respectivamente. Implicitamente, a permissão de gravação permite a leitura. Portanto, se você precisar gravar no armazenamento externo, será necessário solicitar apenas uma permissão:

    <manifest ...>
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        ...
    </manifest>
    

Mas se você quiser que as fotos permaneçam privadas apenas para seu app, poderá usar o diretório fornecido por getExternalFilesDir(). No Android 4.3 e versões anteriores, a gravação nesse diretório também requer a permissão WRITE_EXTERNAL_STORAGE. Desde o Android 4.4, a permissão não é mais necessária porque o diretório não pode ser acessado por outros apps. Assim, você pode declarar que a permissão precisa ser solicitada somente nas versões anteriores do Android, adicionando o atributo maxSdkVersion:

    <manifest ...>
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                         android:maxSdkVersion="18" />
        ...
    </manifest>
    

Observação: os arquivos salvos nos diretórios fornecidos por getExternalFilesDir() ou getFilesDir() são excluídos quando o usuário desinstala o app.

Depois de decidir o diretório do arquivo, é necessário criar um nome de arquivo resistente a colisões. Você também pode salvar o caminho em uma variável de membro para uso posterior. Veja um exemplo de solução em um método que retorna um nome de arquivo exclusivo para uma nova foto, usando um carimbo de data e hora:

Kotlin

    var currentPhotoPath: String

    @Throws(IOException::class)
    private fun createImageFile(): File {
        // Create an image file name
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir: File = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(
                "JPEG_${timeStamp}_", /* prefix */
                ".jpg", /* suffix */
                storageDir /* directory */
        ).apply {
            // Save a file: path for use with ACTION_VIEW intents
            currentPhotoPath = absolutePath
        }
    }
    

Java

    String currentPhotoPath;

    private File createImageFile() throws IOException {
        // Create an image file name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String imageFileName = "JPEG_" + timeStamp + "_";
        File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File image = File.createTempFile(
            imageFileName,  /* prefix */
            ".jpg",         /* suffix */
            storageDir      /* directory */
        );

        // Save a file: path for use with ACTION_VIEW intents
        currentPhotoPath = image.getAbsolutePath();
        return image;
    }
    

Com esse método disponível para criar um arquivo para a foto, você pode criar e invocar o Intent da seguinte forma:

Kotlin

    val REQUEST_TAKE_PHOTO = 1

    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            // Ensure that there's a camera activity to handle the intent
            takePictureIntent.resolveActivity(packageManager)?.also {
                // Create the File where the photo should go
                val photoFile: File? = try {
                    createImageFile()
                } catch (ex: IOException) {
                    // Error occurred while creating the File
                    ...
                    null
                }
                // Continue only if the File was successfully created
                photoFile?.also {
                    val photoURI: Uri = FileProvider.getUriForFile(
                            this,
                            "com.example.android.fileprovider",
                            it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO)
                }
            }
        }
    }
    

Java

    static final int REQUEST_TAKE_PHOTO = 1;

    private void dispatchTakePictureIntent() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // Ensure that there's a camera activity to handle the intent
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            // Create the File where the photo should go
            File photoFile = null;
            try {
                photoFile = createImageFile();
            } catch (IOException ex) {
                // Error occurred while creating the File
                ...
            }
            // Continue only if the File was successfully created
            if (photoFile != null) {
                Uri photoURI = FileProvider.getUriForFile(this,
                                                      "com.example.android.fileprovider",
                                                      photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
            }
        }
    }
    

Observação: estamos usando getUriForFile(Context, String, File), que retorna um URI content://. Para apps mais recentes destinados ao Android 7.0 (API de nível 24) e versões posteriores, passar um URI de file:// em um limite de pacote causa um FileUriExposedException. Portanto, apresentamos agora uma maneira mais genérica de armazenar imagens usando um FileProvider.

Agora, você precisa configurar o FileProvider. No manifesto do app, adicione um provedor ao seu aplicativo:
    <application>
       ...
       <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.android.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"></meta-data>
        </provider>
        ...
    </application>
    
Verifique se a string de autoridades corresponde ao segundo argumento para getUriForFile(Context, String, File). Na seção de metadados da definição do provedor, é possível ver que o provedor espera que caminhos qualificados sejam configurados em um arquivo de recurso dedicado, res/xml/file_paths.xml . Veja o conteúdo necessário para este exemplo específico:
    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" />
    </paths>
    
O componente do caminho corresponde ao caminho retornado por getExternalFilesDir() quando chamado com Environment.DIRECTORY_PICTURES. Substitua com.example.package.name pelo nome real do pacote do seu app. Além disso, confira a documentação de FileProvider para ver uma descrição detalhada dos especificadores de caminho que podem ser usados além de external-path.

Adicionar a foto a uma galeria

Ao criar uma foto por meio de um intent, você saberá onde sua imagem está localizada, já que você disse onde salvá-la. Para todas as outras pessoas, é provável que a maneira mais fácil de disponibilizar sua foto seja torná-la acessível pelo provedor de mídia do sistema.

Observação: se você salvou sua foto no diretório fornecido pelo getExternalFilesDir(), o detector de mídia não pode acessar os arquivos, porque eles são particulares do seu app.

O exemplo de método a seguir demonstra como invocar o detector de mídia do sistema para adicionar sua foto ao banco de dados do provedor de mídia, disponibilizando-a no aplicativo Galeria do Android e em outros apps.

Kotlin

    private fun galleryAddPic() {
        Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent ->
            val f = File(currentPhotoPath)
            mediaScanIntent.data = Uri.fromFile(f)
            sendBroadcast(mediaScanIntent)
        }
    }
    

Java

    private void galleryAddPic() {
        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        File f = new File(currentPhotoPath);
        Uri contentUri = Uri.fromFile(f);
        mediaScanIntent.setData(contentUri);
        this.sendBroadcast(mediaScanIntent);
    }
    

Decodificar uma imagem dimensionada

Gerenciar várias imagens em tamanho original com memória limitada pode ser complicado. Se seu aplicativo estiver com memória insuficiente depois de exibir algumas imagens, é possível reduzir drasticamente a quantidade de heap dinâmico expandindo o JPEG em uma matriz de memória dimensionada para corresponder ao tamanho da visualização de destino. O exemplo de método a seguir demonstra essa técnica.

Kotlin

    private fun setPic() {
        // Get the dimensions of the View
        val targetW: Int = imageView.width
        val targetH: Int = imageView.height

        val bmOptions = BitmapFactory.Options().apply {
            // Get the dimensions of the bitmap
            inJustDecodeBounds = true

            val photoW: Int = outWidth
            val photoH: Int = outHeight

            // Determine how much to scale down the image
            val scaleFactor: Int = Math.min(photoW / targetW, photoH / targetH)

            // Decode the image file into a Bitmap sized to fill the View
            inJustDecodeBounds = false
            inSampleSize = scaleFactor
            inPurgeable = true
        }
        BitmapFactory.decodeFile(currentPhotoPath, bmOptions)?.also { bitmap ->
            imageView.setImageBitmap(bitmap)
        }
    }
    

Java

    private void setPic() {
        // Get the dimensions of the View
        int targetW = imageView.getWidth();
        int targetH = imageView.getHeight();

        // Get the dimensions of the bitmap
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;

        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        // Determine how much to scale down the image
        int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

        // Decode the image file into a Bitmap sized to fill the View
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        bmOptions.inPurgeable = true;

        Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
        imageView.setImageBitmap(bitmap);
    }