사진 촬영

참고: 이 페이지에서는 지원 중단된 Camera 클래스를 다룹니다. CameraX, 또는 특정 사용 사례의 경우 Camera2를 사용하는 것이 좋습니다. CameraX와 Camera2는 모두 Android 5.0(API 수준 21) 이상을 지원합니다.

이 과정에서는 기기의 다른 카메라 앱에 사진 촬영 작업을 위임하여 사진을 캡처하는 방법을 알려줍니다. 자체 카메라 기능을 빌드하려면 카메라 제어를 참고하세요.

클라이언트 앱을 실행하는 기기에서 촬영한 하늘 사진을 조합하여 세계 날씨 지도를 만드는 크라우드 소싱 날씨 서비스를 구현한다고 가정해 보세요. 사진 통합은 애플리케이션에서 실행되는 다양한 작업 중 하나에 불과합니다. 카메라를 새로 만들지 않고 간단하게 사진을 촬영하고자 합니다. 다행히 대부분의 Android 지원 기기에는 이미 카메라 애플리케이션이 한 개 이상 설치되어 있습니다. 이 과정에서는 카메라 애플리케이션을 사용하여 사진을 촬영하는 방법을 배웁니다.

카메라 기능 요청

애플리케이션의 필수 기능이 사진 촬영이라면 Google Play에서 공개 상태를 카메라가 있는 기기로 제한하세요. 애플리케이션이 카메라를 필요로 한다고 알리려면 다음과 같이 매니페스트 파일에 <uses-feature> 태그를 삽입합니다.

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

애플리케이션이 카메라를 사용하기는 하지만 작동하는 데 꼭 필요한 것이 아니라면 android:requiredfalse로 설정합니다. 그렇게 하면 Google Play에서 카메라가 없는 기기도 애플리케이션을 다운로드할 수 있습니다. 그런 다음 런타임 시 hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)를 호출하여 카메라의 사용 가능 여부를 확인해야 합니다. 카메라를 사용할 수 없는 경우 카메라 기능을 중지합니다.

썸네일 가져오기

단순히 사진을 촬영하는 기능이 앱의 주된 목적이 아니라면 카메라 애플리케이션에서 이미지를 다시 가져와서 어떤 작업을 하려고 할 것입니다.

Android 카메라 애플리케이션은 onActivityResult()에 전달된 반환 Intent"data" 키 아래 extras에 작은 Bitmap으로 사진을 인코딩합니다. 다음 코드는 이미지를 가져와서 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);
    }
}

참고: "data"에서 가져온 썸네일 이미지는 아이콘으로 사용하기에는 좋지만, 그 이상은 아닙니다. 원본 크기의 이미지를 처리하려면 추가 작업이 필요합니다.

원본 크기의 사진 저장

Android 카메라 애플리케이션은 저장할 파일을 받으면 원본 크기의 사진을 저장합니다. 카메라 앱이 사진을 저장할 정규화된 파일 이름을 제공해야 합니다.

일반적으로 사용자가 기기 카메라로 캡처한 사진은 기기의 공용 외부 저장소에 저장되므로 모든 앱에서 액세스할 수 있습니다. 사진을 공유하기 위한 적절한 디렉터리는 DIRECTORY_PICTURES를 인수로 사용하여 getExternalStoragePublicDirectory()에서 제공합니다. 이 메서드에서 제공하는 디렉터리는 모든 앱에서 공유됩니다. Android 9(API 수준 28) 이하에서는 이 디렉터리를 읽고 이 디렉터리에 쓰려면 각각 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>

Android 10(API 수준 29) 이상에서 사진 공유를 위한 적절한 디렉터리는 MediaStore.Images 테이블입니다. 사용자가 앱을 사용하여 촬영한 사진에만 앱이 액세스하면 되는 경우에는 저장소 권한을 선언할 필요가 없습니다.

