我是轉載的,原作地址:http://blog.csdn.net/xiaanming/article/details/20481185
轉載請註明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/20481185),請尊重他人的辛勤勞動成果,謝謝!
StickyGridHeaders是一個自定義GridView帶sections和headers的Android庫,sections就是GridView
item之間的分隔,headers就是固定在GridView頂部的標題,類似一些Android手機聯繫人的效果,StickyGridHeaders的介紹在https://github.com/TonicArtos/StickyGridHeaders,與此對應也有一個相同效果的自定義ListView帶sections和headers的開源庫https://github.com/emilsjolander/StickyListHeaders,大家有興趣的可以去看下,我這裏介紹的是StickyGridHeaders的使用,我在Android應用方面看到使用StickyGridHeaders的不是很多,而是在Iphone上看到相冊採用的是這種效果,於是我就使用StickyGridHeaders來仿照Iphone按照日期分隔顯示本地圖片
我們先新建一個Android項目StickyHeaderGridView,去https://github.com/TonicArtos/StickyGridHeaders下載開源庫,爲了方便瀏覽源碼我直接將源碼拷到我的工程中了
com.tonicartos.widget.stickygridheaders這個包就是我放StickyGridHeaders開源庫的源碼,com.example.stickyheadergridview這個包是我實現此功能的代碼,類看起來還蠻多的,下面我就一一來介紹了
GridItem用來封裝StickyGridHeadersGridView 每個Item的數據,裏面有本地圖片的路徑,圖片加入手機系統的時間和headerId
-
package com.example.stickyheadergridview;
-
-
-
-
-
-
-
public class GridItem {
-
-
-
-
private String path;
-
-
-
-
private String time;
-
-
-
-
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;
-
}
-
-
-
}
圖片的路徑path和圖片加入的時間time 我們直接可以通過ContentProvider獲取,但是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;
-
-
-
-
-
-
-
public class ImageScanner {
-
private Context mContext;
-
-
public ImageScanner(Context context){
-
this.mContext = context;
-
}
-
-
-
-
-
-
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() {
-
-
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);
-
-
-
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;
-
-
-
-
-
-
-
-
-
-
-
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());
-
-
-
final int cacheSize = maxMemory / 8;
-
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
-
-
-
@Override
-
protected int sizeOf(String key, Bitmap bitmap) {
-
return bitmap.getRowBytes() * bitmap.getHeight();
-
}
-
-
};
-
}
-
-
-
-
-
-
public static NativeImageLoader getInstance(){
-
return mInstance;
-
}
-
-
-
-
-
-
-
-
-
public Bitmap loadNativeImage(final String path, final NativeImageCallBack mCallBack){
-
return this.loadNativeImage(path, null, mCallBack);
-
}
-
-
-
-
-
-
-
-
-
-
public Bitmap loadNativeImage(final String path, final Point mPoint, final NativeImageCallBack mCallBack){
-
-
Bitmap bitmap = getBitmapFromMemCache(path);
-
-
final Handler mHander = new Handler(){
-
-
@Override
-
public void handleMessage(Message msg) {
-
super.handleMessage(msg);
-
mCallBack.onImageLoader((Bitmap)msg.obj, path);
-
}
-
-
};
-
-
-
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;
-
-
}
-
-
-
-
-
-
-
-
-
-
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
-
if (getBitmapFromMemCache(key) == null && bitmap != null) {
-
mMemoryCache.put(key, bitmap);
-
}
-
}
-
-
-
-
-
-
-
private Bitmap getBitmapFromMemCache(String key) {
-
Bitmap bitmap = mMemoryCache.get(key);
-
-
if(bitmap != null){
-
Log.i(TAG, "get image for LRUCache , path = " + key);
-
}
-
return bitmap;
-
}
-
-
-
-
-
public void trimMemCache(){
-
mMemoryCache.evictAll();
-
}
-
-
-
-
-
-
-
-
-
-
private Bitmap decodeThumbBitmapForFile(String path, int viewWidth, int viewHeight){
-
BitmapFactory.Options options = new BitmapFactory.Options();
-
-
options.inJustDecodeBounds = true;
-
BitmapFactory.decodeFile(path, options);
-
-
options.inSampleSize = computeScale(options, viewWidth, viewHeight);
-
-
-
options.inJustDecodeBounds = false;
-
-
-
Log.e(TAG, "get Iamge form file, path = " + path);
-
-
return BitmapFactory.decodeFile(path, options);
-
}
-
-
-
-
-
-
-
-
-
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;
-
-
-
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;
-
}
-
-
-
-
-
-
-
-
-
public interface NativeImageCallBack{
-
-
-
-
-
-
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;
-
-
-
-
-
-
-
-
-
-
public class StickyGridAdapter extends BaseAdapter implements
-
StickyGridHeadersSimpleAdapter {
-
-
private List<GridItem> hasHeaderIdList;
-
private LayoutInflater mInflater;
-
private GridView mGridView;
-
private Point mPoint = new Point(0, 0);
-
-
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);
-
-
-
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;
-
}
-
-
-
-
-
@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;
-
-
-
-
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();
-
-
-
List<GridItem> hasHeaderIdList = generateHeaderId(nonHeaderIdList);
-
-
Collections.sort(hasHeaderIdList, new YMDComparator());
-
mGridView.setAdapter(new StickyGridAdapter(MainActivity.this, hasHeaderIdList, mGridView));
-
-
}
-
});
-
}
-
-
-
-
-
-
-
-
-
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();
-
-
NativeImageLoader.getInstance().trimMemCache();
-
}
-
-
-
-
-
-
-
-
-
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,有問題不明白的同學可以在下面留言!
項目源碼,點擊下載