Android圖片選擇到裁剪之步步深坑

前言

最近在自己的項目里實現(xiàn)了一個頭像選擇的功能因宇,就是先從相冊里選取一張圖片再調(diào)用系統(tǒng)的裁剪功能來制作頭像,效果就像下面這樣:

本以為很小的一個功能坏匪,卻遠(yuǎn)遠(yuǎn)沒有我想的那樣簡單穴张,可以說每一步都暗藏玄機,下面就讓我?guī)Т蠹铱纯催@里面究竟有哪些坑枪蘑。

Android 4.4之存儲訪問框架

首先损谦,讓我們從圖片選擇開始岖免,使用隱式Intent跳轉(zhuǎn)到圖片選擇:

private void routeToGallery() {
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    startActivityForResult(intent, GALLERY_REQUSET_CODE);
}

在回調(diào)中處理返回的圖片,繼而跳轉(zhuǎn)至圖片裁剪:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == GALLERY_REQUSET_CODE && resultCode == RESULT_OK) {
        String path = data.getData().getPath();
        Bitmap image = BitmapFactory.decodeFile(path);
        File faceFile;
        try {
            faceFile = saveBitmap(image);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        Uri fileUri = Uri.fromFile(faceFile);
        routeToCrop(fileUri);      //跳轉(zhuǎn)到圖片裁剪
    }
}

private void routeToCrop(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", true);
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 150);
    intent.putExtra("outputY", 150);
    intent.putExtra("return-data", true);
    startActivityForResult(intent, CROP_REQUEST_CODE);
}

private File saveBitmap(Bitmap bitmap) throws IOException {
    File file = new File(getExternalCacheDir(), "face-cache");
    if (!file.exists()) file.createNewFile();
    try (OutputStream out = new FileOutputStream(file)) {
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
    }
    return file;
}

這一段代碼看似正常照捡,但問題就出在String path = data.getData().getPath();這一句颅湘。這一段代碼在Android 4.4以下是可以正常運行的,不過從Android 4.4開始這里獲取到的將為一個無效的路徑栗精,這是為什么呢闯参?

Android從4.4開始引入了一個概念:存儲訪問框架,簡單來說就是Android提供了一個專門供用戶訪問資源的軟件悲立,將設(shè)備上所有可以訪問資源的軟件接口都整合到了一起鹿寨,避免了用戶只能選擇一個特定軟件的尷尬,在Android 4.4以下薪夕,我們發(fā)送剛才選取圖片的隱式Intent脚草,效果是這樣的,需要用戶去選擇使用哪個應(yīng)用:

Android 4.4以下

而從Android 4.4開始原献,就變成了這樣:

Android 4.4及以上

直接打開一個資源選取的軟件(這個軟件平時是隱藏的馏慨,不會顯示在軟件列表中),其中包含了訪問設(shè)備上所有可訪問資源軟件的接口姑隅,這個改變極大的提高的用戶操作的便捷性写隶。

不過這也帶來了一個問題,從Android 4.4開始讲仰,在onActivityResult()方法的Intent中所包含的uri不再是file://類型慕趴,而是變成了content://類型,這也是為什么在Android 4.4以后調(diào)用data.getData.getPath()獲取到的結(jié)果是無效的鄙陡。因此冕房,我們必須對Android 4.4以上的版本進行特殊的處理:

private void routeToGallery() {
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("image/*");
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        startActivityForResult(intent, GALLERY_REQUSET_CODE_KITKAT);
    } else {
        startActivityForResult(intent, GALLERY_REQUSET_CODE);
    }
}

在回調(diào)中對不同版本分別進行處理:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case GALLERY_REQUSET_CODE:
            handleGalleryResult(resultCode, data);
            break;
        case GALLERY_REQUSET_CODE_KITKAT:
            handleGalleryKitKatResult(resultCode, data);
            break;
    }
}

private void handleGalleryResult(int resultCode, Intent data) {
    // 跟之前一樣
}

