1. 程式人生 > >Android 拍照及相簿選取圖片功能,已適配Android6.0、7.0、8.0

Android 拍照及相簿選取圖片功能,已適配Android6.0、7.0、8.0

更換頭像或者上傳圖片功能已基本是每個 APP 所具備的基礎功能了,但這對於開發者來說是一個很麻煩的事情,除機型之外,適配版本就至少要考慮這幾種情況(6.0以下版本、6.0的動態許可權、7.0的FileProvider、8.0的特殊情況)。

今天來個總結,方便自己也方便同行。

功能說明

本文的示例以下圖為準:
選擇圖片

介面只有一個 ImageView,點選 ImageView 彈出 DialogFragment,分別是拍照和相簿選擇圖片功能,其中都帶有系統的裁剪功能,將裁剪後的圖片顯示在 ImageView 上。如果不需要裁剪功能,只需要將程式碼中的裁剪方法註釋掉即可。

6.0 以下版本

1. 許可權

<uses-feature android:name="android.hardware.camera" />
    <!--相機許可權-->
    <uses-permission android:name="android.permission.CAMERA" />
    <!--寫入SD卡的許可權:如果你希望儲存相機拍照後的照片-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--讀取SD卡的許可權:開啟相簿選取圖片所必須的許可權-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2. 拍照程式碼

/**
     * 開啟系統相機
     */
    private void openSysCamera() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
                new
File(Environment.getExternalStorageDirectory(), imgName))); startActivityForResult(cameraIntent, CAMERA_RESULT_CODE); }

其中 imgName 是拍照圖片的名字,一般以時間戳再加上自定義字串命名;CAMERA_RESULT_CODE 是自定義的一個常量,作為拍照的請求碼。

onActivityResult 處理拍照後的圖片

case CAMERA_RESULT_CODE:
                tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
                cropPic(Uri.fromFile(tempFile));
                break;

將拍照後的圖片建立成一個 File 物件,用來裁剪,裁剪的功能程式碼是和相簿選取圖片通用的。

裁剪程式碼

/**
     * 裁剪圖片
     *
     * @param data
     */
    private void cropPic(Uri data) {
        if (data == null) {
            return;
        }
        Intent cropIntent = new Intent("com.android.camera.action.CROP");
        cropIntent.setDataAndType(data, "image/*");

        // 開啟裁剪:開啟的Intent所顯示的View可裁剪
        cropIntent.putExtra("crop", "true");
        // 裁剪寬高比
        cropIntent.putExtra("aspectX", 1);
        cropIntent.putExtra("aspectY", 1);
        // 裁剪輸出大小
        cropIntent.putExtra("outputX", 320);
        cropIntent.putExtra("outputY", 320);
        cropIntent.putExtra("scale", true);
        /**
         * return-data
         * 這個屬性決定我們在 onActivityResult 中接收到的是什麼資料,
         * 如果設定為true 那麼data將會返回一個bitmap
         * 如果設定為false,則會將圖片儲存到本地並將對應的uri返回,當然這個uri得有我們自己設定。
         * 系統裁剪完成後將會將裁剪完成的圖片儲存在我們所這設定這個uri地址上。我們只需要在裁剪完成後直接呼叫該uri來設定圖片,就可以了。
         */
        cropIntent.putExtra("return-data", true);
        // 當 return-data 為 false 的時候需要設定這句
//        cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        // 圖片輸出格式
//        cropIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        // 頭像識別 會啟動系統的拍照時人臉識別
//        cropIntent.putExtra("noFaceDetection", true);
        startActivityForResult(cropIntent, CROP_RESULT_CODE);
    }

其中的 CROP_RESULT_CODE 也是定義的靜態常量,同拍照時定義的常量相同的作用。

如果裁剪時將 return-data 設定為 false,那麼需要定義一個 Uri 來儲存裁剪後的圖片路徑。

 // 裁剪屬性 cropIntent.putExtra("return-data", false); 時,使用自定義接收圖片的Uri
    private static final String IMAGE_FILE_LOCATION = "file:///" + Environment.getExternalStorageDirectory().getPath() + "/temp.jpg";
    private Uri imageUri = Uri.parse(IMAGE_FILE_LOCATION);

圖片裁剪完成回撥

 case CROP_RESULT_CODE:
                // 裁剪時,這樣設定 cropIntent.putExtra("return-data", true); 處理方案如下
                if (data != null) {
                    Bundle bundle = data.getExtras();
                    if (bundle != null) {
                        Bitmap bitmap = bundle.getParcelable("data");
                        imageView.setImageBitmap(bitmap);
                        // 把裁剪後的圖片儲存至本地 返回路徑
                        String urlpath = FileUtilcll.saveFile(this, "crop.jpg", bitmap);
                        L.e("裁剪圖片地址->" + urlpath);
                    }
                }

                // 裁剪時,這樣設定 cropIntent.putExtra("return-data", false); 處理方案如下
//                try {
//                    ivHead.setImageBitmap(BitmapFactory.decodeStream(
// getActivity().getContentResolver().openInputStream(imageUri)));
//                } catch (FileNotFoundException e) {
//                    e.printStackTrace();
//                }
                break;

FileUtilcll 程式碼

/**
 * 圖片檔案操作
 */
public class FileUtilcll {

    /**
     * 將Bitmap 圖片儲存到本地路徑,並返回路徑
     *
     * @param fileName 檔名稱
     * @param bitmap   圖片
     * @param資源型別,參照 MultimediaContentType 列舉,根據此型別,儲存時可自動歸類
     */
    public static String saveFile(Context c, String fileName, Bitmap bitmap) {
        return saveFile(c, "", fileName, bitmap);
    }

    public static String saveFile(Context c, String filePath, String fileName, Bitmap bitmap) {
        byte[] bytes = bitmapToBytes(bitmap);
        return saveFile(c, filePath, fileName, bytes);
    }

    /**
     * Bitmap 轉 位元組陣列
     *
     * @param bm
     * @return
     */
    public static byte[] bitmapToBytes(Bitmap bm) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bm.compress(CompressFormat.JPEG, 100, baos);
        return baos.toByteArray();
    }

    public static String saveFile(Context c, String filePath, String fileName, byte[] bytes) {
        String fileFullName = "";
        FileOutputStream fos = null;
        String dateFolder = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA)
                .format(new Date());
        try {
            String suffix = "";
            if (filePath == null || filePath.trim().length() == 0) {
                filePath = Environment.getExternalStorageDirectory() + "/cxs/" + dateFolder + "/";
            }
            File file = new File(filePath);
            if (!file.exists()) {
                file.mkdirs();
            }
            File fullFile = new File(filePath, fileName + suffix);
            fileFullName = fullFile.getPath();
            fos = new FileOutputStream(new File(filePath, fileName + suffix));
            fos.write(bytes);
        } catch (Exception e) {
            fileFullName = "";
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    fileFullName = "";
                }
            }
        }
        return fileFullName;
    }

}

到此,拍照功能就完成了,附帶了裁剪。剩下的就是相簿選取照片,這個也不難,固定程式碼,其他功能和拍照時相同的。

 /**
     * 開啟系統相簿
     */
    private void openSysAlbum() {
        Intent albumIntent = new Intent(Intent.ACTION_PICK);
        albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
        startActivityForResult(albumIntent, ALBUM_RESULT_CODE);
    }

其中的 ALBUM_RESULT_CODE 同樣是定義的常量。用來在 onActivityResult 中處理返回結果。

case ALBUM_RESULT_CODE:
                // 相簿
                cropPic(data.getData());
                break;

和拍照公用裁剪程式碼,這裡直接呼叫即可,其他邏輯不變。


6.0動態許可權適配

除了上述功能保持不變之外,需要在程式碼中動態申請檔案讀寫許可權和相機許可權。這裡我將申請的時機寫在了剛進入頁面,而不是剛開啟 APP,也不是點選按鈕的時候。

邏輯

  • 如果使用者點選了拒絕,但沒有點選“不再詢問”,這個時候再次進入介面繼續彈框;
  • 如果使用者點選了拒絕,且選擇了“不再詢問”,那麼再次進入此介面將會彈框提示開啟 APP 的詳情介面,手動開啟對應許可權。

許可權申請程式碼

/**
     * 初始化相機相關許可權
     * 適配6.0+手機的執行時許可權
     */
    private void initPermission() {
        String[] permissions = new String[]{Manifest.permission.CAMERA,
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE};
        //檢查許可權
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            // 之前拒絕了許可權,但沒有點選 不再詢問 這個時候讓它繼續請求許可權
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.CAMERA)) {
                Toast.makeText(this, "使用者曾拒絕開啟相機許可權", Toast.LENGTH_SHORT).show();
                ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
            } else {
                //註冊相機許可權
                ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
            }
        }
    }

這裡我將需要的許可權統一寫在了一個數組裡面。其中的 REQUEST_PERMISSIONS 和上面拍照定義的常量功能相同。

許可權申請回調

@Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_PERMISSIONS:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    //成功
                    Toast.makeText(this, "使用者授權相機許可權", Toast.LENGTH_SHORT).show();
                } else {
                    // 勾選了不再詢問
                    Toast.makeText(this, "使用者拒絕相機許可權", Toast.LENGTH_SHORT).show();
                    /**
                     * 跳轉到 APP 詳情的許可權設定頁
                     *
                     * 可根據自己的需求定製對話方塊,點選某個按鈕在執行下面的程式碼
                     */
                    Intent intent = Util.getAppDetailSettingIntent(PhotoFromSysActivity.this);
                    startActivity(intent);
                }
                break;
        }
    }

其中的 getAppDetailSettingIntent()方法程式碼如下:

/**
     * 獲取 APP 詳情頁面intent
     *
     * @return
     */
    public static Intent getAppDetailSettingIntent(Context context) {
        Intent localIntent = new Intent();
        localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if (Build.VERSION.SDK_INT >= 9) {
            localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
            localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
        } else if (Build.VERSION.SDK_INT <= 8) {
            localIntent.setAction(Intent.ACTION_VIEW);
            localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
            localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
        }
        return localIntent;
    }

到這,許可權這個適配問題就解決了,接下來該是 Android 7.0 的適配了,這裡我們只需要修改拍照的功能即可,相簿是沒有問題的。


Android7.0 適配

Android 7.0 就是 File 路徑的變更,需要使用 FileProvider 來做,下面看拍照的程式碼。

拍照程式碼修改

/**
     * 開啟系統相機
     */
    private void openSysCamera() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
//                new File(Environment.getExternalStorageDirectory(), imgName)));
//        File file = new File(Environment.getExternalStorageDirectory(), imgName);
        try {
            file = createOriImageFile();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (file != null) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                imgUriOri = Uri.fromFile(file);
            } else {
                imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", file);
            }
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriOri);
            startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
        }
    }

File 物件的建立和 拍照圖片的 Uri 物件建立方式更改。建立原影象儲存的程式碼如下:

