Cómo tomar fotos

Nota: En esta página, se hace referencia a la clase Camera, que dejó de estar disponible. Te recomendamos que uses CameraX o, en casos de uso específicos, Camera2. CameraX y Camera2 admiten Android 5.0 (nivel de API 21) y versiones posteriores.

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 climático 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. Tu 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.

Cómo solicitar 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 en su lugar. 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 durante el tiempo de ejecución llamando a hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Si no hay una cámara disponible, debes inhabilitar las funciones de la cámara.

Cómo obtener 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 una 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.

Cómo guardar 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. El directorio proporcionado por este método se comparte con todas las apps. En Android 9 (nivel de API 28) y versiones anteriores, la lectura y escritura en este directorio requiere los permisos READ_EXTERNAL_STORAGE y WRITE_EXTERNAL_STORAGE, respectivamente:

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

En Android 10 (nivel de API 29) y versiones posteriores, el directorio adecuado para compartir fotos es la tabla MediaStore.Images. No necesitas declarar ningún permiso de almacenamiento, siempre que tu app solo necesite acceder a las fotos que el usuario tomó con esta.

Sin embargo, si deseas que las fotos sean privadas para tu app, puedes usar el directorio que proporciona Context.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="28" />
    ...
</manifest>

Nota: Los archivos que guardes en los directorios proporcionados por getExternalFilesDir() o getFilesDir() se borrarán cuando el usuario desinstale tu 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. En este ejemplo, se supone que llamas al método desde un Context.

Kotlin

lateinit 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

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

Java

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

Nota: Utilizamos getUriForFile(Context, String, File), que muestra un URI de content://. Para las apps más recientes dirigidas a Android 7.0 (nivel de API 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="androidx.core.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 cadena 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-files-path name="my_images" path="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.

Cómo agregar 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 tu 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 con el objetivo 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);
}

Cómo decodificar 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 array 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

        BitmapFactory.decodeFile(currentPhotoPath, bmOptions)

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

        // Determine how much to scale down the image
        val scaleFactor: Int = Math.max(1, 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;

    BitmapFactory.decodeFile(currentPhotoPath, bmOptions);

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

    // Determine how much to scale down the image
    int scaleFactor = Math.max(1, 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);
}