Android 深入瞭解相簿內部 一
之前在工作專案的時候遇到過要獲取手機上所有圖片資訊的需求,也就是要在自己應用內部做一個圖片選擇器的功能,當產品提出這個問題的時候我當時的想法就很懷疑這個需要合理,後來我就在github上搜索到了一個挺好的圖片選擇的庫:https://github.com/learnNcode/MediaChooser,後來整合到專案中的時候發現居然系統的相簿的功能差不多的,都可以掃描出手機上的圖片,而且毫無遺漏的。剛好最近有時間無聊了就看看各種的原始碼的具體實現。
這些日子仔細的看了一下里面的程式碼發現其實不管是微信相簿還是QQ相片選擇器,還是市面上各種各樣的相簿軟體最後都是通過呼叫系統的contentProvider的uri來獲取手機上圖片的資訊,因為這些資訊都儲存在資料庫裡,並且記錄了圖片的種種資訊,比如圖片的儲存路徑,經緯度,mimeType,大小,檔案時間,修改時間等等一系列的資訊都儲存在資料庫中,我們只要去讀取該資料庫的資訊,然後再使用一個圖片載入的框架去顯示這些圖片就可以了。但是系統為了安全處理並沒有讓我們直接去訪問資料庫而且通過ContentProvider暴漏出Uri來供外部呼叫的。
首先首先看看微信相簿選擇框架的效果圖:
除了能多選多張圖片之外我們可以選擇不同資料夾的下的圖片和視訊的,我們就按照它的樣式自己動手實現一個,因為之前用習慣了universal-image-loader,而且感覺用的也不錯的。Facebook的fresco雖然比較省記憶體,但是體積非常的大,所以我感覺並不怎麼適合使用的。其實你也可以使用別的框架,都差不多的。
程式碼實現
Gradle
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
接下來我們需要自定義Application,並且重寫onCreate()方法,然後配置好ImageLoader的資訊,這裡我們還使用了
private void initImageLoader() {
DisplayImageOptions.Builder builder = new DisplayImageOptions.Builder();
builder.cacheInMemory(true).cacheOnDisk(true).bitmapConfig(Bitmap.Config.RGB_565);
builder.imageScaleType(ImageScaleType.IN_SAMPLE_INT);
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder (sInstance)
.defaultDisplayImageOptions(builder.build())
.threadPoolSize(3).build();
ImageLoader.getInstance().init(configuration);
}
我們重新寫一個整合BaseAdapter的基類,所有有關圖片展示的類都繼承該類
public abstract class CommonAdapter<T> extends BaseAdapter {
protected List<T> mList;
protected Context mContext;
protected ImageLoader mImageLoader;
protected DisplayImageOptions mOptions;
protected DisplayImageOptions.Builder mBuilder;
public CommonAdapter(Context context) {
this.mContext = context;
this.mList = new ArrayList<>();
mImageLoader = ImageLoader.getInstance();
mBuilder = new DisplayImageOptions.Builder();
mBuilder.bitmapConfig(Bitmap.Config.RGB_565);
mBuilder.cacheInMemory(true);
mBuilder.cacheOnDisk(true);
}
public void setItems(List<T> datas) {
if(datas != null && datas.size() > 0) {
mList.clear();
mList.addAll(datas);
notifyDataSetChanged();
}
}
public void addItem(T data) {
if(mList != null) {
mList.add(data);
notifyDataSetChanged();
}
}
public void addItems(List<T> datas) {
if(datas != null && datas.size() > 0) {
mList.addAll(datas);
notifyDataSetChanged();
}
}
public void clearAll(boolean refresh) {
mList.clear();
if(refresh) {
notifyDataSetChanged();
}
}
public List<T> getList() {
return mList;
}
public ImageLoader getImageLoader() {
return mImageLoader;
}
@Override
public int getCount() {
return mList == null ? 0 : mList.size();
}
@Override
public T getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
abstract public View getView(int position, View convertView, ViewGroup parent);
}
當基礎工作都做好了之後我們首先來獲取相簿資料夾的資訊,因為我們知道圖片可以存放在不同的資料夾中的,然後資料夾的話也是有名字的,同時每個資料夾下肯定會存放了不同數量的圖片的。
獲取所有的圖片資料夾資訊
//查詢相簿資料夾的單位資訊
private static final String PROJECTION_BUCKET[] = {
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATA,
};
private void loaData() {
//根據檔案建立的時間降序排序
final String orderBy = MediaStore.Images.Media.DATE_TAKEN;
List<BucketInfo> list = new ArrayList();
Cursor cursor = null;
try {
/**
* EXTERNAL_CONTENT_URI 這是本次程式碼最核心的uri了,我們就可以通過contentProvider
* 來讀取系統的資料庫來獲取圖片資料夾資訊了。然後就是我們需要獲取哪些資料庫列,以及條件等等
*/
cursor = mContext.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION_BUCKET, null, null, orderBy + " DESC");
if (cursor != null) {
while (cursor.moveToNext()) {
BucketInfo entry = new BucketInfo();
//獲取資料夾的id
entry.setBucketId(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_ID)));
//該名字是圖片的名字,是從該圖片路徑上所擷取下來的
entry.setDisplayName(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)));
//獲取相簿資料夾顯示的名字
entry.setBucketName(cursor.getString(
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)));
//獲取第一張圖片用於資料夾封面展示的
entry.setBucketUrl(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)));
/**
* 這裡需要對重複的bucketInfo進行判斷,因為我們在查詢bucket的時候,
* 其實也是在圖片資訊表中進行查詢的,所以這裡會有很多重複的bucketInfo的,
* 所以需要判斷重複的問題,後面我們會講明理由的
*/
if (!list.contains(entry)) {
list.add(entry);
}
}
}
} catch (Exception e) {
if (cursor != null) {
cursor.close();
}
}
上述程式碼就是本次獲取圖片資料夾最核心的,其中EXTERNAL_CONTENT_URI
也是本文中最關鍵的一部分,因為我們只有通過該uri來獲取手機上存放的圖片資訊。但是我們本次獲取的僅僅只是圖片資料夾的一些資訊的。因為在查詢的過程中肯定是比較耗時的我們需要把這段程式碼放到執行緒中去執行的。
上面我們僅僅獲取了所有圖片的資料夾資訊的,接下來我們需要獲取各個圖片資料夾下的對應圖片的資訊。我們根據bucketName為查詢條件,然後查詢所有的對應的圖片資訊
根據圖片資料夾名字查詢對應的資訊
//格式化字串
private String mFormatType = "yyyy-MM-dd";
/**
* 查詢的資訊列,Media。DATA表示圖片的存放路徑,Media.DATE_TAKEN表示圖片建立的時間
*/
Media._ID是圖片的一個唯一標識
private String[] PROJECTION_BUCKET = {
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media._ID
};
private void loadData(String bucket) {
Cursor cursor = null;
List<MediaInfo> list = null;
try {
//根據檔案建立時間進行降序排序
final String orderBy = MediaStore.Images.Media.DATE_TAKEN;
//根據bucketName進行查詢
String searchParams = "bucket_display_name = \"" + bucket + "\"";
//現在我們還是通過 EXTERNAL_CONTENT_URI進行查詢的,由此我們可知該地址專門訪問圖片的
cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION_BUCKET, searchParams, null, orderBy + " DESC");
list = getDataFromCursor(cursor);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
Message msg = mHandler.obtainMessage();
msg.what = 1;
msg.obj = list;
mHandler.sendMessage(msg);
}
private List<MediaInfo> getDataFromCursor(Cursor cursor) {
List<MediaInfo> list = new ArrayList<>();
if (cursor != null && cursor.getCount() > 0) {
MediaInfo mediaInfo;
while (cursor.moveToNext()) {
mediaInfo = new MediaInfo();
//根據Media.DATA欄位獲取圖片的存放路徑
mediaInfo.setFilePath(cursor.getString(
cursor.getColumnIndexOrThrow((MediaStore.Images.Media.DATA))));
//根據Media.DATE_TAKEN獲取圖片的建立時間
mediaInfo.setDisplaytime(cursor.getLong(
cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN)));
//對時間進行格式化為"yyyyMMdd"格式,根據時間進行排序操作
mediaInfo.setDisplaytime(TimeUtils.parseTime(mFormatType,
TimeUtils.formatTime(mFormatType, mediaInfo.getDisplaytime())));
list.add(mediaInfo);
}
}
return list;
}
上述程式碼就是根據bucketName為搜尋條件查詢對應的圖片資訊。也沒有什麼難度的,只是查詢的條件不同而已的,其實最核心的東西就是這些的。下面我們就來查詢所有的圖片資訊。
獲取手機上所有圖片資訊
private void loadData() {
//根據時間的降序來獲取圖片資訊
final String orderBy = MediaStore.Images.Media.DATE_TAKEN + " DESC";
ContentResolver contentResolver = mContext.getContentResolver();
List<ImageInfo> imageInfos = null;
Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, PROJECTION_BUCKET, null, null, orderBy);
if (cursor != null && cursor.getCount() > 0) {
ImageInfo imageInfo;
File file;
imageInfos = new ArrayList();
while (cursor.moveToNext()) {
//獲取圖片儲存的實際路徑
String filePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
/**
* 這裡需要判斷該路徑的圖片是否存在,因為有時候圖片刪除了,
* 但是系統並沒有立刻就發現,所以資料庫中的資訊也並沒有刪除的。所以就會存在這種問題
*/
if(TextUtils.isEmpty(filePath)) continue;
file = new File(filePath);
if (!file.exists()) continue;
//獲取圖片大小
long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE));
imageInfo = new ImageInfo();
imageInfo.setFilePath(filePath);
//獲取圖片的建立時間
imageInfo.setCreateTime(cursor.getLong(
cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN)));
imageInfo.setFileSize(fileSize);
//格式化時間,省略時分秒,只記錄日期的秒數
imageInfo.setDisplaytime(TimeUtils.parseTime(mFormatType,
TimeUtils.formatTime(mFormatType, imageInfo.getCreateTime())));
imageInfos.add(imageInfo);
}
}
if(cursor != null) {
cursor.close();
}
//對所有的圖片根據建立時間進行降序排序
if (imageInfos != null && imageInfos.size() > 1) {
Collections.sort(imageInfos, new Comparator<ImageInfo>() {
@Override
public int compare(ImageInfo imageInfo1, ImageInfo imageInfo2) {
if (imageInfo1.getDisplaytime() < imageInfo2.getDisplaytime()) {
return 1;
} else if (imageInfo1.getDisplaytime() > imageInfo2.getDisplaytime()) {
return -1;
}
return 0;
}
});
}
Message msg = mHandler.obtainMessage();
if (imageInfos != null && imageInfos.size() > 0) {
msg.obj = imageInfos;
msg.what = 1;
mHandler.sendMessage(msg);
} else {
msg.what = -1;
mHandler.sendMessage(msg);
}
}
從上面的程式碼中我們發現獲取手機上”所有”圖片是非常簡單的,並沒有涉及到什麼高深的東西,只是呼叫一下系統的contentProvider的uri來查詢系統資料庫中的資訊封裝好資料之後就使用ListView或者是GridView來進行顯示,在展示ImageView的時只要注意不要記憶體溢位就行了,這個時候我們就找一個開源的圖片載入框架來進行展示一下就可以了。注意: 在獲取圖片資訊具體路徑的時候,這個時候我們需要對該路徑是否存在圖片應該先檢測一下,因為有時候使用者可能將系統中的圖片刪除了,如果這個時候使用者沒有主動的告訴系統刪除了某張圖片的話,系統並不會馬上之情的,因為系統是在某些特定的情況去掃描整個SD目錄的,並不會時時的去掃描的,如果進行時時掃描的話這個對於系統的開銷是非常大的,所以會出現圖片刪除了,但是系統中記錄的的圖片資訊並沒有馬上刪除,所以這個時候就要進行判斷一下。
總結
上面就是獲取系統圖片的方案,其實市面上所有的相簿選擇框架都是基於該原理進行做的,只不過是他們把那些介面做的更好看了,然後加入了更多好看的動畫效果,但是其最核心最關鍵的獲取手機圖片資訊的原理就是這樣子的。其實獲取手機圖片資訊有以下兩種方法:1. 就是不斷的去掃描SD卡的各個目錄,然後將字尾名為.png或者是其他格式的檔案的資訊儲存起來,然後存放到資料庫裡面去, 2. 還有就是我們在儲存一張圖片的時候主動的傳送一個廣播出去告訴系統自己圖片的資訊。但是如果我們的程式不斷的去掃描SD卡的各個目錄然後獲取圖片資訊的話顯然是非常不現實的,所以這個時候系統就為我們做了很多我們做不了的事情,沒個一段的時間去掃描或者是其他程式主動告訴系統圖片存放的資訊然後儲存起來,而我們只要去呼叫系統的api查詢資料庫就行了,這樣子豈不是一舉兩得呢?