拍攝相片

注意:本頁面所述是指已淘汰的相機類別。建議使用 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:required 設定為 false。如此一來,Google Play 會允許沒有相機的裝置下載應用程式。接著透過呼叫 hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY),由您在執行階段檢查是否有相機可以使用。如果沒有相機可以使用,您應該停用相機功能。

取得縮圖

如果輕鬆拍照並非應用程式的目標,那麼可以從相機應用程式將相片取回,並進行其他操作。

Android 相機應用程式會將回傳到onActivityResult()時的Intent中的相片,以其他資料夾中、索引鍵 "data" 下的小 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 相機應用程式會儲存原尺寸相片。必須提供完整的檔案名稱,讓相機應用程式儲存相片。

一般來說,使用者使用裝置相機拍攝的所有相片都應儲存在裝置的公開外部儲存空間,讓所有應用程式可以存取。共用相片的正確目錄由 getExternalStoragePublicDirectory() 透過 DIRECTORY_PICTURES 引數提供。此方法提供的目錄會由所有應用程式共用。在 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 版開始,由於其他應用程式無法存取該目錄,因此不再需要這項權限。因此只有在較低版本的 Android 上才需要新增 maxSdkVersion 屬性來宣告請求權限:

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

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

使用提供的這個方法為相片建立檔案時,即可如下所示建立並叫用 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);
        }
    }
}

注意:我們使用 getUriForFile(Context, String, File) 來回傳 content:// URI。如果是最近指定 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) 的第二個引數相符。在供應商定義的中繼資料區段中,可以看到供應商預期在專屬的資源檔案 (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>

路徑元件會對應至使用 Environment.DIRECTORY_PICTURES 呼叫時 getExternalFilesDir() 回傳的路徑。請確認用應用程式的實際套件名稱取代 com.example.package.name。此外,請參閱 FileProvider 的說明文件,深入瞭解除 external-path 以外,還有哪些路徑指定碼的詳細說明可供參考。

將相片新增至圖片庫

透過意圖建立相片時,您應該知道圖片的位置,因為是由您在一開始指定相片儲存的位置。若是其他使用者,最簡單的方法是透過系統的媒體供應商讓相片可以被存取。

注意:如果將相片儲存至 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);
}