Android 使用開源庫StickyGridHeaders來實現帶sections和headers的GridView顯示本地圖片效果
大家好!過完年回來到現在差不多一個月沒寫文章了,一是覺得不知道寫哪些方面的文章,沒有好的題材來寫,二是因為自己的一些私事給耽誤了,所以過完年的第一篇文章到現在才發表出來,2014年我還是會繼續在CSDN上面更新我的部落格,歡迎大家關注一下,今天這篇文章主要的是介紹下開源庫StickyGridHeaders的使用,StickyGridHeaders是一個自定義GridView帶sections和headers的Android庫,sections就是GridView item之間的分隔,headers就是固定在GridView頂部的標題,類似一些Android手機聯絡人的效果,StickyGridHeaders的介紹在
com.tonicartos.widget.stickygridheaders這個包就是我放StickyGridHeaders開源庫的原始碼,com.example.stickyheadergridview這個包是我實現此功能的程式碼,類看起來還蠻多的,下面我就一一來介紹了
GridItem用來封裝StickyGridHeadersGridView 每個Item的資料,裡面有本地圖片的路徑,圖片加入手機系統的時間和headerId
圖片的路徑path和圖片加入的時間time 我們直接可以通過ContentProvider獲取,但是headerId需要我們根據邏輯來生成。package com.example.stickyheadergridview; /** * @blog http://blog.csdn.net/xiaanming * * @author xiaanming * */ public class GridItem { /** * 圖片的路徑 */ private String path; /** * 圖片加入手機中的時間,只取了年月日 */ private String time; /** * 每個Item對應的HeaderId */ private int headerId; public GridItem(String path, String time) { super(); this.path = path; this.time = time; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } public int getHeaderId() { return headerId; } public void setHeaderId(int headerId) { this.headerId = headerId; } }
package com.example.stickyheadergridview;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.provider.MediaStore;
/**
* 圖片掃描器
*
* @author xiaanming
*
*/
public class ImageScanner {
private Context mContext;
public ImageScanner(Context context){
this.mContext = context;
}
/**
* 利用ContentProvider掃描手機中的圖片,將掃描的Cursor回撥到ScanCompleteCallBack
* 介面的scanComplete方法中,此方法在執行在子執行緒中
*/
public void scanImages(final ScanCompleteCallBack callback) {
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
callback.scanComplete((Cursor)msg.obj);
}
};
new Thread(new Runnable() {
@Override
public void run() {
//先發送廣播掃描下整個sd卡
mContext.sendBroadcast(new Intent(
Intent.ACTION_MEDIA_MOUNTED,
Uri.parse("file://" + Environment.getExternalStorageDirectory())));
Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver mContentResolver = mContext.getContentResolver();
Cursor mCursor = mContentResolver.query(mImageUri, null, null, null, MediaStore.Images.Media.DATE_ADDED);
//利用Handler通知呼叫執行緒
Message msg = mHandler.obtainMessage();
msg.obj = mCursor;
mHandler.sendMessage(msg);
}
}).start();
}
/**
* 掃描完成之後的回撥介面
*
*/
public static interface ScanCompleteCallBack{
public void scanComplete(Cursor cursor);
}
}
ImageScanner是一個圖片的掃描器類,該類使用ContentProvider掃描手機中的圖片,我們通過呼叫scanImages()方法就能對手機中的圖片進行掃描,將掃描的Cursor回撥到ScanCompleteCallBack 介面的scanComplete方法中,由於考慮到掃描圖片屬於耗時操作,所以該操作執行在子執行緒中,在我們掃描圖片之前我們需要先發送廣播來掃描外部媒體庫,為什麼要這麼做呢,假如我們新增加一張圖片到sd卡,圖片確實已經添加了進去,但是我們此時的媒體庫還沒有同步更新,若不同步媒體庫我們就看不到新增加的圖片,當然我們可以通過重新啟動系統來更新媒體庫,但是這樣不可取,所以我們直接傳送廣播就可以同步媒體庫了。package com.example.stickyheadergridview;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.os.Handler;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.util.Log;
/**
* 本地圖片載入器,採用的是非同步解析本地圖片,單例模式利用getInstance()獲取NativeImageLoader例項
* 呼叫loadNativeImage()方法載入本地圖片,此類可作為一個載入本地圖片的工具類
*
* @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
public class NativeImageLoader {
private static final String TAG = NativeImageLoader.class.getSimpleName();
private static NativeImageLoader mInstance = new NativeImageLoader();
private static LruCache<String, Bitmap> mMemoryCache;
private ExecutorService mImageThreadPool = Executors.newFixedThreadPool(1);
private NativeImageLoader(){
//獲取應用程式的最大記憶體
final int maxMemory = (int) (Runtime.getRuntime().maxMemory());
//用最大記憶體的1/8來儲存圖片
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
//獲取每張圖片的bytes
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
}
/**
* 通過此方法來獲取NativeImageLoader的例項
* @return
*/
public static NativeImageLoader getInstance(){
return mInstance;
}
/**
* 載入本地圖片,對圖片不進行裁剪
* @param path
* @param mCallBack
* @return
*/
public Bitmap loadNativeImage(final String path, final NativeImageCallBack mCallBack){
return this.loadNativeImage(path, null, mCallBack);
}
/**
* 此方法來載入本地圖片,這裡的mPoint是用來封裝ImageView的寬和高,我們會根據ImageView控制元件的大小來裁剪Bitmap
* 如果你不想裁剪圖片,呼叫loadNativeImage(final String path, final NativeImageCallBack mCallBack)來載入
* @param path
* @param mPoint
* @param mCallBack
* @return
*/
public Bitmap loadNativeImage(final String path, final Point mPoint, final NativeImageCallBack mCallBack){
//先獲取記憶體中的Bitmap
Bitmap bitmap = getBitmapFromMemCache(path);
final Handler mHander = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mCallBack.onImageLoader((Bitmap)msg.obj, path);
}
};
//若該Bitmap不在記憶體快取中,則啟用執行緒去載入本地的圖片,並將Bitmap加入到mMemoryCache中
if(bitmap == null){
mImageThreadPool.execute(new Runnable() {
@Override
public void run() {
//先獲取圖片的縮圖
Bitmap mBitmap = decodeThumbBitmapForFile(path, mPoint == null ? 0: mPoint.x, mPoint == null ? 0: mPoint.y);
Message msg = mHander.obtainMessage();
msg.obj = mBitmap;
mHander.sendMessage(msg);
//將圖片加入到記憶體快取
addBitmapToMemoryCache(path, mBitmap);
}
});
}
return bitmap;
}
/**
* 往記憶體快取中新增Bitmap
*
* @param key
* @param bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null && bitmap != null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 根據key來獲取記憶體中的圖片
* @param key
* @return
*/
private Bitmap getBitmapFromMemCache(String key) {
Bitmap bitmap = mMemoryCache.get(key);
if(bitmap != null){
Log.i(TAG, "get image for LRUCache , path = " + key);
}
return bitmap;
}
/**
* 清除LruCache中的bitmap
*/
public void trimMemCache(){
mMemoryCache.evictAll();
}
/**
* 根據View(主要是ImageView)的寬和高來獲取圖片的縮圖
* @param path
* @param viewWidth
* @param viewHeight
* @return
*/
private Bitmap decodeThumbBitmapForFile(String path, int viewWidth, int viewHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
//設定為true,表示解析Bitmap物件,該物件不佔記憶體
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
//設定縮放比例
options.inSampleSize = computeScale(options, viewWidth, viewHeight);
//設定為false,解析Bitmap物件加入到記憶體中
options.inJustDecodeBounds = false;
Log.e(TAG, "get Iamge form file, path = " + path);
return BitmapFactory.decodeFile(path, options);
}
/**
* 根據View(主要是ImageView)的寬和高來計算Bitmap縮放比例。預設不縮放
* @param options
* @param width
* @param height
*/
private int computeScale(BitmapFactory.Options options, int viewWidth, int viewHeight){
int inSampleSize = 1;
if(viewWidth == 0 || viewWidth == 0){
return inSampleSize;
}
int bitmapWidth = options.outWidth;
int bitmapHeight = options.outHeight;
//假如Bitmap的寬度或高度大於我們設定圖片的View的寬高,則計算縮放比例
if(bitmapWidth > viewWidth || bitmapHeight > viewWidth){
int widthScale = Math.round((float) bitmapWidth / (float) viewWidth);
int heightScale = Math.round((float) bitmapHeight / (float) viewWidth);
//為了保證圖片不縮放變形,我們取寬高比例最小的那個
inSampleSize = widthScale < heightScale ? widthScale : heightScale;
}
return inSampleSize;
}
/**
* 載入本地圖片的回撥介面
*
* @author xiaanming
*
*/
public interface NativeImageCallBack{
/**
* 當子執行緒載入完了本地的圖片,將Bitmap和圖片路徑回撥在此方法中
* @param bitmap
* @param path
*/
public void onImageLoader(Bitmap bitmap, String path);
}
}
NativeImageLoader該類是一個單例類,提供了本地圖片載入,記憶體快取,裁剪等邏輯,該類在載入本地圖片的時候採用的是非同步載入的方式,對於大圖片的載入也是比較耗時的,所以採用子執行緒的方式去載入,對於圖片的快取機制使用的是LruCache,我們使用手機分配給應用程式記憶體的1/8用來快取圖片,給圖片快取的記憶體不宜太大,太大也可能會發生OOM,該類是用我之前寫的文章Android 使用ContentProvider掃描手機中的圖片,仿微信顯示本地圖片效果,在這裡我就不做過多的介紹,有興趣的可以去看看那篇文章,不過這裡新增了一個方法trimMemCache(),,用來清空LruCache使用的記憶體我們看主介面的佈局程式碼,裡面只有一個自定義的StickyGridHeadersGridView控制元件
<?xml version="1.0" encoding="utf-8"?>
<com.tonicartos.widget.stickygridheaders.StickyGridHeadersGridView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/asset_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:columnWidth="90dip"
android:horizontalSpacing="3dip"
android:numColumns="auto_fit"
android:verticalSpacing="3dip" />
在看主介面的程式碼之前我們先看StickyGridAdapter的程式碼
package com.example.stickyheadergridview;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.stickyheadergridview.MyImageView.OnMeasureListener;
import com.example.stickyheadergridview.NativeImageLoader.NativeImageCallBack;
import com.tonicartos.widget.stickygridheaders.StickyGridHeadersSimpleAdapter;
/**
* StickyHeaderGridView的介面卡,除了要繼承BaseAdapter之外還需要
* 實現StickyGridHeadersSimpleAdapter介面
*
* @blog http://blog.csdn.net/xiaanming
*
* @author xiaanming
*
*/
public class StickyGridAdapter extends BaseAdapter implements
StickyGridHeadersSimpleAdapter {
private List<GridItem> hasHeaderIdList;
private LayoutInflater mInflater;
private GridView mGridView;
private Point mPoint = new Point(0, 0);//用來封裝ImageView的寬和高的物件
public StickyGridAdapter(Context context, List<GridItem> hasHeaderIdList,
GridView mGridView) {
mInflater = LayoutInflater.from(context);
this.mGridView = mGridView;
this.hasHeaderIdList = hasHeaderIdList;
}
@Override
public int getCount() {
return hasHeaderIdList.size();
}
@Override
public Object getItem(int position) {
return hasHeaderIdList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder mViewHolder;
if (convertView == null) {
mViewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.grid_item, parent, false);
mViewHolder.mImageView = (MyImageView) convertView
.findViewById(R.id.grid_item);
convertView.setTag(mViewHolder);
//用來監聽ImageView的寬和高
mViewHolder.mImageView.setOnMeasureListener(new OnMeasureListener() {
@Override
public void onMeasureSize(int width, int height) {
mPoint.set(width, height);
}
});
} else {
mViewHolder = (ViewHolder) convertView.getTag();
}
String path = hasHeaderIdList.get(position).getPath();
mViewHolder.mImageView.setTag(path);
Bitmap bitmap = NativeImageLoader.getInstance().loadNativeImage(path, mPoint,
new NativeImageCallBack() {
@Override
public void onImageLoader(Bitmap bitmap, String path) {
ImageView mImageView = (ImageView) mGridView
.findViewWithTag(path);
if (bitmap != null && mImageView != null) {
mImageView.setImageBitmap(bitmap);
}
}
});
if (bitmap != null) {
mViewHolder.mImageView.setImageBitmap(bitmap);
} else {
mViewHolder.mImageView.setImageResource(R.drawable.friends_sends_pictures_no);
}
return convertView;
}
@Override
public View getHeaderView(int position, View convertView, ViewGroup parent) {
HeaderViewHolder mHeaderHolder;
if (convertView == null) {
mHeaderHolder = new HeaderViewHolder();
convertView = mInflater.inflate(R.layout.header, parent, false);
mHeaderHolder.mTextView = (TextView) convertView
.findViewById(R.id.header);
convertView.setTag(mHeaderHolder);
} else {
mHeaderHolder = (HeaderViewHolder) convertView.getTag();
}
mHeaderHolder.mTextView.setText(hasHeaderIdList.get(position).getTime());
return convertView;
}
/**
* 獲取HeaderId, 只要HeaderId不相等就新增一個Header
*/
@Override
public long getHeaderId(int position) {
return hasHeaderIdList.get(position).getHeaderId();
}
public static class ViewHolder {
public MyImageView mImageView;
}
public static class HeaderViewHolder {
public TextView mTextView;
}
}
除了要繼承BaseAdapter之外還需要實現StickyGridHeadersSimpleAdapter介面,繼承BaseAdapter需要實現getCount(),getItem(int position), getItemId(int position),getView(int position, View convertView, ViewGroup parent)這四個方法,這幾個方法的實現跟我們平常實現的方式一樣,主要是看一下getView()方法,我們將每個item的圖片路徑設定Tag到該ImageView上面,然後利用NativeImageLoader來載入本地圖片,在這裡使用的ImageView依然是自定義的MyImageView,該自定義ImageView主要實現當MyImageView測量完畢之後,就會將測量的寬和高回撥到onMeasureSize()中,然後我們可以根據MyImageView的大小來裁剪圖片另外我們需要實現StickyGridHeadersSimpleAdapter介面的getHeaderId(int position)和getHeaderView(int position, View convertView, ViewGroup parent),getHeaderId(int position)方法返回每個Item的headerId,getHeaderView()方法是生成sections和headers的,如果某個item的headerId跟他下一個item的HeaderId不同,則會呼叫getHeaderView方法生成一個sections用來區分不同的組,還會根據firstVisibleItem的headerId來生成一個位於頂部的headers,所以如何生成每個Item的headerId才是關鍵,生成headerId的方法在MainActivity中
package com.example.stickyheadergridview;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.TimeZone;
import android.app.Activity;
import android.app.ProgressDialog;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.MediaStore;
import android.widget.GridView;
import com.example.stickyheadergridview.ImageScanner.ScanCompleteCallBack;
public class MainActivity extends Activity {
private ProgressDialog mProgressDialog;
/**
* 圖片掃描器
*/
private ImageScanner mScanner;
private GridView mGridView;
/**
* 沒有HeaderId的List
*/
private List<GridItem> nonHeaderIdList = new ArrayList<GridItem>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGridView = (GridView) findViewById(R.id.asset_grid);
mScanner = new ImageScanner(this);
mScanner.scanImages(new ScanCompleteCallBack() {
{
mProgressDialog = ProgressDialog.show(MainActivity.this, null, "正在載入...");
}
@Override
public void scanComplete(Cursor cursor) {
// 關閉進度條
mProgressDialog.dismiss();
if(cursor == null){
return;
}
while (cursor.moveToNext()) {
// 獲取圖片的路徑
String path = cursor.getString(cursor
.getColumnIndex(MediaStore.Images.Media.DATA));
//獲取圖片的新增到系統的毫秒數
long times = cursor.getLong(cursor
.getColumnIndex(MediaStore.Images.Media.DATE_ADDED));
GridItem mGridItem = new GridItem(path, paserTimeToYMD(times, "yyyy年MM月dd日"));
nonHeaderIdList.add(mGridItem);
}
cursor.close();
//給GridView的item的資料生成HeaderId
List<GridItem> hasHeaderIdList = generateHeaderId(nonHeaderIdList);
//排序
Collections.sort(hasHeaderIdList, new YMDComparator());
mGridView.setAdapter(new StickyGridAdapter(MainActivity.this, hasHeaderIdList, mGridView));
}
});
}
/**
* 對GridView的Item生成HeaderId, 根據圖片的新增時間的年、月、日來生成HeaderId
* 年、月、日相等HeaderId就相同
* @param nonHeaderIdList
* @return
*/
private List<GridItem> generateHeaderId(List<GridItem> nonHeaderIdList) {
Map<String, Integer> mHeaderIdMap = new HashMap<String, Integer>();
int mHeaderId = 1;
List<GridItem> hasHeaderIdList;
for(ListIterator<GridItem> it = nonHeaderIdList.listIterator(); it.hasNext();){
GridItem mGridItem = it.next();
String ymd = mGridItem.getTime();
if(!mHeaderIdMap.containsKey(ymd)){
mGridItem.setHeaderId(mHeaderId);
mHeaderIdMap.put(ymd, mHeaderId);
mHeaderId ++;
}else{
mGridItem.setHeaderId(mHeaderIdMap.get(ymd));
}
}
hasHeaderIdList = nonHeaderIdList;
return hasHeaderIdList;
}
@Override
protected void onDestroy() {
super.onDestroy();
//退出頁面清除LRUCache中的Bitmap佔用的記憶體
NativeImageLoader.getInstance().trimMemCache();
}
/**
* 將毫秒數裝換成pattern這個格式,我這裡是轉換成年月日
* @param time
* @param pattern
* @return
*/
public static String paserTimeToYMD(long time, String pattern ) {
System.setProperty("user.timezone", "Asia/Shanghai");
TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone.setDefault(tz);
SimpleDateFormat format = new SimpleDateFormat(pattern);
return format.format(new Date(time * 1000L));
}
}
主介面的程式碼主要是組裝StickyGridHeadersGridView的資料,我們將掃描出來的圖片的路徑,時間的毫秒數解析成年月日的格式封裝到GridItem中,然後將GridItem加入到List中,此時每個Item還沒有生成headerId,我們需要呼叫generateHeaderId(),該方法主要是將同一天加入的系統的圖片生成相同的HeaderId,這樣子同一天加入的圖片就在一個組中,當然你要改成同一個月的圖片在一起,修改paserTimeToYMD()方法的第二個引數就行了,當Activity finish之後,我們利用NativeImageLoader.getInstance().trimMemCache()釋放記憶體,當然我們還需要對GridView的資料進行排序,比如說headerId相同的item不連續,headerId相同的item就會生成多個sections(即多個分組),所以我們要利用YMDComparator使得在同一天加入的圖片在一起,YMDComparator的程式碼如下
package com.example.stickyheadergridview;
import java.util.Comparator;
public class YMDComparator implements Comparator<GridItem> {
@Override
public int compare(GridItem o1, GridItem o2) {
return o1.getTime().compareTo(o2.getTime());
}
}
當然這篇文章不使用YMDComparator也是可以的,因為我在利用ContentProvider獲取圖片的時候,就是根據加入系統的時間排序的,排序只是針對一般的資料來說的。接下來我們執行下程式看看效果如何
今天的文章就到這裡結束了,感謝大家的觀看,上面還有一個類和一些資原始檔沒有貼出來,大家有興趣研究下就直接下載專案原始碼,記住採用LruCache快取圖片的時候,cacheSize不要設定得過大,不然產生OOM的概率就更大些,我利用上面的程式測試顯示600多張圖片來回滑動,沒有產生OOM,有問題不明白的同學可以在下面留言!