Android優(yōu)雅地拍照或選擇圖片后裁剪并上傳服務(wù)器

選擇圖片并上傳是前端常會(huì)面臨的需求,6.0以后需要?jiǎng)討B(tài)權(quán)限適配,7.0 以后由于應(yīng)用間共享文件的限制則需要授予URI臨時(shí)訪問(wèn)權(quán)限。

一、權(quán)限

1、相冊(cè)需要讀取存儲(chǔ)卡的權(quán)限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

2、拍照需要寫入存儲(chǔ)卡的權(quán)限以及攝像頭的權(quán)限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>

二、應(yīng)用間文件共享

授予URI臨時(shí)訪問(wèn)權(quán)限,最簡(jiǎn)單方式是使用 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 文件夾下創(chuàng)建 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="" /> //代表設(shè)備的根目錄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:屬性是用來(lái)隱藏具體子目錄用的,即name會(huì)出現(xiàn)在URI中,并可對(duì)應(yīng)到實(shí)際的path子目錄。
  • path:屬性是實(shí)際的子目錄

三、文件工具類

  • 創(chuàng)建臨時(shí)圖片文件
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", "創(chuàng)建臨時(shí)圖片失敗", 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授權(quán)
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);
    }
}

五、獲取權(quán)限

我們一般會(huì)在應(yīng)用首次啟動(dòng)時(shí),展示所需要的權(quán)限,及權(quán)限的用途,并由用戶決定要授予的權(quán)限。然后根據(jù)用戶的選擇真正地索要權(quán)限。詳見[Android優(yōu)雅的權(quán)限處理(還未開寫,嘿嘿)]
  • 用到的庫(kù)
implementation 'com.github.tbruyelle:rxpermissions:0.10.2'
implementation 'io.reactivex.rxjava2:rxjava:2.1.16'
  • 檢查是否有權(quán)限
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;
 }
  • 獲取系統(tǒng)相冊(cè)權(quán)限
RxPermissions.getInstance(MainActivity.this)
      .request(Manifest.permission.READ_EXTERNAL_STORAGE)//這里填寫所需要的權(quán)限
      .subscribe(new Action1<Boolean>() {
          @Override
          public void call(Boolean aBoolean) {
              if (aBoolean) {//true表示獲取權(quán)限成功(注意這里在android6.0以下默認(rèn)為true)
                  Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 獲取成功);
                  // 打開相冊(cè)
              } else {
                  Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 獲取失敗);
              }
           }
        });
  • 獲取拍照權(quán)限
RxPermissions.getInstance(MainActivity.this)
      .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, 
               Manifest.permission.CAMERA)//這里填寫所需要的權(quán)限
      .subscribe(new Action1<Boolean>() {
          @Override
          public void call(Boolean aBoolean) {
              if (aBoolean) {//true表示獲取權(quán)限成功(注意這里在android6.0以下默認(rèn)為true)
                  Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 獲取成功);
                  // 打開攝像頭
              } else {
                  Log.i("permissions", Manifest.permission.READ_EXTERNAL_STORAGE + ":" + 獲取失敗);
              }
           }
        });

六、打開系統(tǒng)相冊(cè)

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);
}

七、調(diào)起系統(tǒng)相機(jī)

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);
    // 系統(tǒng)裁剪 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 時(shí)通過(guò) intent 返回 bitmap,傳輸效率較低,有機(jī)型不支持。所以設(shè)置為 false,將圖片保存到本地對(duì)應(yīng)的uri
    cropIntent.putExtra("return-data", false);
    // 設(shè)置裁剪后圖片保存的位置
    cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
    // 保存的圖片輸出格式
    cropIntent.putExtra("outputFormat", params.getOutputFormat().toString());
    // 不啟動(dòng)系統(tǒng)拍照時(shí)的人臉識(shí)別
    cropIntent.putExtra("noFaceDetection", params.isNoFaceDetection());
    // 授權(quán)
    FileUtil.grantUriPermission(activity, cropIntent, outputUri);
    // 啟動(dòng)系統(tǒng)裁剪
    activity.startActivityForResult(cropIntent, RequestCode.CROP_RESULT_CODE);
}

九、執(zhí)行結(jié)果

  • onActivityResult
private File cropTempFile = null; // 剪切后的圖片文件
private String imageFilePath; // 剪切后的圖片地址,用于上傳
private CropImageParams cropImageParams; // 剪切圖片的參數(shù)

@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:
            // 相冊(cè)
            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);

十、上傳圖片

  • 用到的庫(kù)
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)//設(shè)置文件上傳類型
            .addFormDataPart("file", file.getName(), requestBody)//包含文件名字和內(nèi)容
            .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("請(qǐng)求失敗"));
                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);
            }
        }
    });
}
  • 回調(diào)接口
public interface NetCallBack {
    void onFailure(Exception e);

    void onSuccess(Object o);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容