// Result uri is "content://" after Android 4.4
private void handleGalleryKitKatResult(int resultCode, Intent data) {
    File faceFile;
    try {
        ParcelFileDescriptor parcelFileDescriptor =
                getContentResolver().openFileDescriptor(contentUri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        faceFile = saveBitmap(image);
    } catch (IOException e) {
        e.printStackTrace();
        return;
    }
    Uri fileUri = Uri.fromFile(faceFile);
    routeToCrop(fileUri);
}

Android 7.0之FileProvider

完成了圖片的選擇功能,轉(zhuǎn)眼又碰到了一個問題:

Android為了提高私有文件的安全性柔吼,從7.0開始對外傳遞file://類型的uri會觸發(fā)FileUriExposedException毒费。因此丙唧,在分享私有文件時必須使用FileProvider愈魏。

對Android的這一改變還不太了解的同學(xué)可以看一下這兩篇文章:Android 7.0 行為變更Setting Up File Sharing

第一步

manifest文件中加入FileProvider

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="gavinli.translator">
    <application
        ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="gavinli.translator"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
        ...
    </application>
</manifest>

第二步

xml文件夾中創(chuàng)建filepaths.xml文件想际,并聲明所要分享的文件目錄:

<resources>
    <paths>
        <external-cache-path name="mycache" path="./" />
    </paths>
</resources>

這里的path就代表你想要分享的文件目錄培漏,而name就是具體顯示在uri中的信息,最終生成的uri就像下面這樣:

這種經(jīng)過處理的uri可以很好的隱藏掉實際的文件路徑胡本。

第三步

在代碼中對Android 7.0以上的版本進行特殊處理:

private void handleGalleryKitKatResult(int resultCode, Intent data) {
    ...

    Uri fileUri;
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // Android 7.0 "file://" uri權(quán)限適配
        fileUri = FileProvider.getUriForFile(this,
                "gavinli.translator", faceFile);
    } else {
        fileUri = Uri.fromFile(faceFile);
    }
    routeToCrop(fileUri);
}

這里傳入的"gavinli.translator"牌柄,需要與之前在manifest文件中聲明的android:authorities一致。

第四步

在裁剪圖片的Intent中加入對該圖片的訪問權(quán)限:

private void routeToCrop(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");

    // 加入訪問權(quán)限
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            | Intent.FLAG_GRANT_READ_URI_PERMISSION);

    ...
}

最后一步

在回調(diào)中獲取裁剪后的圖片:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        ...
        case CROP_REQUEST_CODE:
            Bundle bundle = data.getExtras();
            Bitmap face = bundle.getParcelable("data");
            break;
    }
}

Intent的限制

你以為到這里就結(jié)束了嗎侧甫?其實還遠(yuǎn)遠(yuǎn)沒有珊佣。我們這里裁剪的圖片是用作頭像的蹋宦,所以大小一般都比較小≈涠停可以當(dāng)圖片的大小變大后就會發(fā)現(xiàn)冷冗,每次裁剪后在Intent中獲取到的圖片其實都是縮略圖。

這是因為Android對Intent中所包含數(shù)據(jù)的大小是有限制的惑艇,一般不能超過1M蒿辙,否則應(yīng)用就會崩潰,這就是Intent中的圖片數(shù)據(jù)只能是縮略圖的原因滨巴。而解決的辦法也很簡單思灌,我們需要給圖片裁剪應(yīng)用指定一個輸出文件,用來存放裁剪后的圖片:

private void routeToCrop(Uri uri) {
    ...
    intent.putExtra("return-data", false);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
            new File(getExternalCacheDir(), "face-cropped")));
    startActivityForResult(intent, CROP_REQUEST_CODE);
}

現(xiàn)在恭取,在回調(diào)中的圖片就不能再直接從Intent中獲取了泰偿,而是需要先拿到Intent中的uri,再使用uri進行獲取蜈垮,具體的過程和之前處理uri的方式一樣甜奄,這里就不再贅述了。當(dāng)然窃款,直接從之前指定的文件中讀取數(shù)據(jù)也是可以的课兄。

