Scattare foto

Nota:questa pagina fa riferimento alla classe Camera, che è stata ritirata. Ti consigliamo di utilizzare CameraX o, per casi d'uso specifici, Camera2. Sia CameraX che Camera2 supportano Android 5.0 (livello API 21) e versioni successive.

Questa lezione spiega come scattare una foto delegando l'operazione a un'altra app per fotocamera sul dispositivo. Se preferisci creare la tua funzionalità della fotocamera, consulta Controllare la fotocamera.

Supponiamo che tu stia implementando un servizio meteo in crowdsourcing che crea una mappa meteo globale unendo le immagini del cielo scattate dai dispositivi che eseguono la tua app client. L'integrazione delle foto è solo una piccola parte della tua applicazione. Vuoi scattare foto con il minimo sforzo, non reinventare la fotocamera. Fortunatamente, sulla maggior parte dei dispositivi Android è già installata almeno un'applicazione per la fotocamera. In questa lezione imparerai a fargli scattare una foto per te.

Richiedi la funzionalità della fotocamera

Se una funzione essenziale della tua applicazione è scattare foto, limita la sua visibilità su Google Play ai dispositivi dotati di fotocamera. Per indicare che la tua applicazione richiede una fotocamera, inserisci un tag <uses-feature> nel file manifest:

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

Se la tua applicazione utilizza, ma non richiede, una fotocamera per funzionare, imposta android:required su false. In questo modo, Google Play consentirà ai dispositivi senza fotocamera di scaricare la tua applicazione. È quindi tua responsabilità verificare la disponibilità della fotocamera in fase di runtime chiamando hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Se una videocamera non è disponibile, devi disattivare le relative funzionalità.

Ottieni la miniatura

Se l'azione di base di scattare una foto non è l'obiettivo principale della tua app, probabilmente vorrai recuperare l'immagine dall'applicazione della fotocamera e utilizzarla.

L'applicazione Fotocamera di Android codifica la foto nell'Intent restituito inviato a onActivityResult() come un piccolo Bitmap negli extra, sotto la chiave "data". Il seguente codice recupera questa immagine e la visualizza in 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:questa miniatura di "data" potrebbe essere adatta per un'icona, ma non per molto altro. La gestione di un'immagine a grandezza originale richiede un po' più di lavoro.

Salva la foto a grandezza originale

L'applicazione Fotocamera di Android salva una foto a grandezza originale se le fornisci un file in cui salvarla. Devi fornire un nome file completo in cui l'app della fotocamera deve salvare la foto.

In genere, tutte le foto acquisite dall'utente con la fotocamera del dispositivo devono essere salvate sul dispositivo nello spazio di archiviazione esterno pubblico in modo che siano accessibili a tutte le app. La directory corretta per le foto condivise viene fornita da getExternalStoragePublicDirectory(), con l'argomento DIRECTORY_PICTURES. La directory fornita da questo metodo è condivisa tra tutte le app. Su Android 9 (livello API 28) e versioni precedenti, la lettura e la scrittura in questa directory richiedono rispettivamente le autorizzazioni READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE:

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

Su Android 10 (livello API 29) e versioni successive, la directory corretta per la condivisione delle foto è la tabella MediaStore.Images. Non è necessario dichiarare alcuna autorizzazione di archiviazione, a condizione che la tua app debba accedere solo alle foto scattate dall'utente utilizzando la tua app.

Tuttavia, se vuoi che le foto rimangano private solo per la tua app, puoi utilizzare la directory fornita da Context.getExternalFilesDir(). Su Android 4.3 e versioni precedenti, la scrittura in questa directory richiede anche l'autorizzazione WRITE_EXTERNAL_STORAGE. A partire da Android 4.4, l'autorizzazione non è più richiesta perché la directory non è accessibile da altre app, quindi puoi dichiarare che l'autorizzazione deve essere richiesta solo nelle versioni precedenti di Android aggiungendo l'attributo maxSdkVersion:

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

Nota:i file salvati nelle directory fornite da getExternalFilesDir() o getFilesDir() vengono eliminati quando l'utente disinstalla l'app.

Una volta decisa la directory del file, devi creare un nome file resistente alle collisioni. Potresti anche salvare il percorso in una variabile membro per utilizzarlo in un secondo momento. Ecco un esempio di soluzione in un metodo che restituisce un nome file univoco per una nuova foto utilizzando un timestamp di data e ora. Questo esempio presuppone che tu stia chiamando il metodo dall'interno di 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 questo metodo disponibile per creare un file per la foto, ora puoi creare e richiamare Intent in questo modo:

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: utilizziamo getUriForFile(Context, String, File), che restituisce un URI content://. Per le app più recenti che hanno come target Android 7.0 (livello API 24) e versioni successive, il passaggio di un URI file:// attraverso un confine del pacchetto causa un FileUriExposedException. Pertanto, ora presentiamo un modo più generico di archiviare le immagini utilizzando un FileProvider.

Ora devi configurare FileProvider. Nel file manifest dell'app, aggiungi un provider alla tua applicazione:

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

Assicurati che la stringa delle autorità corrisponda al secondo argomento di getUriForFile(Context, String, File). Nella sezione dei metadati della definizione del provider, puoi vedere che il provider prevede che i percorsi idonei siano configurati in un file di risorse dedicato, res/xml/file_paths.xml. Ecco i contenuti richiesti per questo esempio specifico:

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

Il componente del percorso corrisponde al percorso restituito da getExternalFilesDir() quando viene chiamato con Environment.DIRECTORY_PICTURES. Assicurati di sostituire com.example.package.name con il nome del pacchetto effettivo della tua app. Consulta anche la documentazione di FileProvider per una descrizione dettagliata degli identificatori di percorso che puoi utilizzare oltre a external-path.

Aggiungi la foto a una galleria

Quando crei una foto tramite un intent, dovresti sapere dove si trova l'immagine, perché hai indicato dove salvarla in un primo momento. Per tutti gli altri, forse il modo più semplice per rendere accessibile la tua foto è renderla accessibile dal provider di contenuti multimediali del sistema.

Nota: se hai salvato la foto nella directory fornita da getExternalFilesDir(), lo scanner multimediale non può accedere ai file perché sono privati per la tua app.

Il seguente metodo di esempio mostra come richiamare lo scanner multimediale del sistema per aggiungere la tua foto al database del provider di contenuti multimediali, rendendola disponibile nell'applicazione Galleria Android e in altre app.

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 un'immagine scalata

La gestione di più immagini di dimensioni originali può essere difficile con una memoria limitata. Se la tua applicazione esaurisce la memoria dopo aver visualizzato solo alcune immagini, puoi ridurre drasticamente la quantità di heap dinamico utilizzato espandendo il JPEG in un array di memoria già ridimensionato in modo da corrispondere alle dimensioni della visualizzazione di destinazione. Il seguente metodo di esempio illustra questa tecnica.

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