Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

사진 촬영

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

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

카메라 기능 요청

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

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

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

카메라 앱으로 사진 촬영

Android에서 다른 애플리케이션에 작업을 위임하는 방법은 해야 할 일을 설명한 Intent를 호출하는 것입니다. 이 절차는 세 부분을 포함하는데 이는 Intent 자체, 외부 Activity를 시작하는 호출, 포커스가 활동에 반환될 때 이미지 데이터를 처리하는 코드 일부입니다.

다음은 사진을 캡처하는 인텐트를 호출하는 함수입니다.

Kotlin

    val REQUEST_IMAGE_CAPTURE = 1

    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            takePictureIntent.resolveActivity(packageManager)?.also {
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
            }
        }
    }
    

자바

    static final int REQUEST_IMAGE_CAPTURE = 1;

    private void dispatchTakePictureIntent() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
    }
    

startActivityForResult() 메서드는 resolveActivity()를 호출하는 조건에 의해 보호되며 이 함수는 인텐트를 처리할 수 있는 첫 번째 활동 구성요소를 반환합니다. 이 확인 절차가 중요한 이유는 앱이 처리할 수 없는 인텐트를 사용하여 startActivityForResult()를 호출하면 앱이 비정상 종료되기 때문입니다. 따라서 결과가 null이 아닌 한 안심하고 인텐트를 사용할 수 있습니다.

미리보기 이미지 가져오기

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

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

자바

    @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()에서 제공합니다. 이 메서드에서 제공하는 디렉터리는 모든 앱에서 공유하기 때문에 이 디렉터리를 읽고 쓰려면 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 권한이 각각 필요합니다. 쓰기 권한은 암시적으로 읽기를 허용하므로 외부 저장소에 쓰려면 다음과 같이 하나의 권한만 요청하면 됩니다.

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

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

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

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

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

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

    val REQUEST_TAKE_PHOTO = 1

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

자바

    static final int REQUEST_TAKE_PHOTO = 1;

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

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

이제 FileProvider를 구성해야 합니다. 앱의 manifest에서 제공자를 애플리케이션에 추가합니다.
    <application>
       ...
       <provider
            android:name="android.support.v4.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="Android/data/com.example.package.name/files/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)
        }
    }
    

자바

    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

            val photoW: Int = outWidth
            val photoH: Int = outHeight

            // Determine how much to scale down the image
            val scaleFactor: Int = 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)
        }
    }
    

자바

    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;

        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        // Determine how much to scale down the image
        int scaleFactor = 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);
    }