Prendre des photos

Remarque : La classe Camera, qui est mentionnée sur cette page, est obsolète. Nous vous recommandons d'utiliser CameraX ou, dans des cas d'utilisation spécifiques, Camera2. CameraX et Camera2 sont compatibles avec Android 5.0 (niveau d'API 21) ou version ultérieure.

Cette leçon explique comment prendre une photo en déléguant la tâche à une autre application Appareil photo sur l'appareil. (Si vous préférez créer les fonctionnalités de votre propre appareil photo, consultez Contrôler l'appareil photo.)

Supposons que vous implémentez un service météorologique alimenté par les utilisateurs, qui crée une carte météo mondiale en combinant les images du ciel prises par les appareils équipés de votre application cliente. L'intégration de photos ne représente qu'une petite partie de votre application. Vous voulez prendre des photos facilement, sans réinventer l'appareil photo. Heureusement, la plupart des appareils Android ont déjà d'au moins une application Appareil photo. Dans cette leçon, vous allez apprendre comment lui faire prendre une photo pour vous.

Demander la fonctionnalité Appareil photo

Si l'une des fonctions essentielles de votre application consiste à prendre des photos, limitez sa visibilité sur Google Play aux appareils équipés d'un appareil photo. Pour annoncer que votre application nécessite un appareil photo, placez une balise <uses-feature> dans le fichier manifeste :

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

Si votre application fonctionne sans avoir toutefois besoin d'un appareil photo, définissez plutôt android:required sur false. De cette façon, Google Play autorisera les appareils sans appareil photo à télécharger votre application. Vous devez ensuite vérifier la disponibilité de l'appareil photo au moment de l'exécution en appelant hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). S'il ne l'est pas, vous devez alors désactiver ses fonctionnalités.

Obtenir la vignette

Si prendre une simple photo n'est pas votre ambition majeure avec votre application, vous voudrez probablement récupérer ensuite l'image dans l'application Appareil photo pour en faire quelque chose.

L'application Appareil photo d'Android encode la photo dans l'Intent envoyé à onActivityResult() sous la forme d'un petit Bitmap en plus, sous la clé "data". Le code suivant récupère cette image et l'affiche dans une 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);
    }
}

Remarque : Cette vignette de "data" peut convenir pour une icône, mais guère plus. Traiter une image en taille réelle nécessite un peu plus de travail.

Enregistrer la photo en taille réelle

L'application Appareil photo d'Android enregistre une photo en taille réelle si vous lui donnez un fichier pour le faire. Vous devez attribuer un nom complet au fichier dans lequel l'application Appareil photo doit enregistrer la photo.

En général, toutes les photos que l'utilisateur prend avec l'appareil photo doivent être enregistrées dans l'espace de stockage externe public, afin que toutes les applications y aient accès. Le répertoire approprié pour les photos partagées est fourni par getExternalStoragePublicDirectory(), avec l'argument DIRECTORY_PICTURES. Lorsqu'il est fourni via cette méthode, le répertoire est partagé entre toutes les applications. Sous Android 9 (niveau d'API 28) ou versions antérieures, la lecture et l'écriture dans ce répertoire nécessitent respectivement les autorisations READ_EXTERNAL_STORAGE et WRITE_EXTERNAL_STORAGE :

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

Sous Android 10 (niveau d'API 29) ou version ultérieure, le répertoire approprié pour le partage de photos est la table MediaStore.Images. Vous n'avez pas besoin de déclarer d'autorisations d'accès à l'espace de stockage, tant que votre application a uniquement besoin d'accéder aux photos que l'utilisateur a prises avec elle.

Toutefois, si vous voulez que les photos restent seulement dans votre application, vous pouvez utiliser à la place le répertoire fourni par Context.getExternalFilesDir(). Sous Android 4.3 et versions antérieures, l'écriture dans ce répertoire nécessite également l'autorisation WRITE_EXTERNAL_STORAGE. À partir d'Android 4.4, l'autorisation n'est plus nécessaire, car le répertoire est inaccessible aux autres applications. Vous pouvez donc déclarer que l'autorisation ne doit être demandée que sur les versions antérieures d'Android en ajoutant l'attribut maxSdkVersion :

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

Remarque : Les fichiers que vous enregistrez dans les répertoires fournis par getExternalFilesDir() ou getFilesDir() sont supprimés lorsque l'utilisateur désinstalle votre application.

Une fois que vous avez choisi le répertoire du fichier, vous devez créer un nom de fichier "anti-collision". Vous pouvez également enregistrer le chemin d'accès dans une variable de membre pour une utilisation ultérieure. Voici un exemple de solution dans une méthode qui renvoie un nom de fichier unique pour une nouvelle photo avec un horodatage. (Dans cet exemple, nous partons du principe que vous appelez la méthode à partir d'un élément 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;
}

Avec cette méthode disponible pour créer un fichier pour la photo, vous pouvez désormais créer et appeler Intent comme suit :

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

Remarque : Nous utilisons getUriForFile(Context, String, File) qui renvoie un URI content://. Pour les applications plus récentes ciblant Android 7.0 (niveau d'API 24) et versions ultérieures, la transmission d'un URI file:// au-delà des limites d'un package entraîne un FileUriExposedException. C'est pourquoi nous présentons désormais un moyen plus générique de stocker des images à l'aide d'un FileProvider.

Vous devez maintenant configurer le FileProvider. Dans le fichier manifeste de votre application, ajoutez un fournisseur à votre application :

<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>

Assurez-vous que la chaîne d'autorités correspond au deuxième argument de getUriForFile(Context, String, File). Dans la section des métadonnées de la définition du fournisseur, vous pouvez voir que celui-ci s'attend à ce que les chemins d'accès éligibles soient configurés dans un fichier de ressources dédié, res/xml/file_paths.xml. Voici le contenu requis pour cet exemple précis :

<?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>

Le composant de chemin d'accès correspond au chemin d'accès renvoyé par getExternalFilesDir() lorsqu'il est appelé avec Environment.DIRECTORY_PICTURES. Veillez à remplacer com.example.package.name par le nom de package réel de votre application. Consultez également la documentation du FileProvider pour avoir une description détaillée des spécificateurs de chemin d'accès que vous pouvez utiliser en plus de external-path.

Ajouter la photo à une galerie

Lorsque vous créez une photo par le biais d'un intent, vous devez savoir où se trouve votre image, car vous avez indiqué où l'enregistrer en premier lieu. Pour n'importe qui d'autre, le moyen le plus simple de rendre votre photo accessible consiste à le faire à partir du fournisseur multimédia du système.

Remarque : Si vous avez enregistré votre photo dans le répertoire fourni par getExternalFilesDir(), l'outil d'analyse multimédia ne peut pas accéder aux fichiers, car ils sont privés.

L'exemple de méthode ci-dessous montre comment appeler l'outil d'analyse multimédia du système pour ajouter votre photo à la base de données du fournisseur multimédia, afin qu'elle soit disponible dans l'application Galerie d'Android et dans d'autres applications.

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

Décoder une image mise à l'échelle

Gérer plusieurs images en taille réelle peut être compliqué quand la mémoire est limitée. Si vous constatez que votre application n'a pas suffisamment de mémoire après avoir affiché seulement quelques images, vous pouvez réduire considérablement la quantité de tas de mémoire dynamique utilisée en développant le fichier JPEG dans un tableau de mémoire déjà mis à l'échelle pour correspondre à la taille de la vue de destination. L'exemple de méthode ci-dessous illustre cette technique.

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