您當前的位置:首頁 > 攝影

適配到Android 12,全版本支援儲存圖片到相簿方案

作者:由 願天堂沒有程式碼 發表于 攝影時間:2022-03-14

由於Google對使用者隱私和系統安全做的越來越完善,應用對一些敏感資訊的操作越來越難。比如最常見的共享儲存空間的訪問,像儲存圖片到相簿這種常見的需求。

Android 6。0

以前,應用要想儲存圖片到相簿,只需要透過

File

物件開啟IO流就可以儲存;

Android 6。0

添加了執行時許可權,需要先申請儲存許可權才可以儲存圖片;

Android 10

引入了分割槽儲存,但不是強制的,可以透過清單配置

android:requestLegacyExternalStorage=“true”

關閉分割槽儲存;

Android 11

強制開啟分割槽儲存,應用以 Android 11 為目標版本,系統會忽略

requestLegacyExternalStorage

標記,訪問共享儲存空間都需要使用

MediaStore

進行訪問。

我們透過上面的時間線可以看出,Google對系統公共儲存的訪問的門檻逐漸升高,摒棄傳統的Java File物件直接訪問檔案的方式,想將Android的共享空間訪問方式統一成一套API。這是我們的主角

MediaStore

MediaStore

是Android誕生之初就存在的一套媒體庫框架,透過文件可以看到

Added in API level 1

。但是由於最初系統比較開放,我們對它的使用並不多,但是隨著分割槽儲存的開啟,它的舞臺會越來越多。

所以怎麼才是正確的儲存圖片的方案呢?話不多說,步入正題

大致流程

我們訪問

MediaStore

有點像訪問資料庫,實際上就是資料庫,只是多了一些IO流的操作。將圖片想象成資料庫中的一條資料,我們怎麼插入資料庫呢,回想sqlite怎麼操作的。

實際上

Mediastore

也是這樣的:

先將圖片記錄插入媒體庫,獲得插入的Uri;

然後透過插入Uri開啟輸出流將檔案寫入;

大致流程就是這樣子,只是不同的系統版本有一些細微的差距;

Android 10 之前的版本需要申請儲存許可權,

Android 10及以後版本是不需要讀寫許可權的

Android 10 之前是透過File路徑開啟流的,所以需要判斷檔案是否已經存在,否者的話會將以存在的圖片給覆蓋

Android 10 及以後版本添加了

IS_PENDING

狀態標識,為0時其他應用才可見,所以在圖片儲存過後需要更新這個標識。

相信說了這麼多,大家已經不耐煩了,不慌程式碼馬上就來。

編碼時間

這裡用儲存Bitmap到相簿為例,儲存檔案 和 許可權申請的邏輯,這裡就不貼程式碼了,詳見 Demo

檢查清單檔案,如果應用裡沒有其他需要儲存許可權的需求可以加上

android:maxSdkVersion=“28”

,這樣Android 10的裝置的應用詳情就看不到這個許可權了。

<!——Android Q之後不需要儲存許可權,完全使用MediaStore API來實現——>

android:name=

“android。permission。READ_EXTERNAL_STORAGE”

android:maxSdkVersion=

“28”

/>

android:name=

“android。permission。WRITE_EXTERNAL_STORAGE”

android:maxSdkVersion=

“28”

/>

儲存圖片到相簿。這裡為了演示方便,生產環境記得在IO執行緒處理,ANR了可不怪我。

private

fun

saveImageInternal

()

{

val

uri

=

assets

open

“wallhaven_rdyyjm。jpg”

)。

use

{

it

saveToAlbum

this

fileName

=

“save_wallhaven_rdyyjm。jpg”

null

}

?:

return

Toast

makeText

this

uri

toString

(),

Toast

LENGTH_SHORT

)。

show

()

}

是不是很簡單,詳細實現是怎麼弄的,接著往下看。這是一個儲存Bitmap的擴充套件方法

/**

* 儲存Bitmap到相簿的Pictures資料夾

*

* @param context 上下文

* @param fileName 檔名。 需要攜帶字尾

* @param relativePath 相對於Pictures的路徑

* @param quality 質量

*/

fun

Bitmap

saveToAlbum

context

Context

fileName

String

relativePath

String

=

null

quality

Int

=

75

):

Uri

