Robienie zdjęć

Uwaga: ta strona dotyczy klasy Camera, która została wycofana. Zalecamy korzystanie z CameraX lub, w przypadku określonych zastosowań, z Camera2. Zarówno CameraX, jak i Camera2 obsługują Androida 5.0 (poziom interfejsu API 21) i nowsze.

Z tej lekcji dowiesz się, jak zrobić zdjęcie, powierzając to zadanie innej aplikacji do zdjęć na urządzeniu. (Jeśli wolisz tworzyć własne funkcje kamery, zapoznaj się z artykułem Sterowanie kamerą).

Załóżmy, że wdrażasz usługę pogodową, która tworzy globalną mapę pogodową przez łączenie zdjęć nieba zrobionych przez urządzenia z Twoją aplikacją kliencką. Integracja zdjęć to tylko niewielka część aplikacji. Chcesz robić zdjęcia bez zbędnych komplikacji, a nie wymyślać nowego aparatu. Na szczęście na większości urządzeń z Androidem jest już zainstalowana co najmniej 1 aplikacja aparatu. Z tej lekcji dowiesz się, jak zrobić zdjęcie.

Prośba o funkcję aparatu

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

<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. W ten sposób Google Play zezwoli na pobieranie aplikacji z urządzeń bez aparatu. Następnie Twoim zadaniem jest sprawdzenie dostępności kamery w czasie wykonywania kodu przez wywołanie funkcji hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Jeśli kamera jest niedostępna, wyłącz funkcje kamery.

Pobieranie miniatury

Jeśli samo zrobienie zdjęcia nie jest szczytem ambicji Twojej aplikacji, prawdopodobnie chcesz odzyskać obraz z aplikacji aparatu i zrobić z nim coś użytecznego.

Aplikacja Aparat na Androida koduje zdjęcie w zwrotnym Intent dostarczonym do onActivityResult() jako mały Bitmap w dodatkach pod kluczem "data". Poniższy kod pobiera to zdjęcie i wyświetla je w elemencie 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 "data" może być odpowiednia na ikonę, ale nie na wiele więcej. Praca z pełnowymiarowymi obrazami wymaga nieco więcej wysiłku.

Zapisywanie zdjęcia w pełnym rozmiarze

Aplikacja Aparat na Androidzie zapisuje zdjęcie w pełnej wielkości, jeśli podasz jej plik do zapisu. Musisz podać pełną nazwę pliku, w którym aplikacja aparatu ma zapisać zdjęcie.

Ogólnie wszystkie zdjęcia zrobione przez użytkownika za pomocą aparatu urządzenia powinny być zapisywane na urządzeniu w publicznej pamięci zewnętrznej, aby były dostępne dla wszystkich aplikacji. Prawidłowy katalog dla udostępnionych zdjęć jest udostępniany przez getExternalStoragePublicDirectory() za pomocą argumentu DIRECTORY_PICTURES. Katalog udostępniony za pomocą tej metody jest wspólny dla wszystkich aplikacji. W przypadku Androida 9 (poziom API 28) i starszych odczytywanie i zapisywanie w tym katalogu wymaga odpowiednio uprawnień READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE:

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

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

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(). W Androidzie 4.3 lub starszym zapisywanie w tym katalogu wymaga też uprawnienia WRITE_EXTERNAL_STORAGE. Od Androida 4.4 to uprawnienie nie jest już wymagane, ponieważ inne aplikacje nie mają dostępu do tego katalogu. Możesz więc zadeklarować, że uprawnienie powinno być wymagane tylko w wersjach Androida niższych niż 4.4, dodając atrybut maxSdkVersion:

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

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

Gdy zdecydujesz się na katalog dla pliku, musisz utworzyć nazwę pliku odporną na kolizje. Możesz też zapisać ścieżkę w zmiennej członkowskiej, aby użyć jej później. Oto przykładowe rozwiązanie w metodie, która zwraca unikalną nazwę pliku dla nowego zdjęcia za pomocą sygnatury 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 możesz teraz tworzyć pliki zdjęć i wywoływać je w ten sposób:Intent

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 funkcji getUriForFile(Context, String, File), która zwraca identyfikator URI content://. W przypadku nowszych aplikacji kierowanych na Androida 7.0 (poziom interfejsu API 24) lub nowszego przekazywanie identyfikatora URI file:// przez granicę pakietu powoduje błądFileUriExposedException. Dlatego przedstawiamy teraz bardziej ogólny sposób przechowywania obrazów za pomocą FileProvider.

Teraz musisz skonfigurować FileProvider. W manifeście aplikacji dodaj dostawcę:

<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 funkcji getUriForFile(Context, String, File). W sekcji metadanych definicji dostawcy możesz zobaczyć, że dostawca oczekuje, że ścieżki kwalifikujące się do użycia zostaną skonfigurowane w dedykowanym pliku zasobów res/xml/file_paths.xml. Oto zawartość wymagana w tym 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 funkcję getExternalFilesDir(), gdy zostanie wywołana z parametrem Environment.DIRECTORY_PICTURES. Pamiętaj, aby zastąpić com.example.package.name rzeczywistą nazwą pakietu aplikacji. Zapoznaj się też z dokumentacją FileProvider, aby uzyskać obszerny opis wskaźników ścieżki, których możesz używać zamiast external-path.

Dodawanie zdjęcia do galerii

Gdy tworzysz zdjęcie za pomocą intencji, wiesz, gdzie się ono znajduje, ponieważ na początku określasz, gdzie je zapisać. W przypadku innych użytkowników najłatwiejszym sposobem udostępnienia zdjęcia jest udostępnienie go za pomocą dostawcy multimediów systemu.

Uwaga: jeśli zdjęcie zostało zapisane w katalogu udostępnionym przez getExternalFilesDir(), skaner multimediów nie ma do niego dostępu, ponieważ jest ono prywatne dla Twojej aplikacji.

Ten przykładowy sposób pokazuje, jak wywołać skaner multimediów systemu, aby dodać zdjęcie do bazy danych dostawcy multimediów, dzięki czemu będzie ono dostępne 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 powiększonego obrazu

Zarządzanie wieloma obrazami w pełnej wielkości może być trudne, gdy masz ograniczoną pamięć. Jeśli okaże się, że po wyświetleniu zaledwie kilku obrazów aplikacji brakuje pamięci, możesz znacznie zmniejszyć ilość używanej dynamicznej pamięci sterty, rozszerzając plik JPEG do tablicy pamięci, która jest już dostosowana do rozmiaru widoku docelowego. Poniżej przedstawiamy przykładową metodę, która pokazuje tę technikę.

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