Toma fotos

En esta lección, se describe cómo capturar una foto delegando el trabajo a otra app de cámara en el dispositivo. (Si prefieres desarrollar tu propia funcionalidad de cámara, consulta Cómo controlar la cámara).

Supongamos que estás implementando un servicio meteorológico a partir de un aporte colectivo que crea un mapa meteorológico global mediante la combinación de imágenes del cielo tomadas por dispositivos que ejecutan tu app cliente. La integración de fotos es solo una pequeña parte de la aplicación. Y el objetivo es tomar fotos con un mínimo de complicaciones, no reinventar la cámara. Afortunadamente, la mayoría de los dispositivos con Android ya tienen instalada al menos una aplicación de cámara. En esta lección, aprenderás cómo hacer que tome una foto por ti.

Solicita la función de cámara

Si una función esencial de tu aplicación es tomar fotos, restringe su visibilidad en Google Play a los dispositivos que tienen una cámara. Para anunciar que tu aplicación depende de tener una cámara, coloca una etiqueta <uses-feature> en el archivo de manifiesto:

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

Si tu aplicación usa una cámara, pero no la requiere para funcionar, configura android:required como false. De esa manera, Google Play permitirá que los dispositivos sin cámara descarguen la aplicación. Luego, es tu responsabilidad verificar la disponibilidad de la cámara en el tiempo de ejecución. Para ello, llama al hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Si no hay una cámara disponible, debes inhabilitar las funciones de la cámara.

Toma una foto con una app de cámara

La forma en que Android delega acciones a otras aplicaciones es mediante la invocación de un Intent que describe lo que deseas que se haga. Este proceso tiene tres partes: el Intent en sí, una llamada para iniciar la Activity externa y un poco de código para manejar los datos de la imagen cuando el foco vuelve a tu actividad.

La siguiente es una función que invoca un intent para capturar una 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);
        }
    }
    

Observa que el método startActivityForResult() está protegido por una condición que llama a resolveActivity(), que muestra el primer componente de actividad que puede manejar el intent. Realizar esta verificación es importante porque si llamas a startActivityForResult() con un intent que ninguna app puede manejar, tu app fallará. Por lo tanto, siempre que el resultado no sea nulo, se puede usar el intent.

Obtén la miniatura

Si la simple tarea de tomar una foto no es el objetivo principal de tu app, seguramente desees recuperar la imagen de la aplicación de cámara y hacer algo con ella.

La aplicación de cámara de Android codifica la foto en el Intent de devolución que se entrega a onActivityResult() como un Bitmap pequeño en la carpeta Extras, en los "data" clave. El siguiente código recupera esta imagen y la muestra en un 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);
        }
    }
    

Nota: Esta imagen en miniatura de "data" podría usarse para un ícono, pero no para mucho más. Usar una imagen de tamaño original requiere un poco más de trabajo.

Guarda la foto de tamaño original

La aplicación de cámara de Android guarda una foto de tamaño original si proporcionas un archivo para hacerlo. Debes incluir un nombre de archivo completo donde la app de cámara debe guardar la foto.

En general, todas las fotos que el usuario captura con la cámara del dispositivo deben guardarse en el almacenamiento externo público del dispositivo para que todas las apps puedan acceder a ellas. getExternalStoragePublicDirectory() proporciona el directorio adecuado para las fotos compartidas, con el argumento DIRECTORY_PICTURES. Debido a que el directorio proporcionado por este método se comparte con todas las apps, para leer y escribir allí, se requieren los permisos READ_EXTERNAL_STORAGE y WRITE_EXTERNAL_STORAGE, respectivamente. El permiso de escritura permite de manera implícita la lectura, por lo que, si necesitas escribir en el almacenamiento externo, debes solicitar solo un permiso:

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

Sin embargo, si deseas que las fotos sean privadas para tu app, puedes usar el directorio que proporciona getExternalFilesDir() en su lugar. En Android 4.3 y versiones anteriores, para escribir en este directorio, también se requiere el permiso WRITE_EXTERNAL_STORAGE. A partir de Android 4.4, el permiso ya no es necesario porque otras apps no pueden acceder al directorio. Por lo tanto, para declarar que el permiso solo debe solicitarse en las versiones anteriores de Android, agrega el atributo maxSdkVersion:

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

Nota: Los archivos que guardes en los directorios que proporcionan getExternalFilesDir() o getFilesDir() se borrarán cuando el usuario desinstale la app.

Una vez que elijas el directorio para el archivo, deberás crear un nombre de archivo resistente a colisiones. Es posible que también desees guardar la ruta en una variable de miembro para su uso posterior. A continuación, se incluye una solución de ejemplo en un método que muestra un nombre de archivo único para una foto nueva usando una marca de fecha y 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;
    }
    

Con este método disponible para crear un archivo para la foto, ahora puedes crear el Intent y, luego, invocarlo de esta manera:

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);
            }
        }
    }
    

Nota: Utilizamos getUriForFile(Context, String, File), que devuelve un URI de content://. Para las apps más recientes dirigidas a Android 7.0 (API nivel 24) y versiones posteriores, pasar un URI de file:// a través de un límite de paquete provoca un FileUriExposedException. Por lo tanto, presentamos una forma más genérica de almacenar imágenes con un FileProvider.

Ahora debes configurar FileProvider. En el manifiesto de la app, agrega un proveedor a la aplicación:
    <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>
    
Asegúrate de que la string de autoridades coincida con el segundo argumento en getUriForFile(Context, String, File). En la sección de metadatos de la definición del proveedor, puedes ver que el proveedor espera que las rutas aptas se configuren en un archivo de recursos exclusivo, res/xml/file_paths.xml. El siguiente es el contenido requerido para este ejemplo en particular:
    <?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>
    
El componente de ruta corresponde a la ruta que muestra getExternalFilesDir() cuando se llama con Environment.DIRECTORY_PICTURES. Asegúrate de reemplazar com.example.package.name con el nombre del paquete real de tu app. Además, consulta la documentación de FileProvider para obtener una descripción detallada de los especificadores de ruta que puedes usar aparte de external-path.

Agrega la foto a una galería

Cuando creas una foto a través de un intent, debes saber dónde se encuentra tu imagen, porque indicaste dónde guardarla en primer lugar. Para todos los demás, quizás la forma más fácil de hacer que su foto sea accesible es desde el proveedor de contenido multimedia del sistema.

Nota: Si guardaste la foto en el directorio que proporciona getExternalFilesDir(), el escáner multimedia no podrá acceder a los archivos, porque son privados de tu app.

En el siguiente método de ejemplo, se muestra cómo invocar el escáner multimedia del sistema para agregar la foto a la base de datos del proveedor de contenido multimedia a fin de que esté disponible en la aplicación de Galería de Android y en otras 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);
    }
    

Decodifica una imagen ajustada a escala

Administrar múltiples imágenes de tamaño original puede ser complicado con memoria limitada. Si ves que tu aplicación se está quedando sin memoria después de mostrar solo algunas imágenes, puedes reducir drásticamente la cantidad de almacenamiento dinámico utilizado con la expansión del JPEG a un arreglo de memoria que ya esté ajustado a escala para coincidir con el tamaño de la vista de destino. En el siguiente método de ejemplo, se muestra esta 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);
    }