本文描述將一個Bitmap對象保存爲一個圖片文件的主要步驟。保存的圖片文件能夠立刻在系統相冊和圖庫中找到。
主要步驟
這裏只介紹按下“保存”後如何將一個Bitmap對象保存爲圖片文件的執行步驟,對圖片的下載,圖片到Bitmap對象的轉換,Bitmap對象的格式轉換和壓縮,以及界面設計部分全部都忽略了。
- 確定存儲路徑
- 獲取外部存儲權限
- 確定外部存儲狀態
- 確定文件名
- 保存到文件中
- 發送廣播,通知系統掃描保存後的文件
確定存儲路徑
在android中文件存儲路徑包括內部存儲和外部存儲兩種類型。
對內部存儲,當一個app被安裝到手機後,Android系統會在內部存儲的/data/data/目錄下創建一個以包名稱命名的文件夾。例如/data/data/com.sohu.inputmethod.sogou/。一個應用對內部存儲的所有訪問都被限制在這個文件夾中,也就是說Android應用只能在該目錄中讀取,創建,修改文件。對該目錄之外的其他內部存儲中的目錄都沒有任何操作的權限。因此,如果將圖片保存在內部存儲中,只能被應用自身讀取,其他應用均無法讀取。如果需要讓系統圖庫,相冊或其他應用能夠找到保存的圖片,必須將圖片保存到外部存儲中。
對外部存儲,當一個app被安裝到手機後,Android系統會在外部存儲的/Android/data/目錄下創建一個以包名命名的文件夾(這裏第一個/不是根路徑,而是相對外部存儲所掛載路徑的相對路徑)。例如/storage/emulated/0/Android/data/com.sohu.inputmethod/。這個路徑同樣只能被應用自身讀取,其他應用不能訪問。因此,也不能將圖片保存在這個目錄中。
除外部存儲的/Android目錄之外的其他目錄一般都是可以被其他應用訪問的。目前,大多數應用都會在外部存儲的根路徑下建立一個類似包名的多層目錄,以存儲需要共享的文件。例如/storage/emulated/0/sogou/image/。還需要注意的是,很多查看圖片的應用都支持按照文件夾來查看圖片。如果將圖片所在的文件夾取名爲image,photo之類的,就無法和其他文件夾區分開,用戶也不能識別該文件夾的用途。因此最好取一個有區分度的文件夾名字,例如百度貼吧就保存在/tieba目錄,微信是保存在/****tencent/MicroMsg/WeiXin目錄。
由於Android系統的碎片化問題,不同設備上外部存儲的路徑很可能會不同,因此,不能直接使用/storage/emulated/0/作爲外部存儲的根路徑。
Android SDK中 Environment類 提供了getExternalStorageDirectory()方法來獲取外部存儲的根路徑。示例如下:
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tencent/MicroMsg/WeiXin/"
需要注意的是Environment.getExternalStorageDirectory()返回的路徑中最後一個字符不是/,如果需要創建子目錄,需要在子目錄的前後都加上/。
**
**
獲取外部存儲權限
由於需要在外部存儲中寫文件,需要在AndroidManifest.xml中增加如下的權限聲明。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
確定外部存儲狀態
由於外部存儲需要被掛載,也可以被卸載,在寫入文件之前,需要先判斷外部存儲的狀態是否正常。只有狀態正常情況下纔可以執行保存文件的操作。獲取外部存儲狀態同樣是通過Environment類,通過Environment.getExternalStorageState()可以得到一個字符串,來表示外部存儲的狀態。同時在Environment類中定義了一系列的String常量表示不同的狀態。在所有的狀態中只有內部存儲處於Environment.MEDIA_MOUNTED狀態時纔可以讀寫文件,因此,需要將獲取到的狀態和Environment.MEDIA_MOUNTED做比較,如果不是Environment.MEDIA_MOUNTED狀態,就返回保存失敗。示例如下。
//獲取內部存儲狀態
String state = Environment.getExternalStorageState();
//如果狀態不是mounted,無法讀寫
if (!state.equals(Environment.MEDIA_MOUNTED)) {
return;
}
確定文件名
保存的圖片文件名可以由應用根據自身需要自行確定,一般來說需要有一個命名規則,然後根據命名規則計算得到文件名。
這裏列舉幾種常見的命名規則。
- 隨機命名
這種命名規則是隨機生成一個字符串或一組數字來對圖片命名。
字符串可以通過UUID來生成,數字可以通過Random()類來生成,例如:
//通過UUID生成字符串文件名
String fileName1 = UUID.randomUUID().toString();
//通過Random()類生成數組命名
Random random = new Random();
String fileName2 = String.valueOf(random.nextInt(Integer.MAX_VALUE));
2.這種命名規則是按照數字從小到大的順序來對圖片命名。
在程序啓動時先獲取圖片文件名中當前最大數字的文件名,之後每保存一張圖片就將數字加1即可。
3.時間命名
這種命名規則是根據保存圖片的當前系統時間來對圖片命名。
系統時間可以通過System.currentTimeMillis()來獲取,不過System.currentTimeMillis()獲取到的時間是一個long型的整數,如果用它做文件名,無法通過文件名直接看出文件的具體保存時間。可以通過SimpleDateFormat先對當前時間做格式化,然後再將其作爲文件名來使用。例如:
Calendar now = new GregorianCalendar();
SimpleDateFormat simpleDate = new SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault());
String fileName = simpleDate.format(now.getTime());
使用這種命名規則來命名需要注意的是同一秒鐘可能會有多張圖片需要保存,在得到當前系統時間對應的文件名後,需要判斷該文件是否存在。如果文件已經存在,需要重新生成文件名。重新生成的文件名可以在之前的文件名後加上一個隨機數後綴,或者是用毫秒數做後綴。
4.文件URL命名
每張網絡圖片都有一個對應的圖片URL,可以根據圖片的URL來對圖片命名。
不過URL中會包含一些不能用作文件名的特殊字符,此外直接用URL來命名可能會帶來安全問題。爲了避免這兩個問題,可以將圖片URL的MD5值作爲文件名來使用。由於MD5是不可逆的,也就無法通過MD5值反向得到圖片URL,同時MD5值對應的字符串只包含[0-9A-Z],不包含特殊字符,可是作爲文件名使用。
由於每張圖片的URL是唯一的,其對應的文件名也就是唯一的。如果需要每張網絡圖片只能生成一個文件,不允許保存爲多份拷貝,可以用這種命名規則。在得到URL對應的文件名後,先判斷文件是否已經存在,如果已經存在,直接覆蓋或不處理。
保存到文件中
保存圖片文件時,通過Bitmap的compress()方法將Bitmap對象壓縮到一個文件輸出流中,然後flush()即可。示例如下。
try {
File file = new File(dir + fileName + ".jpg");
FileOutputStream out = new FileOutputStream(file);
mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
發送廣播,通知系統掃描保存後的文件
至此,已經實現將Bitmap對象保存成外部存儲中的一個jpg格式的文件。但此時該文件只是保存在外部存儲的一個目錄中,必須進入其所在的目錄中纔可以看到。在系統圖庫,相冊和其他應用中無法看到新建的圖片文件。爲了讓其他應用能夠知道圖片文件被創建,必須通知MediaProvider服務將新建的文件添加到圖片數據庫中。
Android系統中常駐一個MediaProvider服務,對應的進程名爲android.process.media,此服務用來管理本機上的媒體文件,提供媒體管理服務。在系統開機或者收到外部存儲的掛載消息後,MediaProvider會調用MediaScanner,MediaScanner會掃描外部存儲中的所有文件,根據文件類型的後綴將文件信息保存到對應的數據庫中,供其他APP使用。
MediaScannerReceiver是一個廣播接收者,當它接收到特定的廣播請求後,就會去掃描指定的文件,並根據文件信息將其添加到數據庫中。當圖片文件被創建後,就可以發送廣播給MediaScannerReceiver,通知其掃描新建的圖片文件。示例如下。
try {
File file = new File(dir + fileName + ".jpg");
FileOutputStream out = new FileOutputStream(file);
mBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
//保存圖片後發送廣播通知更新數據庫
Uri uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
} catch (Exception e) {
e.printStackTrace();
}
圖片的異步保存
保存圖片文件時,如果圖片很大,或需要同時保存多張圖片時,就需要較多的時間。爲了避免阻塞UI線程,出現幀率下降或ANR,通常需要將圖片保存操作放到線程中去執行。當圖片保存完畢後通過sendMessage()方法通知UI線程保存結果。
將圖片保存放到後臺線程去執行需要增加一些同步機制避免一些多線程問題。例如有兩張圖片需要保存,分別放到兩個線程中去執行,保存圖片時文件名以數字順序增加。第一個線程選中文件名爲125.jpg,但此時文件還未創建,第二個線程判斷125.jpg不存在,於是也選取125.jpg作爲文件名,兩張圖片就保存到同一個文件中了。
保存圖片很簡單,方法如下:
/** 首先默認個文件保存路徑 */
private static final String SAVE_PIC_PATH=Environment.getExternalStorageState().equalsIgnoreCase(Environment.MEDIA_MOUNTED) ? Environment.getExternalStorageDirectory().getAbsolutePath() : /mnt/sdcard;//保存到SD卡
private static final String SAVE_REAL_PATH = SAVE_PIC_PATH+ /good/savePic;//保存的確切位置
下面就是保存的方法,傳入參數就可以了:
public static void saveFile(Bitmap bm, String fileName, String path) throws IOException {
String subForder = SAVE_REAL_PATH + path;
File foder = new File(subForder);
if (!foder.exists()) {
foder.mkdirs();
}
File myCaptureFile = new File(subForder, fileName);
if (!myCaptureFile.exists()) {
myCaptureFile.createNewFile();
}
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(myCaptureFile));
bm.compress(Bitmap.CompressFormat.JPEG, 80, bos);
bos.flush();
bos.close();
}
這樣就保存好了,可是有的時候明明保存下來了,爲什麼進入相冊時查看不到呢?反正我是遇到這樣的問題的,原來我們在保存成功後,還要發一個系統廣播通知手機有圖片更新,廣播如下:
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(file);
intent.setData(uri);
context.sendBroadcast(intent);//這個廣播的目的就是更新圖庫,發了這個廣播進入相冊就可以找到你保存的圖片了!,記得要傳你更新的file哦
private void saveToLocal(Bitmap bitmap, String bitName) throws IOException {
File file = new File("/sdcard/DCIM/Camera/" + bitName + ".jpg");
if (file.exists()) {
file.delete();
}
FileOutputStream out;
try {
out = new FileOutputStream(file);
if (bitmap.compress(Bitmap.CompressFormat.PNG, 90, out)) {
out.flush();
out.close();
//保存圖片後發送廣播通知更新數據庫
// Uri uri = Uri.fromFile(file);
// sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(file);
intent.setData(uri);
this.sendBroadcast(intent);
showToast("保存成功");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}