{

val

resolver

=

context

contentResolver

val

outputFile

=

OutputFileTaker

()

// 插入圖片資訊

val

imageUri

=

resolver

insertMediaImage

fileName

relativePath

outputFile

if

imageUri

==

null

{

Log

w

TAG

“insert: error: uri == null”

return

null

}

// 透過Uri開啟輸出流

imageUri

outputStream

resolver

?:

return

null

)。

use

{

val

format

=

fileName

getBitmapFormat

()

// 儲存圖片

this

@saveToAlbum

compress

format

quality

it

// 更新 IS_PENDING 狀態

imageUri

finishPending

context

resolver

outputFile

file

}

return

imageUri

}

插入圖片到媒體庫

需要注意Android 10以下需要圖片查重,防止檔案被覆蓋的問題。

// 儲存位置,這裡使用Picures,也可以改為 DCIM

private

val

ALBUM_DIR

=

Environment

DIRECTORY_PICTURES

/**

* 用於Q以下系統獲取圖片檔案大小來更新[MediaStore。Images。Media。SIZE]

*/

private

class

OutputFileTaker

var

file

File

=

null

/**

* 插入圖片到媒體庫

*/

private

fun

ContentResolver

insertMediaImage

fileName

String

relativePath

String

?,

outputFileTaker

OutputFileTaker

=

null

):

Uri

{

// 圖片資訊

val

imageValues

=

ContentValues

()。

apply

{

val

mimeType

=

fileName

getMimeType

()

if

mimeType

!=

null

{

put

MediaStore

Images

Media

MIME_TYPE

mimeType

}

// 插入時間

val

date

=

System

currentTimeMillis

()

/

1000

put

MediaStore

Images

Media

DATE_ADDED

date

put

MediaStore

Images

Media

DATE_MODIFIED

date

}

// 儲存的位置

val

collection

Uri

if

Build

VERSION

SDK_INT

>=

Build

VERSION_CODES

Q

{

val

path

=

if

relativePath

!=

null

“${ALBUM_DIR}/${relativePath}”

else

ALBUM_DIR

imageValues

apply

{

put

MediaStore

Images

Media

DISPLAY_NAME

fileName

put

MediaStore

Images

Media

RELATIVE_PATH

path

put

MediaStore

Images

Media

IS_PENDING

1

}

collection

=

MediaStore

Images

Media

getContentUri

MediaStore

VOLUME_EXTERNAL_PRIMARY

// 高版本不用查重直接插入,會自動重新命名

}

else

{

// 老版本

val

pictures

=

Environment

getExternalStoragePublicDirectory

ALBUM_DIR

val

saveDir

=

if

relativePath

!=

null

File

pictures

relativePath

else

pictures

if

(!

saveDir

exists

()

&&

saveDir

mkdirs

())

{

Log

e

TAG

“save: error: can‘t create Pictures directory”

return

null

}

// 檔案路徑查重,重複的話在檔名後拼接數字

var

imageFile

=

File

saveDir

fileName

val

fileNameWithoutExtension

=

imageFile

nameWithoutExtension

val

fileExtension

=

imageFile

extension

// 查詢檔案是否已經存在

var

queryUri

=

this

queryMediaImage28

imageFile

absolutePath

var

suffix

=

1

while

queryUri

!=

null

{

// 存在的話重新命名,路徑後面拼接 fileNameWithoutExtension(數字)。png

val

newName

=

fileNameWithoutExtension

+

“(${suffix++})。”

+

fileExtension

imageFile

=

File

saveDir

newName

queryUri

=

this

queryMediaImage28

imageFile

absolutePath

}

imageValues

apply

{

put

MediaStore

Images

Media

DISPLAY_NAME

imageFile

name

// 儲存路徑

val

imagePath

=

imageFile

absolutePath

Log

v

TAG

“save file: $imagePath”

put

MediaStore

Images

Media

DATA

imagePath

}

outputFileTaker

?。

file

=

imageFile

// 回傳檔案路徑,用於設定檔案大小

collection

=

MediaStore

Images

Media

EXTERNAL_CONTENT_URI

}

// 插入圖片資訊

return

this

insert

collection

imageValues

}

/**

* Android Q以下版本,查詢媒體庫中當前路徑是否存在

* @return Uri 返回null時說明不存在,可以進行圖片插入邏輯

*/

