Robienie zdjęć

Ta strona odnosi się do klasy Camera, która została wycofana. Zalecamy używanie CameraX lub, w określonych przypadkach, Camera2. Zarówno CameraX, jak i Camera2 obsługują Androida 5.0 (poziom interfejsu API 21) i nowsze wersje.

Aby dowiedzieć się, jak przejść z Camera1 na CameraX, przeczytaj artykuł Migracja z Camera1 na CameraX. Możesz też użyć umiejętności agentowej do migracji z Camera1 na CameraX.

Z tej lekcji dowiesz się, jak zrobić zdjęcie, przekazując to zadanie innej aplikacji aparatu na urządzeniu. (Jeśli wolisz utworzyć własną funkcję aparatu, zapoznaj się z artykułem Sterowanie aparatem).

Załóżmy, że wdrażasz usługę pogodową opartą na crowdsourcingu, która tworzy globalną mapę pogodową, łącząc zdjęcia nieba zrobione przez urządzenia z Twoją aplikacją kliencką. Integracja zdjęć to tylko niewielka część Twojej aplikacji. Chcesz robić zdjęcia bez zbędnych komplikacji, a nie wymyślać aparat na nowo. Na szczęście większość urządzeń z Androidem ma już zainstalowaną co najmniej 1 aplikację aparatu. Z tej lekcji dowiesz się, jak zrobić zdjęcie za pomocą Gemini.

Poproś o funkcję aparatu

Jeśli podstawową funkcją aplikacji jest robienie zdjęć, ogranicz jej widoczność w Google Play do urządzeń z aparatem. Aby poinformować, że aplikacja wymaga aparatu, umieść w pliku manifestu tag <uses-feature>:

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

Jeśli aplikacja używa aparatu, ale nie wymaga go do działania, ustaw android:required na false. Dzięki temu Google Play pozwoli na pobieranie aplikacji na urządzenia bez aparatu. Następnie musisz sprawdzić dostępność aparatu w czasie działania, wywołując funkcję hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Jeśli kamera jest niedostępna, wyłącz jej funkcje.

Pobieranie miniatury

Jeśli podstawowa funkcja robienia zdjęć nie jest jedyną ambicją Twojej aplikacji, prawdopodobnie chcesz pobrać obraz z aplikacji aparatu i coś z nim zrobić.

Aplikacja Aparat na Androida koduje zdjęcie w obiekcie Intent zwracanym do onActivityResult() jako mały obiekt Bitmap w dodatkach pod kluczem "data". Poniższy kod pobiera ten obraz i wyświetla go w 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);
    }
}

Uwaga: ta miniatura z aplikacji "data" może być dobra jako ikona, ale nie nadaje się do innych celów. Praca z obrazem w pełnym rozmiarze wymaga nieco więcej wysiłku.

Zapisywanie zdjęcia w pełnym rozmiarze

Aplikacja Aparat na Androida zapisuje zdjęcie w pełnym rozmiarze, jeśli podasz jej plik, w którym ma je zapisać. Musisz podać pełną i jednoznaczną nazwę pliku, pod którą aplikacja aparatu ma zapisać zdjęcie.

Zasadniczo wszystkie zdjęcia zrobione przez użytkownika aparatem urządzenia powinny być zapisywane na urządzeniu w publicznej pamięci zewnętrznej, aby były dostępne dla wszystkich aplikacji. Właściwy katalog udostępnionych zdjęć jest podawany przez getExternalStoragePublicDirectory() z argumentem DIRECTORY_PICTURES. Katalog udostępniany przez tę metodę jest współdzielony przez wszystkie aplikacje. W Androidzie 9 (poziom API 28) i starszych wersjach odczyt i zapis w tym katalogu wymagają odpowiednio uprawnień READ_EXTERNAL_STORAGE i WRITE_EXTERNAL_STORAGE:

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

W przypadku Androida 10 (poziom interfejsu API 29) i nowszych wersji odpowiednim katalogiem do udostępniania zdjęć jest tabela MediaStore.Images. Nie musisz deklarować żadnych uprawnień do pamięci, o ile aplikacja potrzebuje dostępu tylko do zdjęć zrobionych przez użytkownika za jej pomocą.

