Android 框架提供對設(shè)備上可用的相機(jī)和各種相機(jī)功能的支持,通過它我們可以在應(yīng)用程序中拍攝圖片和視頻。本文將介紹一種快速,簡單的拍攝圖像和視頻方法,并概述創(chuàng)建自定義相機(jī)的高級用法。
你還在為開發(fā)中頻繁切換環(huán)境打包而煩惱嗎?快來試試 Environment Switcher 吧!使用它可以在app運(yùn)行時(shí)一鍵切換環(huán)境,而且還支持其他貼心小功能,有了它媽媽再也不用擔(dān)心頻繁環(huán)境切換了。https://github.com/CodeXiaoMai/EnvironmentSwitcher
注意:Camera類已被棄用,官方建議使用更新的 android.hardware.camera2,但是它適用于Android 5.0(API級別21)或更高版本,所以...... 你懂的。
前言
在拍攝照片和視頻之前,首先我們應(yīng)該考慮一些關(guān)于我們的應(yīng)用程序應(yīng)該如何使用相機(jī)的問題。
相機(jī)的必要性 - 相機(jī)對我們的應(yīng)用程序是否非常重要?我們的應(yīng)用程序是否不允許安裝在沒有相機(jī)的設(shè)備上?如果是這樣,我們應(yīng)該在 manifest 中聲明對相機(jī)的要求。
快速拍照還是自定義相機(jī) - 我們的應(yīng)用程序?qū)⑷绾问褂孟鄼C(jī)?我們是只想快速的拍攝圖片或視頻剪輯,還是提供新的方式使用相機(jī)?如果是想快速的拍攝圖片或剪輯,我們應(yīng)該考慮使用現(xiàn)有的相機(jī)應(yīng)用程序(系統(tǒng)已經(jīng)提供默認(rèn)的相機(jī)應(yīng)用)。而要開發(fā)定制的相機(jī)功能,我們應(yīng)該創(chuàng)建自己的相機(jī)應(yīng)用程序部分。
存儲 - 我們的應(yīng)用程序生成的圖像或視頻是僅在我們的應(yīng)用程序中可見,還是為了方便其他應(yīng)用程序(如Gallery或其他媒體和社交應(yīng)用程序)使用它們?如果我們的應(yīng)用程序被卸載,是否希望圖片和視頻仍然可用?
相關(guān)基礎(chǔ)內(nèi)容
Android框架支持通過android.hardware.camera2 API或 Intent 啟動攝像機(jī) 兩種方式捕獲圖像和視頻。以下是我們需要用到的類:
android.hardware.camera2
這是用于控制設(shè)備攝像機(jī)的主要API。當(dāng)我們創(chuàng)建相機(jī)應(yīng)用程序時(shí),可用于拍攝照片或視頻。Camera
這個類是用于控制設(shè)備攝像頭的舊版API(已經(jīng)被廢棄)。SurfaceView
這個類用于呈現(xiàn)實(shí)時(shí)相機(jī)預(yù)覽。MediaRecorder
該類用于記錄相機(jī)錄制的視頻。Intent
使用 MediaStore.ACTION_IMAGE_CAPTURE 可用于捕獲圖像,而使用 MediaStore.ACTION_VIDEO_CAPTURE 可用于拍攝視頻。這兩個 Intent 是為了方便我們直接調(diào)用其他相機(jī)應(yīng)用進(jìn)行拍照或錄制視頻,而不用直接使用Camera對象。
清單聲明
在使用Camera API開始開發(fā)應(yīng)用程序之前,我們首先應(yīng)該正確的配置 manifest,以允許使用相機(jī)硬件和其他相關(guān)功能。
-
相機(jī)權(quán)限 - 如果我們通過調(diào)用現(xiàn)有的相機(jī)應(yīng)用程序來使用相機(jī),則應(yīng)用程序不需要請求此權(quán)限,反之如果是自定義相機(jī)則我們的應(yīng)用程序必須要求使用設(shè)備攝像頭的權(quán)限。
<uses-permission android:name="android.permission.CAMERA" /> -
相機(jī)功能 - 我們的應(yīng)用程序還必須聲明使用相機(jī)功能
<uses-feature android:name="android.hardware.camera" />
將相機(jī)功能添加到 manifest 中會導(dǎo)致Google Play阻止我們的應(yīng)用程序安裝到?jīng)]有相機(jī)或不支持我們所指定的相機(jī)功能的設(shè)備上。
如果我們的應(yīng)用程序的核心功能為拍攝照片,或者如果設(shè)備沒有相機(jī)則應(yīng)用無法運(yùn)行,我們可以通過上面的代碼將其在應(yīng)用商店(Google Play)上的可見性限制為具有相機(jī)的設(shè)備。而如果我們的應(yīng)用程序有一部分功能需要相機(jī),但并非必須需要相機(jī)才能運(yùn)行(其他功能還是可以用的嘛),則應(yīng)將 manifest 中的 android:required 設(shè)置為 false,否則會導(dǎo)致設(shè)備沒有相機(jī)的用戶無法安裝此應(yīng)用。
<manifest ... >
<uses-feature android:name="android.hardware.camera" android:required="false" />
...
</manifest>
-
存儲權(quán)限 - 如果我們的應(yīng)用程序需要將圖像或視頻保存到設(shè)備的外部存儲(SD卡),則還必須在 manifest 中添加:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
錄音權(quán)限** - 對于使用視頻捕獲錄制音頻,我們的應(yīng)用程序必須請求音頻捕獲權(quán)限。
<uses-permission android:name="android.permission.RECORD_AUDIO" /> -
定位權(quán)限 - 如果我們的應(yīng)用程序使用GPS位置信息標(biāo)記圖像,則必須請求ACCESS_FINE_LOCATION權(quán)限。需要注意的是,如果我們的應(yīng)用運(yùn)行在Android 5.0(API級別21)或更高版本,則還需要聲明使用設(shè)備的GPS:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> ... <!-- 如果你需要保存照片的位置信息就乖乖的添加吧,除非你不支持 5.0 以上的設(shè)備 --> <uses-feature android:name="android.hardware.location.gps" />
使用現(xiàn)有相機(jī)應(yīng)用拍攝照片
通過Intent來調(diào)用現(xiàn)有的相機(jī)應(yīng)用程序,可以快速地實(shí)現(xiàn)在應(yīng)用程序中拍攝照片或視頻,這是最簡單快速的方法。
通過現(xiàn)有的相機(jī)應(yīng)用程序來實(shí)現(xiàn)拍照這個過程涉及 3 個步驟:創(chuàng)建 Intent、啟動已有相機(jī)應(yīng)用 以及返回到我們的 Activity 時(shí)處理圖像數(shù)據(jù)。
1. 創(chuàng)建 Intent 并啟動相機(jī)應(yīng)用
這是一個通過 Intent 啟動系統(tǒng)相機(jī)的例子:
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()方法受到調(diào)用resolveActivity()的條件的保護(hù),resolveActivity()方法返回可處理該intent的第一個活動組件。執(zhí)行此檢查很重要,因?yàn)槿绻覀兪褂靡粋€沒有應(yīng)用程序可以響應(yīng)的 Intent 調(diào)用startActivityForResult(),程序?qū)罎?。所以只要結(jié)果不為空,就可以安全的使用它。
2. 處理圖像數(shù)據(jù)
當(dāng)使用相機(jī)應(yīng)用程序拍照成功后,系統(tǒng)會將照片進(jìn)行編碼放入返回的 Intent 中并傳遞給onActivityResult()。
2.1 獲取縮略圖
Intent 的 extras 中的 “data” 關(guān)鍵字 存儲的就是縮略圖。以下代碼是從返回結(jié)果中取出此圖像并將其顯示在ImageView中。
@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");
mImageView.setImageBitmap(imgetBitmap);
}
}
注意:來自“data”的縮略圖可能對像素要求不高的圖片很適合。處理全尺寸圖像需要更多的工作。
2.2 保存全尺寸照片
顯然僅僅用縮略圖是不能滿足我們的要求的,我們可以用一個文件來保存一張全尺寸照片。
通常,用戶使用攝像頭拍攝的照片應(yīng)保存在公共外部存儲設(shè)備上,以便所有應(yīng)用都可以訪問。調(diào)用 Environment.getExternalStoragePublicDirectory()并傳遞參數(shù)DIRECTORY_PICTURES 將返回共享照片的正確目錄。由于此方法提供的目錄在所有應(yīng)用程序之間共享,所以讀取和寫入需要分別具有READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE權(quán)限。寫入權(quán)限隱含地允許讀取,所以如果你需要寫入外部存儲,那么你只需要請求一個權(quán)限:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
但是,如果我們希望照片僅對我們自己的應(yīng)用程序保持私有狀態(tài),則可以使用getExternalFilesDir()提供的目錄。在Android 4.3及更低版本上,寫入此目錄也需要 WRITE_EXTERNAL_STORAGE 權(quán)限。從Android 4.4開始,不再需要權(quán)限,因?yàn)槟夸洸荒鼙黄渌麘?yīng)用程序訪問,因此我們可以通過添加maxSdkVersion屬性來聲明僅在較低版本的Android上的權(quán)限:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest>
注意:當(dāng)用戶卸載應(yīng)用程序時(shí),系統(tǒng)將刪除由getExternalFilesDir()或 getFilesDir()提供的目錄中保存的文件。
確定文件的目錄后,我們需要創(chuàng)建一個防止沖突的文件名。我們可能還希望將路徑保存在成員變量中以備以后使用。以下是使用日期時(shí)間戳作為新照片的唯一文件名的方法的示例解決方案:
String mCurrentPhotoPath;
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: 用于ACTION_VIEW意圖的路徑
mCurrentPhotoPath = image.getAbsolutePath();
return image;
}
使用這種方法可以為照片保存為一個文件,現(xiàn)在可以像這樣創(chuàng)建和調(diào)用Intent:
public 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) {
File photoFile = null;
try {
// Create the File where the photo should go
photoFile = createImageFile();
} catch (IOException e) {
e.printStackTrace();
}
// Continue only if the File was successfully created
if (photoFile != null) {
Uri photoURI = FileProvider.getUriForFile(this,
"com.xiaomai.myproject.fileprovider",
photoFile
);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
}
}
}
注意:我們使用getUriForFile(Context,String,F(xiàn)ile)返回一個
content://URI。對于Android 7.0和更高(API等級大于等于24)的應(yīng)用,通過一個包邊界傳遞一個file://URI會導(dǎo)致FileUriExposedException。因此,我們現(xiàn)在介紹一種使用FileProvider存儲圖像的更通用的方法。
現(xiàn)在,我們需要配置FileProvider。在應(yīng)用的清單中,為我們的應(yīng)用添加一個提供者:
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.xiaomai.myproject.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>
確保android:authorities的字符串內(nèi)容匹配 getUriForFile(Context,String,F(xiàn)ile)的第二個參數(shù)。在提供程序定義的元數(shù)據(jù)部分,我們可以看到提供程序期望在專用資源文件res/xml/file_paths.xml中配置符合條件的路徑。以下是此特定示例所需的內(nèi)容:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" />
</paths>
路徑組件對應(yīng)于調(diào)用getExternalFilesDir() 并使用Environment.DIRECTORY_PICTURES時(shí)返回的路徑。確保你已經(jīng)將com.example.package.name替換為應(yīng)用程序的實(shí)際包名稱。另外,可以查看FileProvider的文檔,以便使用除了外部路徑之外的路徑說明符。
將照片添加到圖庫
當(dāng)通過 Intent 啟動拍攝照片時(shí),我們知道圖像所在的位置,因?yàn)槲覀冎付藢⑵浔4嬖谀睦?。對于其他人來說,也許最簡單的方法是使我們的照片可以訪問,使其可以從系統(tǒng)的媒體訪問。
注意:如果我們將照片保存到由getExternalFilesDir()提供的目錄中,則媒體掃描程序無法訪問這些文件,因?yàn)樗鼈儗ξ覀兊膽?yīng)用程序是私有的。
以下示例方法演示了如何調(diào)用系統(tǒng)的媒體掃描程序?qū)⑽覀兊恼掌砑拥矫襟w提供商的數(shù)據(jù)庫,使其可以在Android圖庫應(yīng)用程序和其他應(yīng)用程序中使用。
private void galleryAddPic() {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
File file = new File(mCurrentPhotoPath);
Uri contentUri = Uri.fromFile(file);
mediaScanIntent.setData(contentUri);
sendBroadcast(mediaScanIntent);
}
解碼縮放圖片
使用有限的內(nèi)存來管理多個全尺寸圖像可能很棘手。如果在僅僅顯示幾個圖像后就發(fā)現(xiàn)應(yīng)用程序內(nèi)存不足,則可以通過將JPEG縮放到匹配目標(biāo)視圖大小并存入內(nèi)存中,來大大減少動態(tài)堆的使用數(shù)量。以下示例方法演示了此技術(shù)。
private void setPic() {
// 獲取ImageView的尺寸
int targetWidth = mImageView.getWidth();
int targetHeight = mImageView.getHeight();
// 獲取圖片的真實(shí)尺寸
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
int photoWidth = bmOptions.outWidth;
int photoHeight = bmOptions.outHeight;
// 確定圖像的縮放比例
int scaleFactor = Math.min(photoWidth / targetWidth, photoHeight / targetHeight);
// 將圖像文件解碼為位圖大小以填充視圖
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
mImageView.setImageBitmap(bitmap);
}