Android 6.0之運行時權(quán)限

不知道大家發(fā)現(xiàn)了沒有,之前保存圖片的目錄都是使用的Context.getExternalCacheDir()晨继,這個方法獲取到的目錄為/sdcard/Android/data/gavinli.translator/cache烟阐,是應(yīng)用專屬的外部存儲空間,不需要聲明權(quán)限紊扬。而要想使用公共的存儲空間蜒茄,就勢必要面對一個問題:Android 6.0的運行時權(quán)限

首先餐屎,在manifest文件中聲明讀取外置存儲的權(quán)限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="gavinli.translator">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    ...
</manifest>

之后檀葛,在代碼中加入運行時的權(quán)限申請:

private void request() {
    String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
    if(ContextCompat.checkSelfPermission(this, permisson)
            != PackageManager.PERMISSION_GRANTED) {
        requestPermissions(permissions, REQUEST_CODE);
    } else {
        // 存儲圖片
    }
}

public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                           int[] grantResults) {
    if(requestCode == REQUEST_CODE) {
        if(grantResults[i] == PackageManager.PERMISSION_GRANTED) {
            // 存儲圖片
        }
    }
}

后記

到這里,這一次的踩坑之旅就全部結(jié)束了腹缩,我們也看到了Android這幾個版本以來一步步對權(quán)限的限制屿聋,雖然這對我們的開發(fā)產(chǎn)生一定的影響,但只要能提高用戶的使用體驗藏鹊,這點困難又算的了什么呢润讥?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市盘寡,隨后出現(xiàn)的幾起案子楚殿,更是在濱河造成了極大的恐慌,老刑警劉巖竿痰,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脆粥,死亡現(xiàn)場離奇詭異砌溺,居然都是意外死亡,警方通過查閱死者的電腦和手機变隔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門抚吠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弟胀,你說我怎么就攤上這事楷力。” “怎么了孵户?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵萧朝,是天一觀的道長。 經(jīng)常有香客問我夏哭,道長检柬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任竖配,我火速辦了婚禮何址,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘进胯。我一直安慰自己用爪,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布胁镐。 她就那樣靜靜地躺著偎血,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盯漂。 梳的紋絲不亂的頭發(fā)上颇玷,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音就缆,去河邊找鬼帖渠。 笑死,一個胖子當(dāng)著我的面吹牛竭宰,可吹牛的內(nèi)容都是我干的空郊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼羞延,長吁一口氣:“原來是場噩夢啊……” “哼渣淳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伴箩,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鄙漏,沒想到半個月后嗤谚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棺蛛,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年巩步,在試婚紗的時候發(fā)現(xiàn)自己被綠了旁赊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡椅野,死狀恐怖终畅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情竟闪,我是刑警寧澤离福,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站炼蛤,受9級特大地震影響妖爷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜理朋,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一絮识、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗽上,春花似錦次舌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至烹看,卻和暖如春国拇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惯殊。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工酱吝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人土思。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓务热,卻偏偏與公主長得像,于是被迫代替她去往敵國和親己儒。 傳聞我的和親對象是個殘疾皇子崎岂,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,075評論 25 707
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,401評論 0 17
  • 注:本文完全拷貝自https://developer.android.com/guide/components/i...
    RxCode閱讀 1,740評論 1 13
  • 我總以為,我并不愛你,不過是貪戀你熾熱的感情江醇,看著它燃燒起來的光芒濒憋,讓我心下歡喜。 我總以為陶夜,愛情是最不可靠的感情...
    JaneZou閱讀 224評論 0 1
  • 自從讀書多了之后条辟,發(fā)現(xiàn)自己能夠更加專注的投入其中黔夭,并且也能夠收獲更多的東西,而在這個過程中也開始找尋更多有意思的東...
    王立登閱讀 8,651評論 72 409