Android 框架提供對設(shè)備上可用的相機和各種相機功能的支持挤安,通過它我們可以在應(yīng)用程序中拍攝圖片和視頻搜吧。本文將介紹一種快速娩梨,簡單的拍攝圖像和視頻方法碟渺,并概述創(chuàng)建自定義相機的高級用法。
你還在為開發(fā)中頻繁切換環(huán)境打包而煩惱嗎街氢?快來試試 Environment Switcher 吧扯键!使用它可以在app運行時一鍵切換環(huá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)該如何使用相機的問題叶堆。
相機的必要性 - 相機對我們的應(yīng)用程序是否非常重要?我們的應(yīng)用程序是否不允許安裝在沒有相機的設(shè)備上斥杜?如果是這樣虱颗,我們應(yīng)該在 manifest 中聲明對相機的要求。
快速拍照還是自定義相機 - 我們的應(yīng)用程序?qū)⑷绾问褂孟鄼C蔗喂?我們是只想快速的拍攝圖片或視頻剪輯忘渔,還是提供新的方式使用相機?如果是想快速的拍攝圖片或剪輯缰儿,我們應(yīng)該考慮使用現(xiàn)有的相機應(yīng)用程序(系統(tǒng)已經(jīng)提供默認的相機應(yīng)用)畦粮。而要開發(fā)定制的相機功能,我們應(yīng)該創(chuàng)建自己的相機應(yīng)用程序部分。
存儲 - 我們的應(yīng)用程序生成的圖像或視頻是僅在我們的應(yīng)用程序中可見宣赔,還是為了方便其他應(yīng)用程序(如Gallery或其他媒體和社交應(yīng)用程序)使用它們预麸?如果我們的應(yīng)用程序被卸載,是否希望圖片和視頻仍然可用儒将?
相關(guān)基礎(chǔ)內(nèi)容
Android框架支持通過android.hardware.camera2 API或 Intent 啟動攝像機 兩種方式捕獲圖像和視頻吏祸。以下是我們需要用到的類:
android.hardware.camera2
這是用于控制設(shè)備攝像機的主要API。當我們創(chuàng)建相機應(yīng)用程序時钩蚊,可用于拍攝照片或視頻贡翘。Camera
這個類是用于控制設(shè)備攝像頭的舊版API(已經(jīng)被廢棄)。SurfaceView
這個類用于呈現(xiàn)實時相機預(yù)覽砰逻。MediaRecorder
該類用于記錄相機錄制的視頻鸣驱。Intent
使用 MediaStore.ACTION_IMAGE_CAPTURE 可用于捕獲圖像,而使用 MediaStore.ACTION_VIDEO_CAPTURE 可用于拍攝視頻蝠咆。這兩個 Intent 是為了方便我們直接調(diào)用其他相機應(yīng)用進行拍照或錄制視頻踊东,而不用直接使用Camera對象。
清單聲明
在使用Camera API開始開發(fā)應(yīng)用程序之前勺美,我們首先應(yīng)該正確的配置 manifest递胧,以允許使用相機硬件和其他相關(guān)功能碑韵。
-
相機權(quán)限 - 如果我們通過調(diào)用現(xiàn)有的相機應(yīng)用程序來使用相機赡茸,則應(yīng)用程序不需要請求此權(quán)限,反之如果是自定義相機則我們的應(yīng)用程序必須要求使用設(shè)備攝像頭的權(quán)限祝闻。
<uses-permission android:name="android.permission.CAMERA" />
-
相機功能 - 我們的應(yīng)用程序還必須聲明使用相機功能
<uses-feature android:name="android.hardware.camera" />
將相機功能添加到 manifest 中會導(dǎo)致Google Play阻止我們的應(yīng)用程序安裝到?jīng)]有相機或不支持我們所指定的相機功能的設(shè)備上占卧。
如果我們的應(yīng)用程序的核心功能為拍攝照片,或者如果設(shè)備沒有相機則應(yīng)用無法運行联喘,我們可以通過上面的代碼將其在應(yīng)用商店(Google Play)上的可見性限制為具有相機的設(shè)備华蜒。而如果我們的應(yīng)用程序有一部分功能需要相機,但并非必須需要相機才能運行(其他功能還是可以用的嘛)豁遭,則應(yīng)將 manifest 中的 android:required
設(shè)置為 false
叭喜,否則會導(dǎo)致設(shè)備沒有相機的用戶無法安裝此應(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位置信息標記圖像闪幽,則必須請求ACCESS_FINE_LOCATION權(quán)限啥辨。需要注意的是,如果我們的應(yīng)用運行在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)有相機應(yīng)用拍攝照片
通過Intent來調(diào)用現(xiàn)有的相機應(yīng)用程序,可以快速地實現(xiàn)在應(yīng)用程序中拍攝照片或視頻,這是最簡單快速的方法级乍。
通過現(xiàn)有的相機應(yīng)用程序來實現(xiàn)拍照這個過程涉及 3 個步驟:創(chuàng)建 Intent舌劳、啟動已有相機應(yīng)用 以及返回到我們的 Activity 時處理圖像數(shù)據(jù)。
1. 創(chuàng)建 Intent 并啟動相機應(yīng)用
這是一個通過 Intent 啟動系統(tǒng)相機的例子:
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()的條件的保護蒿囤,resolveActivity()方法返回可處理該intent的第一個活動組件。執(zhí)行此檢查很重要崇决,因為如果我們使用一個沒有應(yīng)用程序可以響應(yīng)的 Intent 調(diào)用startActivityForResult()材诽,程序?qū)罎ⅰK灾灰Y(jié)果不為空恒傻,就可以安全的使用它脸侥。
2. 處理圖像數(shù)據(jù)
當使用相機應(yīng)用程序拍照成功后,系統(tǒng)會將照片進行編碼放入返回的 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īng)用程序訪問寄摆,因此我們可以通過添加maxSdkVersion屬性來聲明僅在較低版本的Android上的權(quán)限:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
</manifest>
注意:當用戶卸載應(yīng)用程序時,系統(tǒng)將刪除由getExternalFilesDir()或 getFilesDir()提供的目錄中保存的文件修赞。
確定文件的目錄后婶恼,我們需要創(chuàng)建一個防止沖突的文件名桑阶。我們可能還希望將路徑保存在成員變量中以備以后使用。以下是使用日期時間戳作為新照片的唯一文件名的方法的示例解決方案:
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時返回的路徑巩搏。確保你已經(jīng)將com.example.package.name
替換為應(yīng)用程序的實際包名稱。另外趾代,可以查看FileProvider的文檔贯底,以便使用除了外部路徑之外的路徑說明符。
將照片添加到圖庫
當通過 Intent 啟動拍攝照片時稽坤,我們知道圖像所在的位置丈甸,因為我們指定了將其保存在哪里糯俗。對于其他人來說尿褪,也許最簡單的方法是使我們的照片可以訪問,使其可以從系統(tǒng)的媒體訪問得湘。
注意:如果我們將照片保存到由getExternalFilesDir()提供的目錄中杖玲,則媒體掃描程序無法訪問這些文件,因為它們對我們的應(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縮放到匹配目標視圖大小并存入內(nèi)存中,來大大減少動態(tài)堆的使用數(shù)量惩淳。以下示例方法演示了此技術(shù)蕉毯。
private void setPic() {
// 獲取ImageView的尺寸
int targetWidth = mImageView.getWidth();
int targetHeight = mImageView.getHeight();
// 獲取圖片的真實尺寸
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);
}