注意:本頁面所述是指已淘汰的 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: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_STORAGE
和 WRITE_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)
的第二個引數相符。在供應商定義的中繼資料區段中,可以看到供應商預期在專屬的資源檔案 (
<?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); }