本章主要講了如何使用 intent 拍照,存儲照片和展示照片
GitHub 地址:
完成16章,未完成挑戰(zhàn)
完成16章挑戰(zhàn)1
完成16章挑戰(zhàn)2
1. 外部存儲
相機(jī)照片動輒幾 MB 大小,直接保存在數(shù)據(jù)庫中肯定不現(xiàn)實(shí)。很自然,大家會想到直接使用設(shè)備的文件系統(tǒng)。
一般來講,應(yīng)用都應(yīng)該使用私有存儲空間保存各類文件。還記得嗎?在前面章節(jié)中,我們在私有存儲空間保存過 SQLite 數(shù)據(jù)文件。使用類似 Context.getFileStreamPath(String)和 Context.getFilesDir()這樣的方法,我們也可以實(shí)現(xiàn)這樣的存儲目標(biāo),下表所示:
| Context 類提供的方法 | 使用目的 |
|---|---|
File getFilesDir() |
獲取/data/data/<packagename>/files 目錄 |
FileInputStream openFileInput(String name) |
打開現(xiàn)有文件進(jìn)行讀取 |
FileOutputStream openFileOutput(String name, int mode) |
打開文件進(jìn)行寫入,如不存在就創(chuàng)建它 |
File getDir(String name, int mode) |
獲取/data/data/<packagename>/目錄的子目錄(如不存在就先創(chuàng)建它) |
String[] fileList() |
獲取/data/data/<packagename>/files 目錄下的文件列表??膳c其他方法配合使用,例如 openFileInput(String) |
File getCacheDir() |
獲取/data/data/<packagename>/cache 目錄。應(yīng)注意及時清理該目錄,并節(jié)約使用空間 |
如果想存儲的文件僅供應(yīng)用內(nèi)部使用,使用上表中的各類方法就可以了。而如果想共享文件給其他應(yīng)用或是接收其他應(yīng)用的文件(如相機(jī)應(yīng)用拍攝的照片)時,路只有一條:使用外部存儲保存文件。
外部存儲有兩類:主外部存儲和其他各類存儲介質(zhì)。所有的 Android 設(shè)備至少應(yīng)有一個主外部存儲地。使用Environment.getExternalStorageDirectory()可以返回這個外部存儲目錄。 以前,這個存儲地通常是指 SD 卡,但現(xiàn)在都已基本整合至了設(shè)備內(nèi)部。即使現(xiàn)在還有設(shè)備使用擴(kuò)展外部存儲,也應(yīng)算作其他各類存儲介質(zhì)這一類了。
Context 也提供了一些訪問外部存儲空間要用到的方法,如下表所示。
| 方法 | 使用目的 |
|---|---|
File getExternalCacheDir() |
獲取主外部存儲上的緩存文件目錄。用法類似 getCacheDir()方法,但要注意,Android 一般不會自動清理該目錄 |
File[] getExternalCacheDirs() |
獲取多個外部存儲上的緩存文件目錄 |
File getExternalFilesDir(String) |
獲取主外部存儲上存放常規(guī)文件的文件目錄。通過 String 參數(shù),可訪問特定內(nèi)容類型的子目錄。內(nèi)容類型常量以 DIRECTORY_為前綴,定義在 Environment 中 。 例如 , 用于 圖像 文件 的 Environment.DIRECTORY_ PICTURES |
File[] getExternalFilesDirs(String) |
類似 getExternalFilesDir(String)方法,但該方法可獲取指定類型的所有文件目錄 |
File[] getExternalMediaDirs() |
獲取 Android 存儲圖片、視頻和音樂文件的所有外部文件目錄。和 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 方法 區(qū)別 在于,調(diào)用該方法,多媒體掃描器會自動掃描目標(biāo)目錄,并將存放的多媒體文件暴露給能夠播放音樂、瀏覽視頻和圖片的應(yīng)用。也就是說, getExternalMediaDirs()方法返回目錄中存放的任何文件都會自動出現(xiàn)在多媒體應(yīng)用中 |
1.1 指定照片存放位置
首先,一張照片的文件名我們用一個 Crime 的 ID 來標(biāo)識,所以在 Crime.java 中加入了獲取文件名的方法:
public String getPhotoFileName() {
return "IMG_" + getId().toString() + ".jpg";
}
然后在 CrimeLab.java 中加入獲取路徑文件的函數(shù):
public File getPhotoFile(Crime crime) {
File externalFilesDir = mContext
.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (externalFilesDir == null) {
return null;
}
return new File(externalFilesDir, crime.getPhotoFileName());
}
1.2 外部存儲使用權(quán)限
讀寫外部存儲需要獲得權(quán)限,一般在AndroidManifest.xml中使用<uses-permission>標(biāo)簽來使用。而對于 API 19(Android 4.4)及以后的新版系統(tǒng)來說,應(yīng)用不需要再申請 Context.getExternalFilesDir(String) 所需要的權(quán)限了,所以這個權(quán)限申請是這么寫的:
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
2. 使用相機(jī) intent
實(shí)現(xiàn)拍照功能只需要使用一個隱式 intent,分為下面幾步:
- 獲取保存圖片的文件存儲位置
- 處理拍照按鈕,實(shí)現(xiàn)觸發(fā)拍照,其實(shí)就是發(fā)送一個帶有
MediaStore.ACTION_IMAGE_CAPTURE的 intent 即可。
對于 intent 的操作,我們需要定義在 MediaStore 類中的ACTION_CAPTURE_IMAGE。MediaStore 類定義了一些公共接口,可用于處理圖像、視頻以及音樂這些常見的多媒體任務(wù)。當(dāng)然,這也包括觸發(fā)相機(jī)應(yīng)用的拍照 intent。
如果只用ACTION_IMAGE_CAPTURE打開相機(jī)應(yīng)用,默認(rèn)只能拍攝縮略圖這樣的低分辨率照片,而且照片會保存在 onActivityResult(...)返回的 Intent 對象里。要想獲得全尺寸照片,就要讓它使用文件系統(tǒng)存儲照片。這可以通過傳入保存在 MediaStore.EXTRA_OUTPUT 中的指向存儲路徑的 Uri 來完成。
編寫用于拍照的隱式 intent,拍攝的照片應(yīng)該保存在 mPhotoFile 指定的地方。同時,別忘了檢查設(shè)備上是否安裝有相機(jī)應(yīng)用,以及是否有地方存儲照片。
mPhotoButton = (ImageButton) v.findViewById(R.id.crime_camera);
// 首先創(chuàng)建一個用于拍照的 Intent 對象
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 檢查是否有可拍照的應(yīng)用
boolean canTakePhoto = mPhotoFile != null &&
captureImage.resolveActivity(packageManager) != null;
mPhotoButton.setEnabled(canTakePhoto);
if (canTakePhoto) {
// 建立訪問照片目錄的 Uri
Uri uri = Uri.fromFile(mPhotoFile);
// 將該 Uri 放入 intent 對象中
captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}
mPhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 使用 startActivityForResult 是為了拍完照后刷新視圖
startActivityForResult(captureImage, REQUEST_PHOTO);
}
});
3. 縮放和顯示位圖
有了照片,接下來就是找到并加載它,然后展示給用戶看。在技術(shù)實(shí)現(xiàn)上,這需要加載照片到大小合適的 Bitmap 對象中。而要從文件生成 Bitmap 對象,我們需要 BitmapFactory 類:
Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getPath());
Bitmap 是個簡單對象,它只存儲實(shí)際像素?cái)?shù)據(jù)。也就是說,即使原始照片已壓縮過,但存入 Bitmap 對象時,文件并不會同樣壓縮。因此,如果有一個16萬像素24位已壓縮為5Mb 大小的 JPG 照片文件,一旦載入 Bitmap 對象,就會立即膨脹至48Mb 大小!
這個問題可以設(shè)法解決,但需要手工縮放位圖照片。具體做法就是,首先確認(rèn)文件到底有多大,然后考慮按照給定區(qū)域大小合理縮放文件。最后,重新讀取縮放后的文件,創(chuàng)建 Bitmap 對象。
既然需要處理圖像文件,我們建立一個通用的工具類,名為 PictureUtils.java。在其中添加 getScaledBitmap(String, int, int)縮放方法,
public class PictureUtils {
public static Bitmap getScaledBitmap(String path, int destWidth, int destHeight) {
// Read in the dimensions of the image on disk
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
float srcWidth = options.outWidth;
float srcHeight = options.outHeight;
// Figure out how much to scale down by
int inSampleSize = 1;
if (srcHeight > destHeight || srcWidth > destWidth) {
if (srcWidth > srcHeight) {
inSampleSize = Math.round(srcHeight / destHeight);
} else {
inSampleSize = Math.round(srcWidth / destWidth);
}
}
options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
// Read in and create final bitmap
return BitmapFactory.decodeFile(path, options);
}
}
上述方法中,inSampleSize 值很關(guān)鍵。它決定著縮略圖像素的大小。假設(shè)這個值是1的話,就表明縮略圖和原始照片的水平像素大小一樣。如果是2的話,它們的水平像素比就是1∶2。因此,inSampleSize 值為2時,縮略圖的像素?cái)?shù)就是原始文件的四分之一。
問題總是接踵而來。解決了縮放問題,又冒出了新問題:fragment 剛啟動時,PhotoView 究竟有多大無人知道。onCreate(...)、onStart()和 onResume()方法啟動后,才會有首個實(shí)例化布局出現(xiàn)。也就在此時,顯示在屏幕上的視圖才會有大小尺寸。這也是出現(xiàn)新問題的原因。
解決方案有兩個:要么等布局實(shí)例化完成并顯示,要么干脆使用保守估算值。特定條件下, 盡管估算比較主觀,但確實(shí)是一個切實(shí)可行的辦法。再添加一個 getScaledBitmap(String, Activity)靜態(tài) Bitmap 估算方法。
public static Bitmap getScaledBitmap(String path, Activity activity) {
Point size = new Point();
activity.getWindowManager().getDefaultDisplay()
.getSize(size);
return getScaledBitmap(path, size.x, size.y);
}
4. 功能聲明
應(yīng)用的拍照功能用起來不錯,但還有件事情要做:告訴目標(biāo)用戶應(yīng)用具有拍照功能。
假如應(yīng)用要用到諸如相機(jī)、NFC,或者任何其他的隨設(shè)備走的功能時,都應(yīng)該要讓 Android 系統(tǒng)知道。否則,假如設(shè)備缺少這樣的功能,類似 Google Play 商店的安裝程序就會拒絕安裝應(yīng)用。
為聲明需要使用相機(jī),在 AndroidManifest.xml 中加入<uses-feature>標(biāo)簽:
<uses-feature
android:name="android.hardware.camera2"
android:required="false"/>
5. 布局文件中的 <include> 標(biāo)簽
如果有重復(fù)的布局可以使用,那么可以采用 include 標(biāo)簽,直接在不同的 layout 中引用。
然而,經(jīng)驗(yàn)表明,布局文件的優(yōu)點(diǎn)是可靠又好用。例如,直接查看布局文件內(nèi)容,就可以快速準(zhǔn)確地知道應(yīng)用視圖是如何構(gòu)建的。然而,一旦用了 include 標(biāo)簽,一切就不好說了。還想明白視圖構(gòu)成的話,就得仔細(xì)翻看布局主文件以及所有 include 的布局文件。這種非直觀的感覺,極易讓人失去耐心。
用戶界面是應(yīng)用改動相對頻繁的部分。既然這樣,不顧一切地追求復(fù)用原則很可能會適得其反。因此,在視圖層開發(fā)時,我們一定要多多考量,盡量做到審慎、合理地使用 include 標(biāo)簽。
6. 挑戰(zhàn)練習(xí)
6.1 優(yōu)化照片顯示
新建一個 GlancePictureFragment,繼承自 DialogFragment,代碼如下:
public class GlancePictureFragment extends DialogFragment {
private static final String ARG_PATH = "path";
private ImageView mImage;
// 由于文件比較大,所以將文件路徑傳入即可
public static GlancePictureFragment newInstance(String path) {
Bundle args = new Bundle();
args.putString(ARG_PATH, path);
GlancePictureFragment fragment = new GlancePictureFragment();
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// 使用 getArguments() 方法取出照片文件路徑
String path = getArguments().getString(ARG_PATH);
// 這個新的 style 其實(shí)就做了一件事,那就是使窗口全屏
// 注意如果繼承了 @android:Theme.Dialog 的話,窗口
// 大小就限定了,所以我沒有繼承
final Dialog dialog = new Dialog(getActivity(), R.style.CustomDialogTheme);
// 這個 layout 中只有一個 ImageView
dialog.setContentView(R.layout.dialog_image_glance);
mImage = (ImageView) dialog.findViewById(R.id.glance_image);
// 仍然使用 PictureUtils 類的工具來獲得縮放的 Bitmap
mImage.setImageBitmap(
PictureUtils.getScaledBitmap(path, getActivity()));
mImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 點(diǎn)擊圖片則退出該 dialog
dialog.dismiss();
}
});
return dialog;
}
}
然后在圖片的點(diǎn)擊事件中聲明即可
6.2 優(yōu)化縮略圖加載
首先修改更新視圖的函數(shù),接受高寬的指定像素:
private void updatePhotoView(int width, int height) {
if (mPhotoFile == null || !mPhotoFile.exists()) {
mPhotoView.setImageDrawable(null);
} else {
Bitmap bitmap = PictureUtils.getScaledBitmap(
mPhotoFile.getPath(), width, height);
mPhotoView.setImageBitmap(bitmap);
}
}
之后,先獲取 mPhotoView 的 ViewTreeObserver,然后設(shè)置 OnGlobalLayoutListener 監(jiān)聽器,在監(jiān)聽器中即可獲取視圖的高度和寬度,然后進(jìn)行圖片顯示。
mPhotoObserver = mPhotoView.getViewTreeObserver();
mPhotoObserver.addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
updatePhotoView(
mPhotoView.getWidth(),
mPhotoView.getHeight());
Log.i("CrimeFragment", "onGlobalLayout: Observed");
}
});
GitHub Page: kniost.github.io
簡書:http://m.itdecent.cn/u/723da691aa42