选择图片并上传是前端常会面临的需求,6.0以后需要动态权限适配,7.0 以后由于应用间共享文件的限制则需要授予URI临时访问权限。
一、权限
1、相册需要读取存储卡的权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
2、拍照需要写入存储卡的权限以及摄像头的权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
二、应用间文件共享
授予URI临时访问权限,最简单方式是使用 FileProvider 类。
1、在AndroidManifest.xml 中声明 FileProvider
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="<yourpackername>.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
2、在 res/xml 文件夹下创建 file_paths.xml 文件
名称( file_paths.xml ) 需要与 FileProvider 中 meta-data 指向的文件一致。FileProvider 传送门
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" /> //代表设备的根目录new File("/");
<files-path name="files" path="files/" /> // 代表context.getFilesDir()
<files-path name="images" path="images/" /> // 代表context.getFilesDir()
<cache-path name="cache" path="cache/" /> // 代表context.getCacheDir()
<external-path name="external" path="external/" /> // 代表Environment.getExternalStorageDirectory()
<external-files-path name="external_images" path="external/images/" /> // 代表context.getExternalFilesDirs()
<external-files-path name="external_files" path="external/files/" /> // 代表context.getExternalFilesDirs()
<external-cache-path name="external_cache" path="external/cache/" /> // 代表getExternalCacheDirs()
</paths>
- name:属性是用来隐藏具体子目录用的,即name会出现在URI中,并可对应到实际的path子目录。
- path:属性是实际的子目录
三、文件工具类
public static File createTempImageFile(@NonNull Context context) {
String filename = DateUtil.getNowStr("yyyyMMdd_HHmmss");
File file = new File(context.getExternalCacheDir() + "/temp");
if (!file.exists()) {
if (!file.mkdirs()) return null;
}
try {
return File.createTempFile(filename, ".jpg", file);
} catch (IOException e) {
Log.e("FILE_UTIL", "创建临时图片失败", e);
return null;
}
}
public static Uri fileToUri(@NonNull Context context, @NonNull File file) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return Uri.fromFile(file);
return FileProvider.getUriForFile(context.getApplicationContext(), BuildConfig.APPLICATION_ID + ".fileprovider", file);
}
public static void grantUriPermission(@NonNull Context context, @NonNull Intent intent, @NonNull Uri uri) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
}
五、获取权限
我们一般会在应用首次启动时,展示所需要的权限,及权限的用途,并由用户决定要授予的权限。然后根据用户的选择真正地索要权限。详见[Android优雅的权限处理(还未开写,嘿嘿)]
implementation 'com.github.tbruyelle:rxpermissions:0.10.2'
implementation 'io.reactivex.rxjava2:rxjava:2.1.16'
public static boolean checkPermission(Context context, String[] permissions) {
PackageManager packageManager = context.getPackageManager();
String packageName = context.getPackageName();
for (String permission : permissions) {
if (PackageManager.PERMISSION_DENIED == packageManager.checkPermission(permission, packageName)) {
Log.w(TAG, "required permission not granted . permission = " + permission);
return false;
}
}
return true;
}
RxPermissions.getInstance(MainActivity.this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)//这里填写所需要的权限
.subscribe(new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
if (aBoolean) {//true表示获取权限成功(注意这里在android6.0以下默认为true)
Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 获取成功);
// 打开相册
} else {
Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 获取失败);
}
}
});
RxPermissions.getInstance(MainActivity.this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA)//这里填写所需要的权限
.subscribe(new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
if (aBoolean) {//true表示获取权限成功(注意这里在android6.0以下默认为true)
Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 获取成功);
// 打开摄像头
} else {
Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 获取失败);
}
}
});
六、打开系统相册
public static void openSysAlbum(@NonNull FragmentActivity activity) {
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
activity.startActivityForResult(albumIntent, RequestCode.ALBUM_RESULT_CODE);
}
七、调起系统相机
private File captureTempFile; // 拍照保存的图片
captureTempFile = FileUtil.createTempImageFile(MainActivity.this);
if (captureTempFile == null) return;
openSysCamera(MainActivity.this, captureTempFile);
public static void openSysCamera(@NonNull FragmentActivity activity, @NonNull File tempFile) {
Uri captureTempUri = FileUtil.fileToUri(activity, tempFile);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, captureTempUri);
activity.startActivityForResult(cameraIntent, RequestCode.CAMERA_RESULT_CODE);
}
八、裁剪图片
public static void cropPic(@NonNull FragmentActivity activity, @NonNull File originFile, @NonNull File outputFile, CropImageParams params) {
Uri originUri = FileUtil.fileToUri(activity, originFile);
cropPic(activity, originUri, outputFile, params);
}
public static void cropPic(@NonNull FragmentActivity activity, @NonNull Uri originUri, @NonNull File outputFile, CropImageParams params) {
Uri outputUri = FileUtil.fileToUri(activity, outputFile);
// 系统裁剪 intent
Intent cropIntent = new Intent("com.android.camera.action.CROP");
cropIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
cropIntent.setDataAndType(originUri, "image/*");
// 开启裁剪:打开的Intent所显示的View可裁剪
cropIntent.putExtra("crop", "true");
// 裁剪宽高比
cropIntent.putExtra("aspectX", params.getAspectX());
cropIntent.putExtra("aspectY", params.getAspectY());
// 裁剪输出大小
cropIntent.putExtra("outputX", params.getOutputX());
cropIntent.putExtra("outputY", params.getOutputY());
cropIntent.putExtra("scale", params.isScale());
// 为 true 时通过 intent 返回 bitmap,传输效率较低,有机型不支持。所以设置为 false,将图片保存到本地对应的uri
cropIntent.putExtra("return-data", false);
// 设置裁剪后图片保存的位置
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
// 保存的图片输出格式
cropIntent.putExtra("outputFormat", params.getOutputFormat().toString());
// 不启动系统拍照时的人脸识别
cropIntent.putExtra("noFaceDetection", params.isNoFaceDetection());
// 授权
FileUtil.grantUriPermission(activity, cropIntent, outputUri);
// 启动系统裁剪
activity.startActivityForResult(cropIntent, RequestCode.CROP_RESULT_CODE);
}
九、执行结果
private File cropTempFile = null; // 剪切后的图片文件
private String imageFilePath; // 剪切后的图片地址,用于上传
private CropImageParams cropImageParams; // 剪切图片的参数
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (resultCode != RESULT_OK) {
super.onActivityResult(requestCode, resultCode, data);
return;
}
switch (requestCode) {
case RequestCode.CAMERA_RESULT_CODE:
cropTempFile = FileUtil.createTempImageFile(this);
if (cropTempFile == null) break;
try {
ImageUtil.cropPic(MainActivity.this, captureTempFile, cropTempFile, cropImageParams);
} catch (Exception e) {
Log.e("MAIN", "拍照失败", e);
}
break;
case RequestCode.CROP_RESULT_CODE:
try {
imageFilePath = cropTempFile.getAbsolutePath();
Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);
avatarView.setImageBitmap(bitmap);
} catch (Exception e) {
Log.e("MAIN", "解析图片失败", e);
}
break;
case RequestCode.ALBUM_RESULT_CODE:
// 相册
if (data == null) break;
if (data.getData() == null) break;
cropTempFile = FileUtil.createTempImageFile(this);
if (cropTempFile == null) break;
try {
ImageUtil.cropPic(MainActivity.this, data.getData(), cropTempFile, cropImageParams);
} catch (Exception e) {
Log.e("MAIN", "选择图片失败", e);
}
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
public class CropImageParams {
private int aspectX;
private int aspectY;
private int outputX;
private int outputY;
private boolean scale;
private Bitmap.CompressFormat outputFormat;
private boolean noFaceDetection;
public int getAspectX() {
return aspectX;
}
public void setAspectX(int aspectX) {
this.aspectX = aspectX;
}
public int getAspectY() {
return aspectY;
}
public void setAspectY(int aspectY) {
this.aspectY = aspectY;
}
public int getOutputX() {
return outputX;
}
public void setOutputX(int outputX) {
this.outputX = outputX;
}
public int getOutputY() {
return outputY;
}
public void setOutputY(int outputY) {
this.outputY = outputY;
}
public boolean isScale() {
return scale;
}
public void setScale(boolean scale) {
this.scale = scale;
}
public Bitmap.CompressFormat getOutputFormat() {
return outputFormat;
}
public void setOutputFormat(Bitmap.CompressFormat outputFormat) {
this.outputFormat = outputFormat;
}
public boolean isNoFaceDetection() {
return noFaceDetection;
}
public void setNoFaceDetection(boolean noFaceDetection) {
this.noFaceDetection = noFaceDetection;
}
}
cropImageParams = new CropImageParams();
cropImageParams.setAspectX(1);
cropImageParams.setAspectY(1);
cropImageParams.setOutputX(320);
cropImageParams.setOutputY(320);
cropImageParams.setScale(true);
cropImageParams.setOutputFormat(Bitmap.CompressFormat.JPEG);
cropImageParams.setNoFaceDetection(false);
十、上传图片
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
public void uploadImage(String url, final Type type, String imagePath, final NetCallBack netCallBack) {
File file = new File(imagePath);
if (!file.exists()) {
onFail(netCallBack, new Exception("文件不存在:" + imagePath));
return;
}
RequestBody requestBody = RequestBody.create(MediaType.parse("image/jpeg"), file);
RequestBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)//设置文件上传类型
.addFormDataPart("file", file.getName(), requestBody)//包含文件名字和内容
.build();
Token token = TokenUtil.getToken();
String tokenStr = "";
if (token != null) tokenStr = token.getToken();
Request request = new Request.Builder()
.url(url)
.addHeader("Authorization", tokenStr)
.post(body)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull final IOException e) {
if (netCallBack != null) onFail(netCallBack, e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (!response.isSuccessful() || response.body() == null) {
Log.e(TAG, "onResponse error: " + response);
if (netCallBack != null) onFail(netCallBack, new Exception("请求失败"));
return;
}
String result = response.body().string();
Log.e(TAG, "onResponse: " + result);
Gson gson = new Gson();
try {
final Object o = gson.fromJson(result, type);
if (netCallBack != null) onSuccess(netCallBack, o);
} catch (Exception e) {
if (netCallBack != null) onFail(netCallBack, e);
}
}
});
}
public interface NetCallBack {
void onFailure(Exception e);
void onSuccess(Object o);
}