private

fun

ContentResolver

queryMediaImage28

imagePath

String

):

Uri

{

if

Build

VERSION

SDK_INT

>=

Build

VERSION_CODES

Q

return

null

val

imageFile

=

File

imagePath

if

imageFile

canRead

()

&&

imageFile

exists

())

{

Log

v

TAG

“query: path: $imagePath exists”

// 檔案已存在,返回一個file://xxx的uri

// 這個邏輯也可以不要,但是為了減少媒體庫查詢次數,可以直接判斷檔案是否存在

return

Uri

fromFile

imageFile

}

// 儲存的位置

val

collection

=

MediaStore

Images

Media

EXTERNAL_CONTENT_URI

// 查詢是否已經存在相同圖片

val

query

=

this

query

collection

arrayOf

MediaStore

Images

Media

_ID

MediaStore

Images

Media

DATA

),

“${MediaStore。Images。Media。DATA} == ?”

arrayOf

imagePath

),

null

query

?。

use

{

while

it

moveToNext

())

{

val

idColumn

=

it

getColumnIndexOrThrow

MediaStore

Images

Media

_ID

val

id

=

it

getLong

idColumn

val

existsUri

=

ContentUris

withAppendedId

collection

id

Log

v

TAG

“query: path: $imagePath exists uri: $existsUri”

return

existsUri

}

}

return

null

}

改變標誌位,通知媒體庫

到這裡整個圖片儲存就結束了。怎麼樣是不是很簡單,趕緊去系統圖庫裡看看圖片是不是已經在了。

private

fun

Uri

finishPending

context

Context

resolver

ContentResolver

outputFile

File

{

val

imageValues

=

ContentValues

()

if

Build

VERSION

SDK_INT

<

Build

VERSION_CODES

Q

{

if

outputFile

!=

null

{

// Android 10 以下需要更新檔案大小欄位,否則部分裝置的相簿裡照片大小顯示為0

imageValues

put

MediaStore

Images

Media

SIZE

outputFile

length

())

}

resolver

update

this

imageValues

null

null

// 通知媒體庫更新,部分裝置不更新 相簿看不到 ???

val

intent

=

Intent

Intent

ACTION_MEDIA_SCANNER_SCAN_FILE

this

context

sendBroadcast

intent

}

else

{

// Android Q添加了IS_PENDING狀態,為0時其他應用才可見

imageValues

put

MediaStore

Images

Media

IS_PENDING

0

resolver

update

this

imageValues

null

null

}

}

雖然程式碼有點多,但是相信

大家期盼已久了

ImageExt。kt

圖片分享

有很多場景是儲存圖片之後,呼叫第三方分享進行圖片分享,但是一些文章不管三七二十一說需要用

FileProvider

。實際上這是不準確的,部分情況是需要,還有一些場景是不需要的。

我們只需要記得

FileProvider是給其他應用分享應用私有檔案的

就夠了,只有在我們需要將應用沙盒內的檔案共享出去的時候才需要配置FileProvider。例如:

應用內更新,系統包安裝器需要讀取系統沙盒內的apk檔案(如果你下載了公共路徑那另說)

應用內沙盒圖片分享,微信已經要求一定要透過FileProvider才可以分享圖片了(沒有適配的趕緊看看分享還能用嗎)

但是儲存到系統圖庫並分享的場景明顯就不符合這個場景,因為相簿不是應用私有的空間。

private

fun

shareImageInternal

()

{

val

uri

=

assets

open

“wallhaven_rdyyjm。jpg”

)。

use

{

it

saveToAlbum

this

fileName

=

“save_wallhaven_rdyyjm。jpg”

null

}

?:

return

val

intent

=

Intent

Intent

ACTION_SEND

putExtra

Intent

EXTRA_STREAM

uri

setType

“image/*”

startActivity

Intent

createChooser

intent

null

))

}

所以在使用FileProvider要區分一下場景,是不是可以不需要,因為FileProvider是一種特殊的ContentProvider,每一個內容提供者在應用啟動的時候都要初始化,所以也會拖慢應用的啟動速度。

作者:de得得de

轉載來源於:

https://juejin。cn/post/7042218651482587172

如有侵權,請聯絡刪除!

標簽: MediaStore  null  images  media  Android