Jeśli jednak chcesz, aby zdjęcia były dostępne tylko dla Twojej aplikacji, możesz użyć katalogu udostępnianego przez Context.getExternalFilesDir(). Na Androidzie 4.3 i starszych wersjach zapisywanie w tym katalogu wymaga też uprawnienia WRITE_EXTERNAL_STORAGE. Począwszy od Androida 4.4, uprawnienie nie jest już wymagane, ponieważ katalog nie jest dostępny dla innych aplikacji. Możesz więc zadeklarować, że uprawnienie powinno być wymagane tylko w przypadku niższych wersji Androida, dodając atrybut maxSdkVersion:

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

Uwaga: pliki zapisane w katalogach udostępnianych przez getExternalFilesDir() lub getFilesDir() są usuwane, gdy użytkownik odinstaluje aplikację.

Gdy wybierzesz katalog dla pliku, musisz utworzyć nazwę pliku odporną na kolizje. Możesz też zapisać ścieżkę w zmiennej składowej, aby użyć jej później. Oto przykładowe rozwiązanie w metodzie, która zwraca unikalną nazwę pliku nowego zdjęcia z sygnaturą daty i godziny. (W tym przykładzie zakładamy, że wywołujesz metodę z poziomu 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;
}

Dzięki tej metodzie tworzenia pliku ze zdjęciem możesz teraz utworzyć i wywołać funkcję Intent w ten sposób:

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

Uwaga: używamy getUriForFile(Context, String, File), który zwraca identyfikator URI content://. W przypadku nowszych aplikacji kierowanych na Androida 7.0 (poziom interfejsu API 24) lub nowszego przekazanie identyfikatora URI file:// przez granicę pakietu powoduje błąd FileUriExposedException. Dlatego teraz przedstawiamy bardziej ogólny sposób przechowywania obrazów za pomocą FileProvider.

Teraz musisz skonfigurować FileProvider. W pliku manifestu aplikacji dodaj dostawcę do aplikacji:

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

Upewnij się, że ciąg znaków authorities pasuje do drugiego argumentu getUriForFile(Context, String, File). W sekcji metadanych definicji dostawcy widać, że dostawca oczekuje, że odpowiednie ścieżki zostaną skonfigurowane w dedykowanym pliku zasobów res/xml/file_paths.xml. Oto treść wymagana w tym konkretnym przykładzie:

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

Komponent ścieżki odpowiada ścieżce zwracanej przez getExternalFilesDir(), gdy jest wywoływana z Environment.DIRECTORY_PICTURES. Pamiętaj, aby zastąpić com.example.package.name rzeczywistą nazwą pakietu aplikacji. Zapoznaj się też z dokumentacją FileProvider, aby uzyskać szczegółowy opis specyfikatorów ścieżki, których możesz używać oprócz external-path.

Dodaj zdjęcie do galerii.

Gdy tworzysz zdjęcie za pomocą intencji, musisz wiedzieć, gdzie się ono znajduje, ponieważ najpierw musisz określić, gdzie ma zostać zapisane. W przypadku innych osób najprostszym sposobem na udostępnienie zdjęcia może być udostępnienie go z systemowego dostawcy multimediów.

Uwaga: jeśli zapiszesz zdjęcie w katalogu udostępnionym przez getExternalFilesDir(), skaner multimediów nie będzie mógł uzyskać dostępu do plików, ponieważ są one prywatne dla Twojej aplikacji.

Poniższa przykładowa metoda pokazuje, jak wywołać systemowy skaner multimediów, aby dodać zdjęcie do bazy danych dostawcy multimediów i udostępnić je w aplikacji Galeria na Androidzie oraz w innych aplikacjach.

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

Dekodowanie skalowanego obrazu

Zarządzanie wieloma obrazami w pełnym rozmiarze może być trudne, gdy pamięć jest ograniczona. Jeśli po wyświetleniu kilku obrazów w aplikacji zacznie brakować pamięci, możesz znacznie zmniejszyć ilość używanej dynamicznej sterty, rozszerzając plik JPEG do tablicy pamięci, która jest już skalowana do rozmiaru widoku docelowego. Tę technikę ilustruje poniższa przykładowa metoda.

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