Android 調用系統功能實現圖片選擇器,你可能會遇到的問題匯總

圖片選擇器在手機應用中屢見不鮮,設置頭像、聊天傳圖等常見類似場景都需要使用。為了保持不同設備上體驗的一致性和較好的兼容性,比較穩妥的做法是在應用內自實現相機拍照、相冊選圖和圖片裁剪功能。但是,這個實現過程比較復雜,費時費力。更多時候,或者說在項目初期,我們都會選擇直接調用系統提供的這些功能來完成一個圖片選擇器。然而,由于安卓設備的多樣性,總會遇到各種各樣的兼容問題。本文就來總結總結,調用系統相機、相冊和裁剪功能實現圖片選擇器的過程中,我們需要注意的一些地方。

示例代碼


這里簡單使用一個示例代碼,演示調用系統相機或相冊,獲取圖片,然后使用系統裁剪功能處理圖片,并顯示到一個 ImageButton 視圖里面:

public class MainActivity extends FragmentActivity {

    public static final int REQUEST_CAMERA = 1;
    public static final int REQUEST_ALBUM = 2;
    public static final int REQUEST_CROP = 3;

    public static final String IMAGE_UNSPECIFIED = "image/*";

    private ImageButton mPictureIb;

    private File mImageFile;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPictureIb = (ImageButton) findViewById(R.id.ib_picture);
    }

    public void onClickPicker(View v) {
        new AlertDialog.Builder(this)
                .setTitle("選擇照片")
                .setItems(new String[]{"拍照", "相冊"}, new OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        if (i == 0) {
                            selectCamera();
                        } else {
                            selectAlbum();
                        }
                    }
                })
                .create()
                .show();
    }
    
    private void selectCamera() {
        createImageFile();
        if (!mImageFile.exists()) {
            return;
        }

        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
        startActivityForResult(cameraIntent, REQUEST_CAMERA);
    }

    private void selectAlbum() {
        Intent albumIntent = new Intent(Intent.ACTION_PICK);
        albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_UNSPECIFIED);
        startActivityForResult(albumIntent, REQUEST_ALBUM);
    }

    private void cropImage(Uri uri){
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
        intent.putExtra("crop", "true");
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
        startActivityForResult(intent, REQUEST_CROP);
    }

    private void createImageFile() {
        mImageFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".jpg");
        try {
            mImageFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "出錯啦", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (RESULT_OK != resultCode) {
            return;
        }
        switch (requestCode) {
            case REQUEST_CAMERA:
                cropImage(Uri.fromFile(mImageFile));
                break;

            case REQUEST_ALBUM:
                createImageFile();
                if (!mImageFile.exists()) {
                    return;
                }

                Uri uri = data.getData();
                if (uri != null) {
                    cropImage(uri);
                }
                break;

            case REQUEST_CROP:
                mPictureIb.setImageURI(Uri.fromFile(mImageFile));
                break;
        }
    }

}

效果如圖(不同設備,系統功能呈現有所不同):

看似完美,你以為上述代碼就能結束了的話,那就大錯特錯啦!這里面還有一些兼容問題要處理,還有一些地方需要特殊說明。

拍照圖片存儲問題


調用系統相機實現拍照功能的核心代碼如下:

Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(cameraIntent, REQUEST_CAMERA);

其中 MediaStore.EXTRA_OUTPUT 數據表示,拍照所得圖片保存到指定目錄下的文件(一般會在 SD 卡中創建當前應用的目錄,并創建臨時文件保存圖片)。然后,在 onActivityResult 方法中根據文件路徑獲取圖片。

如果不為 intent 添加該數據的話,將在 onActivityResult 的 intent 對象中返回一個 Bitmap 對象,通過如下代碼獲?。?/p>

Bitmap bmp = data.getParcelableExtra("data");

值得注意的是,這里的 Bitmap 對象是拍照所得圖片的一個縮略圖,尺寸很??!系統這么做也是充分考慮到應用的內存占用問題。試想一下,如今手機設備中高清相機拍出來的照片,一張圖的大小高達十幾兆,如果返回這么大的圖片,內存占用相當嚴重,何況很多時候知識臨時使用而已。所以,調用系統相機時,一般都會添加 MediaStore.EXTRA_OUTPUT 參數,避免返回 Bitmap 對象。當然,這么做也能保證應用產生的數據,包括文件,都能存儲在應用目錄下,方便清理緩存時統一清除。