그러나 사진을 앱 이외에는 비공개로 두려면 대신 Context.getExternalFilesDir()에서 제공하는 디렉터리를 사용하면 됩니다. Android 4.3 이하 버전에서는 이 디렉터리에 쓸 때도 WRITE_EXTERNAL_STORAGE 권한이 필요합니다. Android 4.4부터는 이 디렉터리를 다른 앱에서 액세스할 수 없으므로 더 이상 권한이 필요 없으며 다음과 같이 maxSdkVersion 속성을 추가하여 Android 이전 버전에서만 권한이 요청되도록 선언할 수 있습니다.

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

참고: getExternalFilesDir() 또는 getFilesDir()에서 제공한 디렉터리에 저장한 파일은 사용자가 앱을 제거할 때 삭제됩니다.

파일의 디렉터리를 결정한 후에는 서로 충돌하지 않는 파일 이름을 만들어야 합니다. 또한 나중에 사용할 수 있도록 멤버 변수에 경로를 저장하는 것이 좋습니다. 다음은 날짜-시간 스탬프를 사용하여 새 사진의 고유한 파일 이름을 반환하는 메서드로 이 문제를 해결하는 한 예입니다. 이 예에서는 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
    }
}

자바

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

사진을 파일로 만드는 이 메서드를 사용하여 이제 다음과 같이 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);
        }
    }
}

참고: 위의 예에서는 content:// URI를 반환하는 getUriForFile(Context, String, File)을 사용하고 있습니다. Android 7.0(API 수준 24) 이상을 타겟팅하는 더 최신의 앱이 패키지 경계를 넘어 file:// URI를 전달하면 FileUriExposedException이 발생합니다. 따라서 FileProvider를 사용하여 이미지를 저장하는 더 일반적인 방법을 제시하려고 합니다.

이제 FileProvider를 구성해야 합니다. 앱의 매니페스트에서 제공자를 애플리케이션에 추가합니다.

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

권한 문자열에서 두 번째 인수는 getUriForFile(Context, String, File)에 일치시켜야 합니다. 제공자 정의의 meta-data 섹션에서 제공자가 전용 리소스 파일(res/xml/file_paths.xml)에 적합한 경로가 구성된다고 예상하는 것을 확인할 수 있습니다. 다음은 이 특정 예에서 필요한 콘텐츠입니다.

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

경로 구성요소는 getExternalFilesDir()Environment.DIRECTORY_PICTURES를 인수로 하여 호출될 때 반환되는 경로와 일치합니다. com.example.package.name은 앱의 실제 패키지 이름으로 대체해야 합니다. 또한 external-path 외에 사용할 수 있는 경로 지정자의 광범위한 설명을 보려면 FileProvider의 문서를 확인하세요.

갤러리에 사진 추가

먼저 이미지를 저장할 위치를 지정했기 때문에 인텐트를 통해 사진을 만들 때 이미지의 위치를 알아야 합니다. 사람들이 사진에 액세스하도록 하는 가장 쉬운 방법은 시스템의 미디어 제공자에서 사진에 액세스하도록 하는 것입니다.

참고: 사진을 getExternalFilesDir()에서 제공한 디렉터리에 저장했다면 미디어 스캐너는 파일이 앱 이외에는 비공개이기 때문에 파일에 액세스할 수 없습니다.

아래의 메서드 예는 시스템의 미디어 스캐너를 호출하여 사진을 미디어 제공자의 데이터베이스에 추가한 후 Android 갤러리 애플리케이션 및 다른 앱에서 사용할 수 있도록 하는 방법을 보여줍니다.

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

크기가 조정된 이미지 디코딩

여러 원본 크기의 이미지를 관리하는 것은 메모리가 제한적인 상황에서 어려운 일일 수 있습니다. 단지 몇 장의 이미지만 표시한 후 애플리케이션이 메모리가 부족한 상태로 실행되고 있다면 이미 대상 뷰의 크기에 맞게 조정된 JPEG을 메모리 배열로 확장하여 사용된 동적 힙을 크게 줄일 수 있습니다. 아래 메서드 예는 이 기법을 보여줍니다.

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