轉載請註明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/20481185),請尊重他人的辛勤勞動成果,謝謝!
大家好!過完年回來到現在差不多一個月沒寫文章了,一是覺得不知道寫哪些方面的文章,沒有好的題材來寫,二是因爲自己的一些私事給耽誤了,所以過完年的第一篇文章到現在才發表出來,2014年我還是會繼續在CSDN上面更新我的博客,歡迎大家關注一下,今天這篇文章主要的是介紹下開源庫StickyGridHeaders的使用,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;
- /**
- * @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);
- }
- }
- 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);
- }
- }
我們看主界面的佈局代碼,裏面只有一個自定義的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;
- }
- }
另外我們需要實現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());
- }
- }
接下來我們運行下程序看看效果如何
今天的文章就到這裏結束了,感謝大家的觀看,上面還有一個類和一些資源文件沒有貼出來,大家有興趣研究下就直接下載項目源碼,記住採用LruCache緩存圖片的時候,cacheSize不要設置得過大,不然產生OOM的概率就更大些,我利用上面的程序測試顯示600多張圖片來回滑動,沒有產生OOM,有問題不明白的同學可以在下面留言!