拍照圖片旋轉問題


部分手機,比如三星手機,調用系統相機拍照所得的照片可能會發生自動旋轉問題,常見為旋轉 90°。所以,要求我們在拍照之后,使用圖片之前,判斷圖片是否發生過旋轉,如果是,要將照片旋轉回來。

這是獲取圖片旋轉角度的代碼:

/**
 * 獲取圖片旋轉角度
 * @param path 圖片路徑
 * @return
 */
private int parseImageDegree(String path) {
    int degree = 0;
    try {
        ExifInterface exifInterface = new ExifInterface(path);
        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;
}

這是根據指定角度旋轉圖片的代碼:

/**
 * 圖片旋轉操作
 *
 * @param bm 需要旋轉的圖片
 * @param degree 旋轉角度
 * @return 旋轉后的圖片
 */
private Bitmap rotateBitmap(Bitmap bm, int degree) {
    Bitmap returnBm = null;

    Matrix matrix = new Matrix();
    matrix.postRotate(degree);
    try {
        returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
    } catch (OutOfMemoryError e) {
    }
    if (returnBm == null) {
        returnBm = bm;
    }
    if (bm != returnBm) {
        bm.recycle();
    }
    return returnBm;
}

橫豎屏切換問題


在部分手機,調用系統拍照功能時,可能會發生橫豎屏切換過程,導致返回應用時當前 Activity 發生銷毀重建,各個生命周期又重新走了一遍。此時,一些應用內的變量數據可能丟失,使用時容易發生空值異常,進而導致 app 崩潰退出。

為了避免這種現象,我們需要在 AndroidManifest.xml 文件的對應 <activity> 標簽中添加屬性:

android:configChanges="orientation|screenSize"

這樣,當發生屏幕旋轉時,不會導致 Activity 銷毀重建,而是執行 onConfigurationChanged() 方法:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
}

調用系統裁剪問題


示例中調用系統裁剪的代碼如下:

Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, IMAGE_UNSPECIFIED);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));
startActivityForResult(intent, REQUEST_CROP);

可以看出,調用系統裁剪功能,需要設置一些 Extra 參數,很多人容易在這里產生疑惑,不知如何取舍,如何設值。這里列舉一下常用的 Extra 名字、值類型和作用:

  • crop:String 類型數據,發送裁剪信號
  • aspectXaspectY:int 類型數據,設置裁剪框的 X 與 Y 值比例
  • outputXoutputY:int 類型數據,設置裁剪輸出的圖片大小
  • scale:boolean 類型數據,設置是否支持裁剪縮放
  • return-data:boolean 類型數據,設置是否在 onActivityResult 方法的 intent 值中返回 Bitmap 對象
  • MediaStore.EXTRA_OUTPUT:Uri 類型數據,設置是否將裁剪結果保存到指定文件中

需要注意的是:

第一,設置 return-data 參數為 true 時,返回的 Bitmap 對象也為縮略圖,獲取方式與前面所述相機拍照獲取 Bitmap 的方式一致;

第二,調用系統相冊并裁剪時,如果使用MediaStore.EXTRA_OUTPUT參數,Uri 盡量不要設置為源文件對應的 Uri 值,另做保存,不損壞系統相冊中的源圖文件;

第三,根據經驗,outputX 與 outputY 值設置太大時,容易出現卡屏現象;

第四,可以不設置 outputX 與 outputY 參數,使用戶根據自身按比例自由裁剪,就像示例代碼這樣。

setImageURI() 注意事項


你可能會用到 setImageURI() 方法給 ImageView 設置圖片內容,這里也有一個地方需要注意。我們先看一下這個方法的源碼:

public void setImageURI(Uri uri) {
    if (mResource != 0 ||
            (mUri != uri &&
             (uri == null || mUri == null || !uri.equals(mUri)))) {
        updateDrawable(null);
        mResource = 0;
        mUri = uri;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        resolveUri();

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();
    }
}

