前言
最近在自己的項目里實現(xiàn)了一個頭像選擇的功能因宇,就是先從相冊里選取一張圖片再調(diào)用系統(tǒng)的裁剪功能來制作頭像,效果就像下面這樣:
本以為很小的一個功能坏匪,卻遠(yuǎn)遠(yuǎn)沒有我想的那樣簡單穴张,可以說每一步都暗藏玄機,下面就讓我?guī)Т蠹铱纯催@里面究竟有哪些坑枪蘑。
Android 4.4之存儲訪問框架
首先损谦,讓我們從圖片選擇開始岖免,使用隱式Intent跳轉(zhuǎn)到圖片選擇:
private void routeToGallery() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, GALLERY_REQUSET_CODE);
}
在回調(diào)中處理返回的圖片,繼而跳轉(zhuǎn)至圖片裁剪:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == GALLERY_REQUSET_CODE && resultCode == RESULT_OK) {
String path = data.getData().getPath();
Bitmap image = BitmapFactory.decodeFile(path);
File faceFile;
try {
faceFile = saveBitmap(image);
} catch (IOException e) {
e.printStackTrace();
return;
}
Uri fileUri = Uri.fromFile(faceFile);
routeToCrop(fileUri); //跳轉(zhuǎn)到圖片裁剪
}
}
private void routeToCrop(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", true);
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 150);
intent.putExtra("outputY", 150);
intent.putExtra("return-data", true);
startActivityForResult(intent, CROP_REQUEST_CODE);
}
private File saveBitmap(Bitmap bitmap) throws IOException {
File file = new File(getExternalCacheDir(), "face-cache");
if (!file.exists()) file.createNewFile();
try (OutputStream out = new FileOutputStream(file)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
}
return file;
}
這一段代碼看似正常照捡,但問題就出在String path = data.getData().getPath();
這一句颅湘。這一段代碼在Android 4.4以下是可以正常運行的,不過從Android 4.4開始這里獲取到的將為一個無效的路徑栗精,這是為什么呢闯参?
Android從4.4開始引入了一個概念:存儲訪問框架,簡單來說就是Android提供了一個專門供用戶訪問資源的軟件悲立,將設(shè)備上所有可以訪問資源的軟件接口都整合到了一起鹿寨,避免了用戶只能選擇一個特定軟件的尷尬,在Android 4.4以下薪夕,我們發(fā)送剛才選取圖片的隱式Intent脚草,效果是這樣的,需要用戶去選擇使用哪個應(yīng)用:
而從Android 4.4開始原献,就變成了這樣:
直接打開一個資源選取的軟件(這個軟件平時是隱藏的馏慨,不會顯示在軟件列表中),其中包含了訪問設(shè)備上所有可訪問資源軟件的接口姑隅,這個改變極大的提高的用戶操作的便捷性写隶。
不過這也帶來了一個問題,從Android 4.4開始讲仰,在onActivityResult()
方法的Intent
中所包含的uri
不再是file://
類型慕趴,而是變成了content://
類型,這也是為什么在Android 4.4以后調(diào)用data.getData.getPath()
獲取到的結(jié)果是無效的鄙陡。因此冕房,我們必須對Android 4.4以上的版本進行特殊的處理:
private void routeToGallery() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
startActivityForResult(intent, GALLERY_REQUSET_CODE_KITKAT);
} else {
startActivityForResult(intent, GALLERY_REQUSET_CODE);
}
}
在回調(diào)中對不同版本分別進行處理:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case GALLERY_REQUSET_CODE:
handleGalleryResult(resultCode, data);
break;
case GALLERY_REQUSET_CODE_KITKAT:
handleGalleryKitKatResult(resultCode, data);
break;
}
}
private void handleGalleryResult(int resultCode, Intent data) {
// 跟之前一樣
}
// Result uri is "content://" after Android 4.4
private void handleGalleryKitKatResult(int resultCode, Intent data) {
File faceFile;
try {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(contentUri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
faceFile = saveBitmap(image);
} catch (IOException e) {
e.printStackTrace();
return;
}
Uri fileUri = Uri.fromFile(faceFile);
routeToCrop(fileUri);
}
Android 7.0之FileProvider
完成了圖片的選擇功能,轉(zhuǎn)眼又碰到了一個問題:
Android為了提高私有文件的安全性柔吼,從7.0開始對外傳遞file://
類型的uri
會觸發(fā)FileUriExposedException
毒费。因此丙唧,在分享私有文件時必須使用FileProvider
愈魏。
對Android的這一改變還不太了解的同學(xué)可以看一下這兩篇文章:Android 7.0 行為變更和Setting Up File Sharing。
第一步
在manifest
文件中加入FileProvider
:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gavinli.translator">
<application
...>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="gavinli.translator"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
</application>
</manifest>
第二步
在xml
文件夾中創(chuàng)建filepaths.xml
文件想际,并聲明所要分享的文件目錄:
<resources>
<paths>
<external-cache-path name="mycache" path="./" />
</paths>
</resources>
這里的path
就代表你想要分享的文件目錄培漏,而name
就是具體顯示在uri
中的信息,最終生成的uri
就像下面這樣:
這種經(jīng)過處理的uri
可以很好的隱藏掉實際的文件路徑胡本。
第三步
在代碼中對Android 7.0以上的版本進行特殊處理:
private void handleGalleryKitKatResult(int resultCode, Intent data) {
...
Uri fileUri;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Android 7.0 "file://" uri權(quán)限適配
fileUri = FileProvider.getUriForFile(this,
"gavinli.translator", faceFile);
} else {
fileUri = Uri.fromFile(faceFile);
}
routeToCrop(fileUri);
}
這里傳入的"gavinli.translator"
牌柄,需要與之前在manifest
文件中聲明的android:authorities
一致。
第四步
在裁剪圖片的Intent
中加入對該圖片的訪問權(quán)限:
private void routeToCrop(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
// 加入訪問權(quán)限
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
...
}
最后一步
在回調(diào)中獲取裁剪后的圖片:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
...
case CROP_REQUEST_CODE:
Bundle bundle = data.getExtras();
Bitmap face = bundle.getParcelable("data");
break;
}
}
Intent的限制
你以為到這里就結(jié)束了嗎侧甫?其實還遠(yuǎn)遠(yuǎn)沒有珊佣。我們這里裁剪的圖片是用作頭像的蹋宦,所以大小一般都比較小≈涠停可以當(dāng)圖片的大小變大后就會發(fā)現(xiàn)冷冗,每次裁剪后在Intent
中獲取到的圖片其實都是縮略圖。
這是因為Android對Intent
中所包含數(shù)據(jù)的大小是有限制的惑艇,一般不能超過1M蒿辙,否則應(yīng)用就會崩潰,這就是Intent
中的圖片數(shù)據(jù)只能是縮略圖的原因滨巴。而解決的辦法也很簡單思灌,我們需要給圖片裁剪應(yīng)用指定一個輸出文件,用來存放裁剪后的圖片:
private void routeToCrop(Uri uri) {
...
intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
new File(getExternalCacheDir(), "face-cropped")));
startActivityForResult(intent, CROP_REQUEST_CODE);
}
現(xiàn)在恭取,在回調(diào)中的圖片就不能再直接從Intent
中獲取了泰偿,而是需要先拿到Intent
中的uri
,再使用uri
進行獲取蜈垮,具體的過程和之前處理uri
的方式一樣甜奄,這里就不再贅述了。當(dāng)然窃款,直接從之前指定的文件中讀取數(shù)據(jù)也是可以的课兄。
Android 6.0之運行時權(quán)限
不知道大家發(fā)現(xiàn)了沒有,之前保存圖片的目錄都是使用的Context.getExternalCacheDir()
晨继,這個方法獲取到的目錄為/sdcard/Android/data/gavinli.translator/cache
烟阐,是應(yīng)用專屬的外部存儲空間,不需要聲明權(quán)限紊扬。而要想使用公共的存儲空間蜒茄,就勢必要面對一個問題:Android 6.0的運行時權(quán)限。
首先餐屎,在manifest
文件中聲明讀取外置存儲的權(quán)限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="gavinli.translator">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
...
</manifest>
之后檀葛,在代碼中加入運行時的權(quán)限申請:
private void request() {
String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
if(ContextCompat.checkSelfPermission(this, permisson)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(permissions, REQUEST_CODE);
} else {
// 存儲圖片
}
}
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if(requestCode == REQUEST_CODE) {
if(grantResults[i] == PackageManager.PERMISSION_GRANTED) {
// 存儲圖片
}
}
}
后記
到這里,這一次的踩坑之旅就全部結(jié)束了腹缩,我們也看到了Android這幾個版本以來一步步對權(quán)限的限制屿聋,雖然這對我們的開發(fā)產(chǎn)生一定的影響,但只要能提高用戶的使用體驗藏鹊,這點困難又算的了什么呢润讥?