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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349