Android進階封裝之一個類實現相容Android 6.0許可權、適配Android7.0 拍照!
Android進階封裝之“史無前例”一個類實現相容Android 6.0許可權、適配Android 拍照7.0: 相機與相簿上傳圖片就用我好啦!
一、前言。
本篇部落格從基本的AndroidN開始說怎麼適配其拍照,其與7.0以下有何區別?
再詳細分析如何封裝在 avtivity和fragment中的區別? (重點)
再詳細說明下本封裝庫如何整合與使用。
博主最近為了適配 AndroidN的拍照,瀏覽了很多技術文章,有些是需要4到5個類來實現、有些不能在fragment中實現… … ,個人敢想 “可以一個類實現封裝全部拍照工作嗎?” , 揚起袖子就是幹!在辛苦的四個小時,終於把這個封裝給弄出來的!
已經相容在小米手機出現Attempt to invoke interface method ‘boolean android.database.Cursor.moveToFirst()問題 。(2017/8/19)
已經相容在fragment出現許可權授權不回撥的bug。(2017/8/19)
已經相容在fragmenr出現圖片不回撥的bug。(2017/8/18)
二、Android7.0和其以下的版本在拍照時候有何區別?
由於從Android7.0(下面統一為AndroidN)開始,直接使用真實的路徑的Uri會被認為是不安全的,會丟擲一個FileUriExposedException這樣的異常。需要使用FileProvider,選擇性地將封裝過的Uri共享到外部。
出於以上問題,很多事情都意味著要適配,比如你在AndroidN以下,可以跳轉到拍照和相簿介面,但是在AndroidN就不行了!但是會有error等級的log輸出,出現FileUriExposedException這樣的異常,原因是Andorid7.0的“私有目錄被限制訪問”,“StrictMode API 政策”。
谷歌這樣做,出自使用者隱私的考慮。既然這樣,我們就必須要通過FileProvider(Provider的一個子類)共享其URL到外部即可。
問題來了,FileProvider應該如何寫?
- 1.在manifest中新增provider,畢竟Provider也是屬於四大元件之一。配置中的authorities按照江湖規矩一般加上包名,${applicationId}是獲取當前專案的包名,前提是在module下的gradle.buile檔案中defaultConfig{}閉包中要有applicationId屬性哦。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths"/>
</provider>
2、在您工程的 res 資料夾根目錄下建立一個xml資料夾,裡面再新建一個file_provider_paths資料夾,其對應都是在上面的meta-data標籤下面的android:resource值的。
- 程式碼中path=”“,是有特殊意義的,它程式碼根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個檔案了。其file_provider_paths內容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="" name="myFile"/>
</paths>
</resources>
- 3、 自此為止,已經把FileProvider的基本的環境搭建好了,別忘了加拍照許可權、讀取和儲存SD卡的許可權哦:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
三、熟悉拍照的整個流程。
①、你先要建立一個URL作為你拍照後得到的圖片的URL , 這裡我們使用 intent , 制定Action為 MediaStore.ACTION_IMAGE_CAPTURE , 這樣就可以跳轉到相機介面了 ,別忘了,在intent上把你要傳的URL放上去,名字一定要是 :MediaStore.EXTRA_OUTPUT 。最後使用startActivityForResult()跳轉,別忘了回撥碼。
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri); startActivityForResult(intent, CODE_ORIGINAL_PHOTO_CAMERA)
②、等到跳轉相機介面後,我們不需理會使用者怎麼操作,我們只需關心傳回來的Uri資料是否為空,畢竟在拍照後用戶可能點選了捨棄,導致拿到相片為空。
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { //相片處理 if (resultCode != RESULT_CANCELED) { switch (requestCode) { //相簿資料,回撥碼要和上面一致。 case IMAGE_REQUEST_CODE: //判斷返回的資料Uri是否為空? if(imgUri!=null){ //doyourthings } break; } }
下面是整個拍照流程圖:
四、熟悉從相簿拿圖片的整個流程。
- ①、從本地相簿拿圖片的原理和相機拍照一樣,也是靠Intent跳轉到相簿介面,指定的Action為Intent.ACTION_PICK,Type為 “image/*” ,別忘了回撥碼。
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, IMAGE_REQUEST_CODE);
- ②、與拍照不同的是,onActivityResult()方法成功返回的話。那麼傳回來的是一個圖片檔案哦。同樣,先判斷是否為空?
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
//相片處理
if (resultCode != RESULT_CANCELED) {
switch (requestCode) {
//相簿資料,回撥碼要和上面一致。
case IMAGE_REQUEST_CODE:
//判斷返回的資料data.getData()是否為空?
if(data.getData()!=null){
//doyourthings
}
break;
}
}
下面是整個相簿選擇圖片流程圖:
五、動態許可權?
讓不少人煩惱的是,在安卓6.0之後,需要動態授權,那麼作為拍照、寫入SD卡和讀取SD卡,這些“危險許可權”,動態授權是必然的。
我這裡採用郭神的做法,如果使用者拒絕的某些許可權的話,會通過介面提示。程式碼如下:
/**
* 申請執行時許可權
* 來自郭神公開課
*/
private void requestRuntimePermission(String[] permissions, PermissionListener listener) {
permissionListener = listener;
List<String> permissionList = new ArrayList<>();
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(mContext, permission) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(permission);
}
}
//此處相容了無法在fragment回撥監聽事件
if (!permissionList.isEmpty()) {
if (isActicity) {
ActivityCompat.requestPermissions((Activity) mContext, permissionList.toArray(new String[permissionList.size()]), 1);
} else {
mFragment.requestPermissions(permissionList.toArray(new String[permissionList.size()]), 1);
}
if (takeCallBacklistener != null) {
takeCallBacklistener.failed(1, permissionList);
}
} else {
permissionListener.onGranted();
}
}
六、關於裁剪的程式碼!
private void statZoom(File srcFile, File output) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(getImageContentUri(mContext, srcFile), "image/*");
// crop為true是設定在開啟的intent中設定顯示的view可以剪裁
intent.putExtra("crop", "true");
// 是否縮放?如果不縮放,會出現黑邊哦
intent.putExtra("scale", true);
// aspectX aspectY 是寬高的比例
intent.putExtra("aspectX", aspectX);
intent.putExtra("aspectY", aspectY);
// outputX,outputY 是剪裁圖片的寬高
intent.putExtra("outputX", outputX);
intent.putExtra("outputY", outputY);
intent.putExtra("return-data", false);//true:不返回uri,false:返回uri
intent.putExtra("scaleUpIfNeeded", true);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(output));
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
//此處相容在fragment不會回撥圖片問題
if (isActicity) {
mActivity.startActivityForResult(intent, CODE_TAILOR_PHOTO);
} else {
mFragment.startActivityForResult(intent, CODE_TAILOR_PHOTO);
}
}
七、封裝的主角來了!
6.1 、介紹只需三步的環境整合:
- 第一步:把 demo下的res的 xml資料夾整個複製到你的工程res資料夾根目錄下:
- 第二步:在你AndroidManifest.xml下的Application節點下加入以下程式碼:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths"/>
</provider>
- 如下所示
-第三步 :把demo的就僅僅一個類 TakePictureManager.class 複製過去就可以啦!別忘了在清單檔案加相關許可權哦!
6.2 、怎麼使用?
示例:
TakePictureManager takePictureManager takePictureManager = new TakePictureManager(this);
//開啟裁剪 比例 1:3 寬高 350 350 (預設不裁剪)
takePictureManager.setTailor(1, 3, 350, 350);
//拍照方式
takePictureManager.startTakeWayByCarema();
//監聽回撥
takePictureManager.setTakePictureCallBackListener(new TakePictureManager.takePictureCallBackListener() {
//成功拿到圖片,isTailor 是否裁剪? ,outFile 拿到的檔案 ,filePath拿到的URl
@Override
public void successful(boolean isTailor, File outFile, Uri filePath) {
}
//失敗回撥
@Override
public void failed(int errorCode, List<String> deniedPermissions) {
}
});
//把本地的onActivityResult()方法回撥繫結到物件
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
takePictureManager.attachToActivityForResult(requestCode, resultCode, data);
}
//onRequestPermissionsResult()方法許可權回撥繫結到物件
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
takePictureManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
使用詳細步驟:
①、先new一個TakePictureManager 物件,構造方法只需傳this即可(不管你在Activity還是在Fragment)。
②、重寫onActivityResult()方法,呼叫物件的attachToActivityForResult()方法,引數依次是onActivityResult()回撥的引數。 實現把拍照或相簿回調發資料繫結在物件方法處理。
③、重寫onRequestPermissionsResult()方法,呼叫物件的onRequestPermissionsResult()方法,引數依次是onRequestPermissionsResult()回撥的引數。 實現把許可權回撥繫結在物件方法處理。
④、這時候,你只需呼叫物件方法,即可輕鬆呼叫相機或相簿。具體的方法引數說明如下:
方法名 | 引數 | 說明 |
---|---|---|
setTailor(int aspectX, int aspectY, int outputX, int outputY) | 要裁剪的寬比例、要裁剪的高比例、要裁剪圖片的寬、要裁剪圖片的高 | 一旦呼叫,表示要裁剪,預設不裁剪 |
startTakeWayByCarema() | 無引數 | 呼叫相機 |
startTakeWayByAlbum() | 無引數 | 呼叫相簿 |
setTakePictureCallBackListener(takePictureCallBackListener listener) | takePictureCallBackListener 回撥介面 | 呼叫相機或相簿後的回撥 |
介面 | 方法 | 說明 |
---|---|---|
takePictureCallBackListener | successful(boolean isTailor, File outFile, Uri filePath) | 成功回撥! isTailor : 是否已裁剪, outFile :輸出的照片檔案 ,filePath :輸出的照片Uri 。 |
failed(int errorCode, List deniedPermissions) | 失敗回撥!errorCode :0表示相片已移除或不存在! 1表示許可權被拒絕,deniedPermissions當權限被拒絕時候,會通過list傳回 |
八、造輪子時候遇到的問題:
在Fragment使用時候,回撥的相片資料被依附的activity的onActivityResult()方法攔截了!相信這個問題困擾許多人的問題,在使用他人程式碼時候,在Activity可以使用,但是在Fragment卻失敗。原因在於:
- 在Fragment就存在startActivityForResult()方法,不需要 getActivity().startActivityForResult() , 也就是說不需要呼叫 依附的Activity的此方法,本身就有此方法。這是我翻閱 Fragment原始碼發現。截圖如下:
- 於是乎我在封裝時候,特意這樣做: