Android 調用系統相機、獲取圖片適配方案及常見問題彙總

背景:在維護公司移動辦公平臺APP 時,由於項目太過老舊,在兼容到最新版本時,出現了拍照、選擇圖片等問題,在此記錄一下遇到的問題及解決的方案。

調用系統相機拍照方案:

Tips:記得申請權限和做運行時權限處理

   btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //在應用關聯緩存目錄中新建 output_image.jpg 圖片文件,因爲Android6.0之後,讀取SDCard被列爲危險權限,如將圖片存放在
                //SD卡的任何其他目錄,均需進行運行時權限處理,而使用應用關聯目錄則可跳過該步驟,同時應用卸載會刪除該目錄
                //具體路勁:/sdcard/Android/data/<pacaage name>/cache
               
                //創建File 對象,用於存儲拍攝的照片,存儲在手機SD 卡的應用關聯緩存目錄下
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                try {
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                //系統版本判斷
                if (Build.VERSION.SDK_INT >= 24) {
                    //系統版本高於 Android7.0,者調用 FileProvider.getUriForFile() 方法,將file對象轉換爲一個封裝過的Uri對象,
                    // 參數一:Context 參數二:任意唯一的字符串 參數三:file對象
                    imageUri = FileProvider.getUriForFile(MainActivity.this, "com.cloudc.notificationtest.provider", outputImage);
                } else {
                    //系統版本低於 Android7.0,就調用Uri.fromFile()方法,將file對象轉換爲Uri對象,該Uri對象標誌着 output_image.jpg 圖片
                    //的真實路勁,1
                    imageUri = Uri.fromFile(outputImage);
                }
                //啓動相機程序 隱式調用系統相機
                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                //添加這一句表示對目標應用臨時授權該Uri所代表的文件
		        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                //指定圖片的輸出地址
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                startActivityForResult(intent, TAKE_PHOTO);
            }
        });

因爲從 Android7.0 開始,直接使用本地真實路勁的 Uri 被認爲是不安全的,會拋出一個FileUriExposedException 異常,而FileProvider則是一種特殊的內容提供器來對數據進行保護,可以選擇行的將封裝過的 Uri 共享給外部,從而提高了應用的安全性。

注意:在Android 4.4 之前,訪問SD 卡的關聯目錄也是需要聲明權限的,從4.4 系統開始不在需要權限聲明

xml / file_path.xml文件:在res/xml 文件夾下
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--external-path 用來指定 Uri 共享,
    name 屬性可隨便填寫,
    path 屬性值表示共享的具體路徑,爲空表示將整個SD 卡共享
    path:你所共享的子目錄。雖然name屬性是一個URI路徑片段,但是path是一個真實的子目錄名。
    注意,path是一個子目錄,而不是單個文件或者多個文件。
    -->
    <external-path name="my_images"
                   path="."/>
</paths>

Manifest.xml文件

  ....
        <!--name 屬性固定
            authorities 屬性必須和代碼中 FileProvider.getUriForFile()中的第二個參數一致
        -->

        <!--meta-data 中指定了Uri 共享路徑,並引用了一個xml文件資源-->
        <provider android:name="android.support.v4.content.FileProvider"
                  android:authorities="com.example.yhadmin.gank.test.Test2Activity.fileprovider"
                  android:exported="false"
                  android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths">
            </meta-data>
        </provider>
        ....

獲取圖片兼容方案:

前提得獲取SDCard 讀寫權限:

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

部分代碼


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {         
            case CHOOSE_PHOTO: {
                if (resultCode == RESULT_OK) {
                    //判斷手機系統版本號
                    if (Build.VERSION.SDK_INT >= 19) {
                        //4.4及以上系統使用此方法處理圖片
                        handleImageOnKitKat(data);
                    } else {
                        //4.4以下系統使用此方法處理圖像
                        handleImageBeforeKitKat(data);
                    }
                }
                break;
            }
            default:
                break;
        }
    }
    //4.4以下系統使用此方法處理圖像
    private void handleImageBeforeKitKat(Intent data) {
        Uri uri = data.getData();
        String imagePath = getImagePath(uri, null);
        displayImage(imagePath);
    }

    @TargetApi(19)//4.4及以上系統使用此方法處理圖片
    private void handleImageOnKitKat(Intent data) {
        String imagePath = null;
        Uri    uri       = data.getData();

        Logger.t("test2Activity").d("URI:"+uri.toString());

        if (DocumentsContract.isDocumentUri(this, uri)) {
            //如果是 document 類型的 Uri,則通過 document id 處理
            String documentId = DocumentsContract.getDocumentId(uri);

            Logger.t("test2Activity").d("documentId:"+uri.toString());

            if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
                //解析出數字格式的 id
                String id        = documentId.split(":")[1];
                String selection = MediaStore.Images.Media._ID + "=" + id;
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
            }

        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            //如果是 content 類型的 Uri,則使用普通方式處理

            Logger.t("test2Activity").d("content:"+uri.getScheme());

            imagePath = getImagePath(uri, null);

        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
            //如果是 Fiel 類型的 Uri,直接獲取圖片路徑即可
            Logger.t("test2Activity").d("uri.getScheme():"+uri.getScheme());
            imagePath = uri.getPath();
        }

        displayImage(imagePath);//加載圖片並顯示
    }
    //加載圖片
    private void displayImage(String imagePath) {
        if (imagePath!=null){
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            bindingView.picture.setImageBitmap(bitmap);
        }else {
            ToastHelper.show(Test2Activity.this,"獲取圖片失敗!");
        }
    }
    //獲取圖片路徑
    private String getImagePath(Uri uri, String selection) {
        String path   = null;
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {

                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }


關於調用系統照相機不執行OnActivityResult的解決方式

1、確認權限是否添加

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
2、Activity 的加載模式
這與 Activity 的加載模式(launchMode)有關,該屬性可以在 AndroidManifest.xml 中設置。
原先將其設爲 singleInstance,經測試,所有需要傳遞或接收的 Activity 不允許設置該屬性,或只能設爲標準模式,否則系統將在 startActivityForResult() 後直接調用 onActivityResult()。

3、調用startActivityForResult的參數問題,
即調用時這樣:startActivityForResult(intent, 0);是第二個參數的問題,該參數必須大於等於0才能在返回值,並激活onActivityResult方法。
我最開始是用的一個activity默認的常量:RESULT_OK,跟蹤了代碼後發現,該常量的值爲-1,當然沒法激活 onActivityResult方法了,隨後隨便修改爲一個大於0的整數,程序即通跑成功。
startActivityForResult(intent, 1); //這樣就行了

API描述:
 /**
     * Same as calling {@link #startActivityForResult(Intent, int, Bundle)}
     * with no options.
     *
     * @param intent The intent to start.
     * @param requestCode If >= 0, this code will be returned in
     *                    onActivityResult() when the activity exits.
     *
     * @throws android.content.ActivityNotFoundException
     *
     * @see #startActivity
     */
    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
        startActivityForResult(intent, requestCode, null);
    }
4、調用相機時,未創建保存圖片的文件夾



問題:“調用小米4手機拍照,不能點擊打勾,就算點擊了也沒有反應。但是同樣的代碼也小米2,2S上沒有問題,3上偶爾會出現。”
原因:
  Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
  intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(filePath, TEMP_IMAGE_NAME)));
  startActivityForResult(intent, 1);
