一、前文
之前使用的圖片裁剪功能一直是使用第三方的,也沒時間去思考自己寫一個的想法。後來無意間發現android自己本來就有裁剪功能,所以自己動手去整合了一把,並且把自己的許可權封裝以及7.0的適配都加進去
二、注意的幾個點
其實也沒有什麼好說的,基本沒有難度,只是有幾個需要注意的點
1.一個是7.0的檔案安全機制,7.0之後android對於檔案的安全增加了保護,在部分地方使用Uri會產生FileUriExposedException檔案暴露異常。
2.其次,就是對於許可權的封裝,只有拿到了許可權才能進行操作,做好許可權適配,這些下面會一併講到。
三、許可權封裝
許可權封裝最好封裝到一個方法裡面,獨立處理許可權,這樣也可以比較輕鬆的整合在BaseActivity裡面,在用的地方直接調,可以達到許可權隨用隨取得效果。先看看關鍵程式碼
public void needPermission(AppPermissionListener mAppPermissionListener, List<String> permissions) {
if (null != permissions && permissions.size() > 0) {
this.appPermissionListener = mAppPermissionListener;
this.allPermission = permissions;
// 允許的許可權
allowPermission.clear();
// 被拒絕的許可權
deniedPermission.clear();
// 不在詢問的許可權
neverAskPermission.clear();
// 開始遍歷拿到的許可權
Observable.fromIterable(allPermission)
.subscribe(new Consumer<String>() {
@Override
public void accept(String s) throws Exception {
// 判斷是否有WRITE_SETTINGS特殊許可權
if (s.equals(Manifest.permission.WRITE_SETTINGS)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// WRITE_SETTINGS需要用該方式判斷
if (!Settings.System.canWrite(mActivity))
allowPermission.add(s);
else
haveWriteSetting = true;
}
} else if (ActivityCompat.checkSelfPermission(mActivity, s)
== PackageManager.PERMISSION_GRANTED) {
allowPermission.add(s);
} else {
deniedPermission.add(s);
}
}
});
if (haveWriteSetting) {
// 請求特殊許可權
requestWriteSettings();
return;
}
if (!deniedPermission.isEmpty()) {
String[] tempArray = new String[deniedPermission.size()];
deniedPermission.toArray(tempArray);
requestPermissions(tempArray);
if (null != mAppPermissionListener) {
mAppPermissionListener.onHaveDenied(deniedPermission);
}
return;
}
if (!neverAskPermission.isEmpty()) {
showNeverAskDialog();
if (null != mAppPermissionListener) {
mAppPermissionListener.onNeverAsk(neverAskPermission);
}
return;
}
// 許可權通過,回撥onAllGranted方法
if (null != mAppPermissionListener) {
mAppPermissionListener.onAllGranted();
}
}
}
複製程式碼
android有兩個特殊許可權,一個是WRITE_SETTINGS,另外一個是SYSTEM_ALERT_WINDOW,這裡只對WRITE_SETTINGS做了處理,我在想有沒有更優雅的方法把SYSTEM_ALERT_WINDOW加進去,所以後面會完善。
在這裡處理了會觸發拒絕許可權的彈框。但是當使用者點選了不再詢問許可權,就不會再彈框。android原生給我們提供了shouldShowRequestPermissionRationale()方法來判斷是否不再彈框。但是 這個方法會在部分手機上失效,比如我手中的魅族pro6手機,可能由於ROM問題,在呼叫該方法的時候就是失效的。那麼,我們可以用另外一種方法來達到相同的目的。
我們可以直接呼叫requestPermissions方法,在onRequestPermissionsResult的許可權回撥方法中進行檢查,如果拒絕許可權列表中還有這個許可權的話,就可以進行彈框引導使用者去設定中手動開啟。不但替代了shouldShowRequestPermissionRationale方法,而且在使用者一通拒絕之後還能給予提醒。下面是onRequestPermissionsResult方法的程式碼
// 許可權返回結果
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CODE) {
for (int i = 0; i < permissions.length; i++) {
String per = permissions[i];
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
if (deniedPermission.contains(per)) {
deniedPermission.remove(per);
} else if (neverAskPermission.contains(per)) {
neverAskPermission.remove(per);
}
if (!allowPermission.contains(per))
allowPermission.add(per);
} else {
if (deniedPermission.contains(per)) {
deniedPermission.remove(per);
neverAskPermission.add(per);
}
}
}
if (!deniedPermission.isEmpty()) {
neverAskPermission.addAll(new ArrayList<>(deniedPermission));
deniedPermission.clear();
}
if (!neverAskPermission.isEmpty()) {
showNeverAskDialog();
if (null != appPermissionListener) {
appPermissionListener.onNeverAsk(neverAskPermission);
}
return;
}
if (null != appPermissionListener) {
appPermissionListener.onAllGranted();
}
}
}
複製程式碼
四、7.0檔案安全機制
不少安卓應用開發的程式設計師,一直都很少有機會接觸到安卓四大元件之一的Content Provider,但是由於android7.0檔案安全機制的限制,使我們不得不去接觸這個元件。我個人覺得這也是一件好事情。不用再只侷限於部分元件的開發。
首先我們需要在res檔案下簡歷一個xml資料夾,然後再建立一個xml檔案,這個xml檔名字自己可以隨便命名,這裡我命名為file_path。如下圖所示
file_path裡面的內容有事有講究的。裡面有一個root-path 節點,雖說android會提示element roo-path is not allowed here。但是我目前還沒有找到在不用這個節點的時候,能夠正常執行的手段。如果有哪位大神知道,可以指出。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="my_images"
path="path/" />
<cache-path
name="my_cache"
path="path/" />
<external-files-path
name="my_file"
path="path/" />
<external-path
name="myApp_file"
path="path/" />
<external-cache-path
name="myApp_cache"
path="path/" />
<root-path name="myApp"
path="" />
</paths>
複製程式碼
然後,我們需要建立一個自己的provider。這樣在問題出現的時候有利於查詢問題。最後,我們需要在Manifest裡面聲名這個Provdier。如下所示
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.common.MyImageFileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
複製程式碼
這裡使用了${applicationId}來代替包名很方便,但是在java程式碼中還是需要寫完全。 接下來說使用。使用起來就會變得很簡單,一句程式碼就可以了
FileProvider.getUriForFile(context, DEFAULT_AUTHORITIES, imageFile)
複製程式碼
這個方法會返回Uri物件,在需要的地方進行使用。
五、如何呼叫相機、相簿以及裁剪工具
相機和相簿的呼叫,網上其實已經在很多地方寫爛了,我這裡直接貼出我的方法
/**
* 開啟相簿
*/
private void openGallery() {
initImageFile();
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
((Activity) context).startActivityForResult(intent, REQUEST_PICK);
}
/**
* 開啟攝像頭
*/
private void doPhoto() {
initImageFile();
Uri uri = FileProvider.getUriForFile(context, DEFAULT_AUTHORITIES, imageFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.putExtra(MediaStore.EXTRA_SCREEN_ORIENTATION,
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
((Activity) context).startActivityForResult(intent, REQUEST_PICK);
}
複製程式碼
接下來,在開啟系統自帶裁剪工具的時候會有個坑,也是android 7.0的檔案安全機制的問題。不過這個和之前的檔案安全機制不一樣。使用FileProvider會提示"無法載入該圖片"。通過找到該圖片路徑可以瞭解到,android7.0的檔案安全機制其實是希望能做成類似IOS沙盒機制的效果,每個app只能訪問自己的沙盒,但是從系統相簿拿到的圖片卻不屬於當前app本身,所以我們這時候就需要一個臨時授權。intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
呼叫系統裁剪功能的程式碼如下
private void beginCropDirect(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
//新增這一句表示對目標應用臨時授權該Uri所代表的檔案
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//可以選擇圖片型別,如果是*表明所有型別的圖片
intent.setDataAndType(uri, "image/*");
// 下面這個crop = true是設定在開啟的Intent中設定顯示的VIEW可裁剪
intent.putExtra("crop", "true");
// aspectX aspectY 是寬高的比例,這裡設定的是正方形(長寬比為1:1)
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
// outputX outputY 是裁剪圖片寬高
intent.putExtra("outputX", 500);
intent.putExtra("outputY", 500);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
//是否將資料保留在Bitmap中返回,true返回bitmap,false返回uri
intent.putExtra("return-data", false);
((Activity) context).startActivityForResult(intent, REQUEST_CROP);
}
複製程式碼
本文程式碼:https://github.com/Kongdy/ImageCropBySystem
個人github地址:https://github.com/Kongdy
個人掘金主頁:https://juejin.im/user/595a64def265da6c2153545b
csdn主頁:http://blog.csdn.net/u014303003