Android优雅地拍照或选择图片后裁剪并上传服务器

选择图片并上传是前端常会面临的需求,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;
    }
}
  • 获取文件的URI
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);
}
  • 给URI授权
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);
}

九、执行结果

  • onActivityResult
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);
 }
  • CropImageParams 类
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 初始化
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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。