/**
     * 建立原影象儲存的檔案
     *
     * @return
     * @throws IOException
     */
    private File createOriImageFile() throws IOException {
        String imgNameOri = "HomePic_" + new SimpleDateFormat(
                "yyyyMMdd_HHmmss").format(new Date());
        File pictureDirOri = new File(getExternalFilesDir(
                Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/OriPicture");
        if (!pictureDirOri.exists()) {
            pictureDirOri.mkdirs();
        }
        File image = File.createTempFile(
                imgNameOri,         /* prefix */
                ".jpg",             /* suffix */
                pictureDirOri       /* directory */
        );
        imgPathOri = image.getAbsolutePath();
        return image;
    }

拍照回撥程式碼修改

 case CAMERA_RESULT_CODE:
//                tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
//                cropPic(Uri.fromFile(tempFile));

                // 適配 Android7.0+
                cropPic(getImageContentUri(file));
                break;

getImageContentUri() 程式碼如下:

/**
     * 7.0以上獲取裁剪 Uri
     *
     * @param imageFile
     * @return
     */
    private Uri getImageContentUri(File imageFile) {
        String filePath = imageFile.getAbsolutePath();
        Cursor cursor = getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID},
                MediaStore.Images.Media.DATA + "=? ",
                new String[]{filePath}, null);

        if (cursor != null && cursor.moveToFirst()) {
            int id = cursor.getInt(cursor
                    .getColumnIndex(MediaStore.MediaColumns._ID));
            Uri baseUri = Uri.parse("content://media/external/images/media");
            return Uri.withAppendedPath(baseUri, "" + id);
        } else {
            if (imageFile.exists()) {
                ContentValues values = new ContentValues();
                values.put(MediaStore.Images.Media.DATA, filePath);
                return getContentResolver().insert(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            } else {
                return null;
            }
        }
    }

程式碼適配解決完了,還有一個 FileProvider 問題需要做如下配置。

  1. 在 res 目錄下建立一個名為 xml 的資料夾,並在其下建立一個名為 file_paths.xml 檔案,其內容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path
        name="images"
        path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
    <external-path
        name="images"
        path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
    <external-files-path
        name="images"
        path="files/Pictures/OriPicture" />
    <root-path
        name="images"
        path="" />
    <root-path
        name="images"
        path="" />
</paths>
2. 在 AndroidMainfest.xml 中的 application 節點下做如下配置:
<!--FileProvider共享檔案、快取-->
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.cxs.yukumenu.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

其中這裡的 android:authorities 屬性值要和程式碼中 FileProvider.getUriForFile()的第二個引數值保持一致。

注意

  • 程式碼已全部給出,貼上即可使用
  • 本次測試真機:華為5.1.1,一加8.0.0

總結

  1. 機型適配是個細活,但能鍛鍊排查問題和解決問題能力。
  2. 本文的程式碼有很多是可以抽取的,如果是在專案中使用,那麼建議提取出特定功能的程式碼。

相關推薦

Android 拍照相簿選取圖片功能Android6.07.08.0

更換頭像或者上傳圖片功能已基本是每個 APP 所具備的基礎功能了,但這對於開發者來說是一個很麻煩的事情,除機型之外,適配版本就至少要考慮這幾種情況(6.0以下版本、6.0的動態許可權、7.0的FileProvider、8.0的特殊情況)。 今天來個總

Android手機拍照或從本地相簿選取圖片設定頭像。小米華為7.0

1,讓使用者通過選擇本地相簿之類的圖片庫中已有的影象,裁剪後作為頭像。 2,讓使用者啟動手機的相機拍照,拍完照片後裁剪,然後作為頭像。 程式碼如下 MainActivity.Java檔案: package portrait.bala.port

Android拍照相簿+系統裁剪功能返回圖片

最近在使用一加3手機,Android系統6.0,進行測試的時候,發現呼叫手機的拍照和相簿選擇圖片的功能返回的時候都無法呼叫系統的裁剪功能,Log日誌也沒有輸出有用的資訊。經過在網上大量的查詢資料,拍照的問題解決了,但是用網上提供的方法都無法進行相簿選擇圖片後進行裁剪。 測試

Android拍照相簿選取圖片並裁剪得到路徑

-----------------轉載請註明出處:http://blog.csdn.net/android_cll 一:先來兩張效果圖: 1.這是我們專案用到這個功能的截圖、 2.demo的截圖,功能和上面截圖功能一樣,懶得截圖、 二:實現步驟: 1.xml的實現

Android拍照相簿選擇圖片上傳所遇到的一些坑

微信公眾號:IT小頑童 前段時間做拍照和相簿選擇圖片上傳,遇到一些坑,最近閒來沒事,就整理一篇部落格,並寫了個demo,專案中使用純java,這次採用java + kotlin混合,GitHub的地址戳此 1、首先遇到的的是Android7.0的坑 說是坑,有點欲加

小程式 拍照相簿選取圖片 上傳到伺服器

// 上傳圖片function upload(page, path) { wx.showToast({ icon: "loading", title: "正在上傳" }), wx.uploadFile({   url: '圖片伺服器的路徑', filePath: path

Android開發】app升級報錯解析包時出現錯誤(華為手機8.0系統)

問題描述: 今天釋出app版本升級,碰到華為手機8.0系統,安裝升級的時候提示,解析包時出現問題。而其他手機都是正常的。而且當我的包沒有經過360加固的時候,也是可以去升級新版本,加固過後就不行了。 這個導致這個問題的原因有很多,我先把我這邊app的問題處理方案介紹給大家

Android拍照和從相簿選取圖片相容7.0

首先配置一下我們的專案(1)在build.gradle裡新增類庫:compile 'com.squareup.picasso:picasso:2.5.2'(2)2.1、在清單檔案裡面新增許可權<uses-permission android:name="android.

Android生成二維碼--拍照或從相簿選取圖片

private void handleImageOnKitKat(Intent data) { String imagePath = null; Uri uri = data.getData(); if (DocumentsContract.isDocumentUri(this, ur

Android拍照圖片裁剪呼叫系統相簿(相容6.0以上許可權處理7.0以上檔案管理)

前言: 最近工作修改較舊的專案時,涉及到了圖片相關功能 ,在使用安卓6.0手機及7.1手機拍照時,遇到了因許可權及檔案管理導致程式崩潰等問題。 剛好把功能修改完,把程式碼簡單地貼一下,方便以後使用。 本文demo包含以下要點: Android6.0執

Android 7.0 拍照相簿選擇和系統圖片裁剪和刪除

Android 7.0 引入了 Provider 給 app 申請檔案儲存路徑,所以需要配置 Provider ,才可以使用 儲存功能。 定義 provider 在 res/xml 資料夾下定義

Android拍照相簿 獲取圖片裁剪圖片

最近在做的B2B的專案,圖片大部分來源於使用者自己上傳; 由於android尺寸的不一,使用者相機,相簿的圖片也是奇形怪狀; 所以在上傳之前對圖片做一次裁剪是很有必要的! 下面是按比例裁剪圖片的demo 資原始檔activity_main.xml

Android圖片壓縮(包含拍照或從相簿選取圖片,PopupWindow的使用)

目前大部分智慧手機拍照效果好,圖片畫素很高.但是在儲存或傳輸圖片的時候不太方便,需要首先將其進行壓縮. 先看看效果圖. 注意,最後一張圖片顯示的壓縮前後兩張圖似乎沒有太大區別,但事實上壓縮前圖片為1.6MB,壓縮後只有141

Android拍照+擷取+相簿獲取擷取圖片+水印功能【完整】

1  拍照擷取 原理就是通過intent呼叫系統的相機,拍完照後再回調進行操作,成功獲取到拍完照的圖片根據uri呼叫系統的裁剪頁面,裁剪完也是在回撥中進行處理,顯示在頁面的imageview中。 2  相簿選取圖片擷取 原理也是通過intent開啟系統的圖片,使用者選擇

呼叫系統相機拍照相簿選取圖片上傳

前不久在專案中再次遇到了這個問題,就是從系統相簿中選取圖片,呼叫系統的相機拍照並上傳的問題。由於之前比較懶沒能在做完之後對寫的程式碼進行整理儲存,以至於再次遇到的時候還是重新去研究了一下造成了開發過程中的時間浪費。 注意的點:1.呼叫系統的相機拍攝並取得原圖,並對角度進行處

相簿選取圖片插入到EditText中實現圖文混排

1.首先用到了 ImagePicker,上github搜尋仿微信圖片選擇就能找到 2.匯入ImagePicker的包 compile 'com.squareup.picasso:picasso:2.5.2' 3.我是用是 picasso載入圖片,github上imag

android拾遺06——從相簿選取圖片

從相簿選取圖片 從相簿選取圖片的流程是: 使用startActivityForResult進入相簿選取圖片 使用onActivityResult讀取獲取到的uri 從uri中解析出圖片 程式碼: package com.exe.feifei.choos

H5實現拍照相簿圖片上傳

最近在做一個H5的小型電商專案,其中有一個是現金刷卡之後需要上傳憑證圖片的,因此也就需要在H5中實現可以上傳圖片。 我們都知道,input標籤的type為file是可以上傳圖片的,本來想著這麼簡單,有啥難的,可是到後來這樣寫完,看效果的時候,缺發現這個東西它是有相容問題的,IOS和An

android相簿選取圖片在小米手機報錯的解決辦法

在跳轉到系統相簿選取照片的時候,用如下程式碼跳轉: Intent albumIntent = new Intent(Intent.ACTION_PICK, null); /** * 下面這句話,與其它方式寫

利用Android Studio自帶螢幕錄製功能生成GIF圖片

一篇好的博文總是離不開有圖有真相,每次開啟一篇部落格,我們總是第一眼希望看到的不是demo原始碼,而是該demo執行的效果畫面。相信大家肯定和我一樣,總是想先看到結果之後再去深究其原始碼。本文將介紹如何使用Android studio自帶螢幕錄製功能生成一個APP執行時的G