Robienie zdjęć

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

Z tej lekcji dowiesz się, jak zrobić zdjęcie, przekazując tę czynność do innej aplikacji aparatu na urządzeniu. (Jeśli wolisz stworzyć własną funkcję kamery, przeczytaj artykuł Sterowanie kamerą).

Załóżmy, że wdrażasz dostarczaną przez społeczność usługę pogodową, która tworzy globalną mapę pogody, łącząc ze sobą zdjęcia nieba wykonane przez urządzenia z aplikacją kliencką. Integracja zdjęć to tylko niewielka część aplikacji. Chcesz robić zdjęcia przy minimalnym nakładzie pracy, a nie wymyślać nowy aparat. Na szczęście na większości urządzeń z Androidem jest już zainstalowana co najmniej 1 aplikacja kamery. Z tej lekcji dowiesz się, jak zrobić zdjęcie.

Poproś o funkcję aparatu

Jeśli ważną funkcją aplikacji jest robienie zdjęć, ogranicz jej widoczność w Google Play tylko do urządzeń z aparatem. Aby reklamować, ż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 Twoja aplikacja używa aparatu do działania, ale nie potrzebuje do działania kamery, zamiast tego ustaw android:required na false. Dzięki temu Google Play zezwoli na pobranie Twojej aplikacji urządzeniom bez kamery. Twoim obowiązkiem jest wtedy sprawdzenie dostępności kamery w czasie działania przez wywołanie hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). Jeśli aparat nie jest dostępny, wyłącz jego funkcje.

Pobierz miniaturę

Jeśli prosty właściwy sposób robienia zdjęcia nie jest sednem ambicji aplikacji, najlepiej będzie przywrócić obraz z aparatu i coś z nim zrobić.

Aplikacja Aparat na Androida koduje zdjęcie w zwracanym wyniku Intent dostarczanym do onActivityResult() jako mały obiekt Bitmap w dodatku, pod kluczem "data". Podany niżej kod pobiera obraz i wyświetla go 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: ten obraz miniatury ze źródła "data" może pasować do ikony, ale niekoniecznie jako inny. Praca z obrazem w pełnym rozmiarze wymaga trochę więcej pracy.

Zapisywanie zdjęcia w pełnym rozmiarze

Aplikacja Aparat w Androidzie zapisuje zdjęcie w pełnym rozmiarze, jeśli prześlesz do niego plik do zapisania. Musisz podać pełną nazwę pliku, w którym aplikacja aparatu ma zapisać zdjęcie.

Wszelkie 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 do udostępniania zdjęć jest udostępniany przez atrybut getExternalStoragePublicDirectory() z argumentem DIRECTORY_PICTURES. Katalog podany przy użyciu tej metody jest wspólny dla wszystkich aplikacji. Na Androidzie 9 (poziom interfejsu API 28) i starszych odczyt i zapis w tym katalogu wymaga 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 Androidzie 10 (poziom interfejsu API 29) i nowszych właściwym katalogiem do udostępniania zdjęć jest tabela MediaStore.Images. Nie musisz deklarować żadnych uprawnień do przechowywania danych, o ile aplikacja potrzebuje tylko dostępu do zdjęć, które użytkownik zrobił w niej.

Jeśli jednak chcesz, aby zdjęcia pozostały prywatne tylko w Twojej aplikacji, możesz użyć katalogu udostępnionego przez Context.getExternalFilesDir(). W Androidzie 4.3 i starszych zapisy w tym katalogu wymagają też uprawnienia WRITE_EXTERNAL_STORAGE. Począwszy od Androida 4.4 uprawnienia te nie są już wymagane, ponieważ katalog nie jest dostępny dla innych aplikacji. Możesz więc zadeklarować, że żądanie tych uprawnień powinno być wysyłane tylko w starszych wersjach Androida. Aby to zrobić, dodaj atrybut maxSdkVersion:

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

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

Po wybraniu katalogu, w którym zostanie zapisany plik, należy utworzyć nazwę odporną na kolizję. Możesz też zapisać ścieżkę w zmiennej członkowskiej do użytku w przyszłości. Oto przykładowe rozwiązanie metody, która zwraca unikalną nazwę pliku dla nowego zdjęcia za pomocą znacznika daty i godziny. (W tym przykładzie zakładamy, że wywołujesz metodę z elementu 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 utworzenia pliku ze zdjęciem możesz utworzyć i wywołać 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 nazwy getUriForFile(Context, String, File), która zwraca identyfikator URI content://. W nowszych aplikacjach kierowanych na Androida 7.0 (poziom interfejsu API 24) lub nowszego przekazywanie identyfikatora URI file:// przez granicę pakietu powoduje wywołanie FileUriExposedException. Dlatego przedstawiamy bardziej ogólny sposób przechowywania obrazów za pomocą właściwości FileProvider.

Teraz musisz skonfigurować FileProvider. W pliku manifestu aplikacji dodaj do niej 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>

Sprawdź, czy ciąg tekstowy dotyczący urzędów pasuje do drugiego argumentu funkcji getUriForFile(Context, String, File). W sekcji metadanych dostawcy widać, że dostawca oczekuje, że odpowiednie ścieżki zostaną skonfigurowane w dedykowanym pliku zasobów res/xml/file_paths.xml. Oto treści wymagane 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() po wywołaniu za pomocą metody Environment.DIRECTORY_PICTURES. Zastąp com.example.package.name rzeczywistą nazwą pakietu swojej aplikacji. Sprawdź też dokumentację FileProvider, w której znajdziesz obszerny opis specyfikatorów ścieżki, których możesz używać poza external-path.

Dodaj zdjęcie do galerii

Kiedy tworzysz zdjęcie za pomocą intencji, musisz wiedzieć, gdzie się on znajduje, ponieważ od razu wiesz, gdzie je zapisać. Dla pozostałych osób chyba najprostszym sposobem na udostępnienie zdjęcia jest udostępnienie go u dostawcy multimediów systemu.

Uwaga: jeśli zdjęcie zostało zapisane w katalogu udostępnionym przez usługę getExternalFilesDir(), skaner multimediów nie ma dostępu do plików, ponieważ są one prywatne w Twojej aplikacji.

Poniższa przykładowa metoda pokazuje, jak wywołać systemowy skaner multimediów w celu dodania zdjęcia do bazy danych dostawcy multimediów i udostępnienia go w aplikacji Galeria na Androidzie i 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 pełnowymiarowymi zdjęciami może być trudne, gdy dysponuje się ograniczoną pamięcią. Jeśli po wyświetleniu kilku obrazów zauważysz, że w Twojej aplikacji kończy się pamięć, możesz znacznie zmniejszyć ilość wykorzystywanej dynamicznej sterty, rozwijając plik JPEG do tablicy pamięci, która została już przeskalowana tak, aby pasowała do rozmiaru widoku docelowego. Poniższa przykładowa metoda obrazuje tę metodę.

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