可以看到,這里的 uri 參數在內部持有緩存變量,當多次調用該方法而 uri 參數值不變時,圖片展示內容不變。問題就在這,如果你多次拍照或裁剪保存的圖片文件路徑相同時,雖然每次處理過后實際存儲的文件內容發生變化,但由于路徑相同,uri 參數一致,導致多次調用 setImageURI() 設置圖片內容時,ImageView 顯示內容不變!這也是為什么示例代碼中我用時間戳處理圖片文件名的原因所在,保證每次存儲的圖片路徑不同。

根據 Uri 獲取文件地址


有時候,我們需要根據 Uri 獲取文件路徑。比如如果你不需要使用裁剪功能的話,調用系統相冊選擇圖片后返回的就是一個 Uri 對象,我們需要從這個 Uri 對象中解析出對應的圖片文件路徑,便于上傳至服務器等后續處理。

比如,這個 Uri 對象可能是:

content://media/external/images/media/3066

很多朋友相信有過這樣的經驗,使用 toString() 或者 getPath() 方法獲取 Uri 對象所對應的文件路徑,其實這是錯誤的!通過 getPath() 獲取的結果字符串是:

media/external/images/media/3066

而正確的獲取方式是:

private String parseFilePath(Uri uri) {
    String[] filePathColumn = { MediaStore.Images.Media.DATA };
    Cursor cursor = getContentResolver().query(uri, filePathColumn, null, null, null);
    cursor.moveToFirst();
    int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
    String picturePath = cursor.getString(columnIndex);
    cursor.close();
    return picturePath;
}

其對應的文件路徑應該是這個樣子的:

/storage/emulated/0/Pictures/Screenshots/S70302-131606.jpg

Base64 文件編碼處理


現在很多網絡框架內部都做了封裝處理,上傳圖片時只需要傳遞一個文件路徑即可。但是,少數情況下,根據服務器需要,我們要對圖片文件字節流編碼后再上傳。這是使用 Base64 編碼并根據字節數組獲取字符串的處理過程:

public static String fileToBase64String(String filePath) {
    File photoFile = new File(filePath);
    try {
        FileInputStream fis = new FileInputStream(photoFile);
        ByteArrayOutputStream baos = new ByteArrayOutputStream(10000);
        byte[] buffer = new byte[1000];
        while (fis.read(buffer)!=-1) {
            baos.write(buffer);
        }
        baos.close();
        fis.close();
        return Arrays.toString(Base64.encode(baos.toByteArray(), Base64.DEFAULT));
    }catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

zip 壓縮文件處理


當上傳多張圖片至服務器時,為了提升傳輸效率,往往會采用 zip 格式壓縮處理。這里提供一個遞歸壓縮代碼,方便大家有需要的時候借鑒參考:

public String zipCompass(String filePath){
    File zipFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".zip");
    try{
        //指定了兩個待壓縮的文件,都在assets目錄中  
        String[] filenames = new String[]{ "activity_main.xml", "strings.xml" };
        FileOutputStream fos = new FileOutputStream(zipFile);
        ZipOutputStream zos = new ZipOutputStream(fos);
        int i = 1;
        //枚舉filenames中的所有待壓縮文件  
        while (i <= filenames.length){
            //從filenames數組中取出當前待壓縮的文件名,作為壓縮后的名稱,以保證壓縮前后文件名一致  
            ZipEntry zipEntry = new ZipEntry(filenames[i - 1]);
            //打開當前的zipEntry對象  
            zos.putNextEntry(zipEntry);

            FileInputStream is = new FileInputStream(filePath);
            byte[] buffer = new byte[8192];
            int count = 0;
            //寫入數據  
            while ((count = is.read(buffer)) >= 0){
                zos.write(buffer, 0, count);
            }
            zos.flush();
            zos.closeEntry();
            is.close();
            i++;

        }
        zos.finish();
        zos.close();
        return zipFile.getAbsolutePath();
    }
    catch (Exception e){
        Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
        return null;
    }
}

添加系統權限


說了這么多,別忘了在 AndroidManifest.xml 文件中添加系統權限(前面示例代碼中沒有考慮到 Android 6.0 運行時權限的問題,實際使用時注意添加處理):

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容