如果此處Uri.fromFile(new File(filePath, TEMP_IMAGE_NAME),圖片的地址有問題就不能點擊確認。
這個問題是小米系統的原因,貌似DCIM和Picture這兩個文件的權限不一樣。
具體參看這兩個文件路徑:
1、Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
2、 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
5. 屏幕橫向縱向切換的問題。 切換後。上一頁 destory了。然後重新oncreate。然後又進入拍照了,在activity的xml裏面加上下面這句話即可:

android:configChanges="orientation|keyboardHidden"
android:screenOrientation="portrait"
6.輸出的文件名無特殊字符
輸出了之後才發現是特殊字符造成的,爲了省事我直接取系統當前時間 yyyy-MM-dd HH:mm:ss z作爲圖片名稱,初看似乎沒什麼影響,但是保存之後轉化爲uri就會出現%之類的特殊字符。然後悲劇就發生了,其實只要避免名稱中出現  "-" ":" 諸如此類 的字符一般上都是可以正常保存返回的。

7.手機系統因內存不足而kill 了前 Activity或拍照完成後應用閃退

這個現象相對少見,目前在廉價的低端手機上出現過。啓動拍照時正常,當在拍照界面點擊確認按鈕,拍照界面消失返回我們的應用時,直接閃退。更讓人崩潰的是,Logcat裏面沒有相應的日誌信息。無奈只能藉助搜索,發現原來是手機廠商對系統做了修改(爲了在低端硬件上能夠運行Android系統...),當我們的應用程序的Activity啓動拍照,進入系統相機時,我們的Activity被銷燬了。這個情況在測試時也不是必現。測試機在使用一段時間後很容易出現,但如果將設備重啓後開啓我們的應用來拍照又沒有問題(這是我們遇到的情況,可能不是必現規則)。

Activity被回收時保存數據,可以在onSaveInstanceState()生命週期方法中處理,將圖片路徑保存到Bundle中。
@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putString("file_path", mFilePath);
    super.onSaveInstanceState(outState);
}
然後在onCreate()方法裏,判斷savedInstanceState不爲空時,取出圖片路徑的值。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    if (savedInstanceState != null) {
        mFilePath = savedInstanceState.getString("file_path");
    // your code here
    }
}
網上看到另外一種恢復的方法。Activity沒有重新創建,而是成員變量被回收了,當拍照返回時,在onActivityResult()方法中mFilePath爲空。解決方法是從onRestoreInstanceState()方法恢復數據。這種情況暫時沒遇到,做個記錄。
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    if (TextUtils.isEmpty(mFilePath)) {
        mFilePath = savedInstanceState.getString("file_path");
    }
    super.onRestoreInstanceState(savedInstanceState);
}
8.拍攝的照片被旋轉了

明明在拍攝時照片是正的,進入裁剪頁面發現照片竟然是被旋轉的。這個現象在不同的設備上,表現會不一樣,可能正常也可能被旋轉,而且旋轉的角度也不同。要解決這個問題,需要首選獲取照片的旋轉角度,再反轉回去就可以了。

ExifInterface接口提供了多媒體文件比如JPG格式圖片的一些附加信息,比如文件的旋轉,gps,拍攝時間等。如下代碼展示了使用ExifInterface獲取圖片的旋轉角度。
/**
* 獲取圖片的旋轉角度
* @param path 圖片的絕對路徑
*/
private int getBitmapDegree(String imagePath) {
    int degree = 0;
    try {
        // 從指定路徑下讀取圖片,並獲取其EXIF信息
        ExifInterface exifInterface = new ExifInterface(imagePath);
        // 獲取圖片的旋轉信息
        int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
            ExifInterface.ORIENTATION_NORMAL);
        switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_90:
                degree = 90;
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                degree = 180;
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                degree = 270;
                break;
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return degree;
}

進行圖形變換如旋轉、縮放、移動的操作,可以使用Matrix類來完成。如下代碼展示了使用Matrix對圖片旋轉,生成新的Bitmap。

/**
* 將圖片按照某個角度進行旋轉
* @param bitmap 需要旋轉的圖片
* @param degree 旋轉角度
*/
public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) {
    Bitmap result = null;
    // 根據旋轉角度,生成旋轉矩陣
    Matrix matrix = new Matrix();
    matrix.postRotate(degree);
    try {
        // 將原始圖片按照旋轉矩陣進行旋轉,並得到新的圖片
        result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    } catch (Exception e) {
    }
    if (bitmap != null && !bitmap.isRecycled()) {
        bitmap.recycle();
    bitmap = null;
    }
    return result;
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章